Skip to content

Commit 6a505c2

Browse files
committed
fix: clean mcp ui and eng-104 flow
Signed-off-by: betterclever <[email protected]>
1 parent 6b0ca4a commit 6a505c2

File tree

5 files changed

+135
-112
lines changed

5 files changed

+135
-112
lines changed

e2e-tests/eng-104-alert-investigation.test.ts

Lines changed: 64 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -111,22 +111,42 @@ async function runWorkflow(workflowId: string, inputs: Record<string, unknown> =
111111
return runId;
112112
}
113113

114-
async function createSecret(name: string, value: string) {
115-
const res = await fetch(`${API_BASE}/secrets`, {
116-
method: 'POST',
117-
headers: HEADERS,
118-
body: JSON.stringify({ name, value }),
119-
});
114+
async function listSecrets(): Promise<Array<{ id: string; name: string }>> {
115+
const res = await fetch(`${API_BASE}/secrets`, { headers: HEADERS });
120116
if (!res.ok) {
121117
const text = await res.text();
122-
throw new Error(`Failed to create secret: ${res.status} ${text}`);
118+
throw new Error(`Failed to list secrets: ${res.status} ${text}`);
123119
}
124-
const secret = await res.json();
125-
return secret.id as string;
120+
return res.json();
126121
}
127122

128-
async function deleteSecret(secretId: string) {
129-
await fetch(`${API_BASE}/secrets/${secretId}`, { method: 'DELETE', headers: HEADERS });
123+
async function createOrRotateSecret(name: string, value: string): Promise<string> {
124+
const secrets = await listSecrets();
125+
const existing = secrets.find((s) => s.name === name);
126+
if (!existing) {
127+
const res = await fetch(`${API_BASE}/secrets`, {
128+
method: 'POST',
129+
headers: HEADERS,
130+
body: JSON.stringify({ name, value }),
131+
});
132+
if (!res.ok) {
133+
const text = await res.text();
134+
throw new Error(`Failed to create secret: ${res.status} ${text}`);
135+
}
136+
const secret = await res.json();
137+
return secret.id as string;
138+
}
139+
140+
const res = await fetch(`${API_BASE}/secrets/${existing.id}/rotate`, {
141+
method: 'PUT',
142+
headers: HEADERS,
143+
body: JSON.stringify({ value }),
144+
});
145+
if (!res.ok) {
146+
const text = await res.text();
147+
throw new Error(`Failed to rotate secret: ${res.status} ${text}`);
148+
}
149+
return existing.id;
130150
}
131151

132152
function loadGuardDutySample() {
@@ -145,18 +165,17 @@ e2eDescribe('ENG-104: End-to-End Alert Investigation Workflow', () => {
145165
e2eTest('triage workflow runs end-to-end with MCP tools + OpenCode agent', { timeout: 480000 }, async () => {
146166
const now = Date.now();
147167

148-
const abuseSecretId = await createSecret(`ENG104_ABUSE_${now}`, ABUSEIPDB_API_KEY!);
149-
const vtSecretId = await createSecret(`ENG104_VT_${now}`, VIRUSTOTAL_API_KEY!);
150-
const zaiSecretId = await createSecret(`ENG104_ZAI_${now}`, ZAI_API_KEY!);
168+
const abuseSecretName = `ENG104_ABUSE_${now}`;
169+
const vtSecretName = `ENG104_VT_${now}`;
170+
const zaiSecretName = `ENG104_ZAI_${now}`;
171+
const awsAccessKeyName = `ENG104_AWS_ACCESS_${now}`;
172+
const awsSecretKeyName = `ENG104_AWS_SECRET_${now}`;
151173

152-
// For AWS MCP components, credentials are contract-based (object, not secret reference)
153-
// So we pass the credential object directly instead of a secret ID
154-
const awsCredentials = {
155-
accessKeyId: AWS_ACCESS_KEY_ID,
156-
secretAccessKey: AWS_SECRET_ACCESS_KEY,
157-
sessionToken: AWS_SESSION_TOKEN,
158-
region: AWS_REGION,
159-
};
174+
await createOrRotateSecret(abuseSecretName, ABUSEIPDB_API_KEY!);
175+
await createOrRotateSecret(vtSecretName, VIRUSTOTAL_API_KEY!);
176+
await createOrRotateSecret(zaiSecretName, ZAI_API_KEY!);
177+
await createOrRotateSecret(awsAccessKeyName, AWS_ACCESS_KEY_ID!);
178+
await createOrRotateSecret(awsSecretKeyName, AWS_SECRET_ACCESS_KEY!);
160179

161180
const guardDutyAlert = loadGuardDutySample();
162181

@@ -178,34 +197,6 @@ e2eDescribe('ENG-104: End-to-End Alert Investigation Workflow', () => {
178197
},
179198
},
180199
},
181-
{
182-
id: 'parse',
183-
type: 'core.logic.script',
184-
position: { x: 250, y: 0 },
185-
data: {
186-
label: 'Parse Alert',
187-
config: {
188-
params: {
189-
variables: [
190-
{ name: 'alert', type: 'json' },
191-
],
192-
returns: [
193-
{ name: 'suspiciousIp', type: 'string' },
194-
{ name: 'publicIp', type: 'string' },
195-
{ name: 'instanceId', type: 'string' },
196-
],
197-
code: `export async function script(input: Input): Promise<Output> {
198-
const alert = input.alert || {};
199-
const portProbe = alert?.service?.action?.portProbeAction?.portProbeDetails || [];
200-
const suspiciousIp = portProbe[0]?.remoteIpDetails?.ipAddressV4 || alert?.intel?.ip || '';
201-
const publicIp = alert?.resource?.instanceDetails?.publicIp || '';
202-
const instanceId = alert?.resource?.instanceDetails?.instanceId || '';
203-
return { suspiciousIp, publicIp, instanceId };
204-
}`,
205-
},
206-
},
207-
},
208-
},
209200
{
210201
id: 'abuseipdb',
211202
type: 'security.abuseipdb.check',
@@ -216,7 +207,7 @@ e2eDescribe('ENG-104: End-to-End Alert Investigation Workflow', () => {
216207
mode: 'tool',
217208
params: { maxAgeInDays: 90 },
218209
inputOverrides: {
219-
apiKey: abuseSecretId,
210+
apiKey: abuseSecretName,
220211
ipAddress: '',
221212
},
222213
},
@@ -232,12 +223,28 @@ e2eDescribe('ENG-104: End-to-End Alert Investigation Workflow', () => {
232223
mode: 'tool',
233224
params: { type: 'ip' },
234225
inputOverrides: {
235-
apiKey: vtSecretId,
226+
apiKey: vtSecretName,
236227
indicator: '',
237228
},
238229
},
239230
},
240231
},
232+
{
233+
id: 'aws-creds',
234+
type: 'core.credentials.aws',
235+
position: { x: 520, y: 200 },
236+
data: {
237+
label: 'AWS Credentials Bundle',
238+
config: {
239+
params: {},
240+
inputOverrides: {
241+
accessKeyId: awsAccessKeyName,
242+
secretAccessKey: awsSecretKeyName,
243+
region: AWS_REGION,
244+
},
245+
},
246+
},
247+
},
241248
{
242249
id: 'cloudtrail',
243250
type: 'security.aws-cloudtrail-mcp',
@@ -250,9 +257,7 @@ e2eDescribe('ENG-104: End-to-End Alert Investigation Workflow', () => {
250257
image: AWS_CLOUDTRAIL_MCP_IMAGE,
251258
region: AWS_REGION,
252259
},
253-
inputOverrides: {
254-
credentials: awsCredentials,
255-
},
260+
inputOverrides: {},
256261
},
257262
},
258263
},
@@ -268,9 +273,7 @@ e2eDescribe('ENG-104: End-to-End Alert Investigation Workflow', () => {
268273
image: AWS_CLOUDWATCH_MCP_IMAGE,
269274
region: AWS_REGION,
270275
},
271-
inputOverrides: {
272-
credentials: awsCredentials,
273-
},
276+
inputOverrides: {},
274277
},
275278
},
276279
},
@@ -302,16 +305,15 @@ e2eDescribe('ENG-104: End-to-End Alert Investigation Workflow', () => {
302305
},
303306
],
304307
edges: [
305-
{ id: 'e1', source: 'start', target: 'parse', sourceHandle: 'alert', targetHandle: 'alert' },
306308
{ id: 'e2', source: 'start', target: 'agent' },
307309

308310
{ id: 't1', source: 'abuseipdb', target: 'agent', sourceHandle: 'tools', targetHandle: 'tools' },
309311
{ id: 't2', source: 'virustotal', target: 'agent', sourceHandle: 'tools', targetHandle: 'tools' },
310312
{ id: 't3', source: 'cloudtrail', target: 'agent', sourceHandle: 'tools', targetHandle: 'tools' },
311313
{ id: 't4', source: 'cloudwatch', target: 'agent', sourceHandle: 'tools', targetHandle: 'tools' },
312314

313-
{ id: 'd1', source: 'parse', target: 'abuseipdb', sourceHandle: 'suspiciousIp', targetHandle: 'ipAddress' },
314-
{ id: 'd2', source: 'parse', target: 'virustotal', sourceHandle: 'suspiciousIp', targetHandle: 'indicator' },
315+
{ id: 'a1', source: 'aws-creds', target: 'cloudtrail', sourceHandle: 'credentials', targetHandle: 'credentials' },
316+
{ id: 'a2', source: 'aws-creds', target: 'cloudwatch', sourceHandle: 'credentials', targetHandle: 'credentials' },
315317
],
316318
};
317319

@@ -340,8 +342,6 @@ e2eDescribe('ENG-104: End-to-End Alert Investigation Workflow', () => {
340342
}
341343
}
342344

343-
await deleteSecret(abuseSecretId);
344-
await deleteSecret(vtSecretId);
345-
await deleteSecret(zaiSecretId);
345+
// Leave secrets for reuse across runs; rotation already updated values.
346346
});
347347
});

frontend/src/components/workflow/ConfigPanel.tsx

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1198,12 +1198,34 @@ export function ConfigPanel({
11981198
</CollapsibleSection>
11991199
)}
12001200

1201-
{!isToolMode && component.agentTool?.enabled && toolSchemaJson && (
1202-
<CollapsibleSection title="Tool Schema" defaultOpen={false}>
1203-
<div className="mt-2">
1204-
<pre className="text-[11px] font-mono whitespace-pre-wrap bg-muted/20 text-foreground p-3 rounded-md border border-border shadow-sm min-h-[40px] max-h-[300px] overflow-y-auto">
1205-
{toolSchemaJson}
1206-
</pre>
1201+
{!isToolMode &&
1202+
component.agentTool?.enabled &&
1203+
toolSchemaJson &&
1204+
component.category !== 'mcp' && (
1205+
<CollapsibleSection title="Tool Schema" defaultOpen={false}>
1206+
<div className="mt-2">
1207+
<pre className="text-[11px] font-mono whitespace-pre-wrap bg-muted/20 text-foreground p-3 rounded-md border border-border shadow-sm min-h-[40px] max-h-[300px] overflow-y-auto">
1208+
{toolSchemaJson}
1209+
</pre>
1210+
</div>
1211+
</CollapsibleSection>
1212+
)}
1213+
1214+
{component.category === 'mcp' && component.agentTool?.toolName && (
1215+
<CollapsibleSection title="MCP Server" defaultOpen={false}>
1216+
<div className="mt-2 space-y-2 text-xs text-muted-foreground">
1217+
<div>
1218+
<span className="font-medium text-foreground">Tool name: </span>
1219+
<span className="font-mono">{component.agentTool.toolName}</span>
1220+
</div>
1221+
{component.agentTool.toolDescription && (
1222+
<div className="text-[11px] leading-relaxed">
1223+
{component.agentTool.toolDescription}
1224+
</div>
1225+
)}
1226+
<div className="text-[11px] italic">
1227+
Tool list appears after the MCP server starts at runtime.
1228+
</div>
12071229
</div>
12081230
</CollapsibleSection>
12091231
)}

frontend/src/components/workflow/node/WorkflowNode.tsx

Lines changed: 25 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -582,34 +582,31 @@ export const WorkflowNode = ({ data, selected, id }: NodeProps<NodeData>) => {
582582
)}
583583
</div>
584584
<div className="flex items-center gap-1">
585-
{mode === 'design' && !isEntryPoint && component?.agentTool?.enabled && (
586-
<button
587-
type="button"
588-
onClick={toggleToolMode}
589-
disabled={isToolModeOnly}
590-
className={cn(
591-
'flex items-center gap-1.5 px-2 py-1 rounded transition-all border',
592-
isToolModeOnly && 'opacity-70 cursor-not-allowed',
593-
isToolMode
594-
? 'bg-purple-600 text-white border-purple-500 shadow-sm'
595-
: 'bg-background text-muted-foreground/60 border-border hover:border-purple-400 hover:text-purple-600',
596-
)}
597-
title={
598-
isToolModeOnly
599-
? 'Tool Mode Only'
600-
: isToolMode
601-
? 'Disable Tool Mode'
602-
: 'Enable Tool Mode'
603-
}
604-
>
605-
<Hammer
606-
className={cn('h-3.5 w-3.5', isToolMode ? 'text-white' : 'text-current')}
607-
/>
608-
<span className="text-[10px] font-bold uppercase tracking-tight">
609-
{isToolMode ? 'Tool' : 'Mode'}
610-
</span>
611-
</button>
612-
)}
585+
{mode === 'design' &&
586+
!isEntryPoint &&
587+
component?.agentTool?.enabled &&
588+
!isToolModeOnly &&
589+
componentCategory !== 'mcp' && (
590+
<button
591+
type="button"
592+
onClick={toggleToolMode}
593+
disabled={isToolModeOnly}
594+
className={cn(
595+
'flex items-center gap-1.5 px-2 py-1 rounded transition-all border',
596+
isToolMode
597+
? 'bg-purple-600 text-white border-purple-500 shadow-sm'
598+
: 'bg-background text-muted-foreground/60 border-border hover:border-purple-400 hover:text-purple-600',
599+
)}
600+
title={isToolMode ? 'Disable Tool Mode' : 'Enable Tool Mode'}
601+
>
602+
<Hammer
603+
className={cn('h-3.5 w-3.5', isToolMode ? 'text-white' : 'text-current')}
604+
/>
605+
<span className="text-[10px] font-bold uppercase tracking-tight">
606+
{isToolMode ? 'Tool' : 'Mode'}
607+
</span>
608+
</button>
609+
)}
613610
{isEntryPoint && (
614611
<div style={{ display: 'none' }}>
615612
<WebhookDetails

frontend/src/features/workflow-builder/hooks/useDesignWorkflowPersistence.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,17 @@ export function useDesignWorkflowPersistence({
8585
const [lastSavedMetadata, setLastSavedMetadata] = useState<SavedMetadata | null>(null);
8686
const [hasGraphChanges, setHasGraphChanges] = useState(false);
8787
const [hasMetadataChanges, setHasMetadataChanges] = useState(false);
88+
const secretsInitialized = useSecretStore((state) => state.initialized);
89+
const secretsLoading = useSecretStore((state) => state.loading);
90+
const fetchSecrets = useSecretStore((state) => state.fetchSecrets);
91+
92+
useEffect(() => {
93+
if (!secretsInitialized && !secretsLoading) {
94+
fetchSecrets().catch((error) => {
95+
console.error('Failed to preload secrets:', error);
96+
});
97+
}
98+
}, [fetchSecrets, secretsInitialized, secretsLoading]);
8899

89100
useEffect(() => {
90101
const currentSignature = computeGraphSignature(designNodes, designEdges);
@@ -150,6 +161,7 @@ export function useDesignWorkflowPersistence({
150161

151162
// --- NEW: VALIDATION CHECK ---
152163
const getComponent = useComponentStore.getState().getComponent;
164+
await useSecretStore.getState().fetchSecrets();
153165
const secrets = useSecretStore.getState().secrets;
154166
const allIssues: string[] = [];
155167

worker/src/temporal/workflows/index.ts

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -973,20 +973,12 @@ export async function shipsecWorkflowRun(
973973
[{ outputs, error: normalizedError.message }],
974974
);
975975
} finally {
976-
// Only cleanup MCP tools if workflow failed or was cancelled
977-
// For successful completion, tools remain in registry for gateway access
978-
if (!workflowCompletedSuccessfully) {
979-
console.log(
980-
`[Workflow] Workflow did not complete successfully, cleaning up MCP containers for run ${input.runId}`,
981-
);
982-
await cleanupLocalMcpActivity({ runId: input.runId }).catch((err) => {
983-
console.error(`[Workflow] Failed to cleanup MCP containers for run ${input.runId}`, err);
984-
});
985-
} else {
986-
console.log(
987-
`[Workflow] Workflow completed successfully, keeping MCP tools available for run ${input.runId}`,
988-
);
989-
}
976+
console.log(
977+
`[Workflow] Cleaning up MCP containers for run ${input.runId} (success=${workflowCompletedSuccessfully})`,
978+
);
979+
await cleanupLocalMcpActivity({ runId: input.runId }).catch((err) => {
980+
console.error(`[Workflow] Failed to cleanup MCP containers for run ${input.runId}`, err);
981+
});
990982
await finalizeRunActivity({ runId: input.runId }).catch((err) => {
991983
console.error(`[Workflow] Failed to finalize run ${input.runId}`, err);
992984
});

0 commit comments

Comments
 (0)