stageflow 0.36.0 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -26
- package/dist/agent/claudeAdapter.js +21 -1
- package/dist/agent/claudeTools.d.ts +13 -0
- package/dist/agent/claudeTools.js +98 -0
- package/dist/agent/piAdapter.d.ts +9 -1
- package/dist/agent/piAdapter.js +59 -18
- package/dist/agent/piIsolatedMcp.js +14 -2
- package/dist/agent/piIsolatedMcpProbe.d.ts +5 -1
- package/dist/agent/piIsolatedMcpProbe.js +16 -30
- package/dist/agent/port.d.ts +5 -0
- package/dist/agent/sandboxPiTools.d.ts +99 -0
- package/dist/agent/sandboxPiTools.js +109 -0
- package/dist/agents/agentId.d.ts +8 -0
- package/dist/agents/agentId.js +27 -0
- package/dist/agents/approvalBroker.d.ts +16 -0
- package/dist/agents/approvalBroker.js +45 -0
- package/dist/agents/authorChat.d.ts +19 -0
- package/dist/agents/authorChat.js +63 -0
- package/dist/agents/chatTurn.d.ts +23 -0
- package/dist/agents/chatTurn.js +77 -0
- package/dist/agents/communications/agentmail.d.ts +19 -0
- package/dist/agents/communications/agentmail.js +46 -0
- package/dist/agents/communications/agentmailListener.d.ts +59 -0
- package/dist/agents/communications/agentmailListener.js +183 -0
- package/dist/agents/communications/allowlist.d.ts +10 -0
- package/dist/agents/communications/allowlist.js +36 -0
- package/dist/agents/communications/channelConnections.d.ts +3 -0
- package/dist/agents/communications/channelConnections.js +51 -0
- package/dist/agents/communications/delivery.d.ts +53 -0
- package/dist/agents/communications/delivery.js +142 -0
- package/dist/agents/communications/email.d.ts +26 -0
- package/dist/agents/communications/email.js +20 -0
- package/dist/agents/communications/identity.d.ts +13 -0
- package/dist/agents/communications/identity.js +18 -0
- package/dist/agents/communications/imessageListener.d.ts +41 -0
- package/dist/agents/communications/imessageListener.js +92 -0
- package/dist/agents/communications/inbound.d.ts +63 -0
- package/dist/agents/communications/inbound.js +143 -0
- package/dist/agents/communications/listener.d.ts +36 -0
- package/dist/agents/communications/listener.js +38 -0
- package/dist/agents/communications/photon.d.ts +19 -0
- package/dist/agents/communications/photon.js +38 -0
- package/dist/agents/communications/port.d.ts +27 -0
- package/dist/agents/communications/port.js +6 -0
- package/dist/agents/communications/slack.d.ts +28 -0
- package/dist/agents/communications/slack.js +65 -0
- package/dist/agents/communications/supervisor.d.ts +36 -0
- package/dist/agents/communications/supervisor.js +96 -0
- package/dist/agents/communications/telegram.d.ts +19 -0
- package/dist/agents/communications/telegram.js +52 -0
- package/dist/agents/definitionSummary.d.ts +13 -0
- package/dist/agents/definitionSummary.js +91 -0
- package/dist/agents/delegate.d.ts +3 -0
- package/dist/agents/delegate.js +8 -0
- package/dist/agents/errors.d.ts +10 -0
- package/dist/agents/errors.js +32 -0
- package/dist/agents/guardrails.d.ts +24 -0
- package/dist/agents/guardrails.js +39 -0
- package/dist/agents/historyFit.d.ts +10 -0
- package/dist/agents/historyFit.js +26 -0
- package/dist/agents/hostContext.d.ts +54 -0
- package/dist/agents/hostContext.js +156 -0
- package/dist/agents/ledgers.d.ts +29 -0
- package/dist/agents/ledgers.js +31 -0
- package/dist/agents/lineDiff.d.ts +12 -0
- package/dist/agents/lineDiff.js +46 -0
- package/dist/agents/ndjson.d.ts +9 -0
- package/dist/agents/ndjson.js +47 -0
- package/dist/agents/otlpExport.d.ts +9 -0
- package/dist/agents/otlpExport.js +58 -0
- package/dist/agents/otlpSink.d.ts +7 -0
- package/dist/agents/otlpSink.js +23 -0
- package/dist/agents/policy.d.ts +31 -0
- package/dist/agents/policy.js +84 -0
- package/dist/agents/projectRoot.d.ts +5 -0
- package/dist/agents/projectRoot.js +15 -0
- package/dist/agents/runtime/piRuntime.d.ts +52 -0
- package/dist/agents/runtime/piRuntime.js +474 -0
- package/dist/agents/runtime/types.d.ts +77 -0
- package/dist/agents/runtime/types.js +26 -0
- package/dist/agents/sandbox.d.ts +17 -0
- package/dist/agents/sandbox.js +10 -0
- package/dist/agents/scheduleSource.d.ts +32 -0
- package/dist/agents/scheduleSource.js +100 -0
- package/dist/agents/service.d.ts +298 -0
- package/dist/agents/service.js +1105 -0
- package/dist/agents/sessions.d.ts +38 -0
- package/dist/agents/sessions.js +321 -0
- package/dist/agents/store.d.ts +277 -0
- package/dist/agents/store.js +643 -0
- package/dist/agents/trace.d.ts +27 -0
- package/dist/agents/trace.js +29 -0
- package/dist/agents/turnLifecycle.d.ts +72 -0
- package/dist/agents/turnLifecycle.js +157 -0
- package/dist/agents/webChat/publicChat.d.ts +36 -0
- package/dist/agents/webChat/publicChat.js +290 -0
- package/dist/agents/webChat/rateLimit.d.ts +15 -0
- package/dist/agents/webChat/rateLimit.js +33 -0
- package/dist/agents/webChat/slug.d.ts +2 -0
- package/dist/agents/webChat/slug.js +9 -0
- package/dist/channels/providers/agentmail.js +14 -1
- package/dist/channels/providers/photonImessage.js +36 -0
- package/dist/channels/providers/slack.js +2 -1
- package/dist/channels/providers/telegram.js +2 -1
- package/dist/channels/types.d.ts +27 -0
- package/dist/cli/agentsCommand.d.ts +14 -0
- package/dist/cli/agentsCommand.js +291 -0
- package/dist/cli.js +9 -0
- package/dist/config/browseCatalog.js +4 -11
- package/dist/config/draftPackage.d.ts +1 -0
- package/dist/config/draftPackage.js +4 -0
- package/dist/config/hostConfig.js +9 -0
- package/dist/config/loadAgent.d.ts +266 -0
- package/dist/config/loadAgent.js +374 -0
- package/dist/config/loadPipeline.js +5 -2
- package/dist/config/loadTask.js +34 -1
- package/dist/config/mcpHostCatalog.d.ts +50 -0
- package/dist/config/mcpHostCatalog.js +405 -0
- package/dist/config/mcpOAuth.d.ts +37 -0
- package/dist/config/mcpOAuth.js +263 -0
- package/dist/config/mcpRedaction.d.ts +42 -0
- package/dist/config/mcpRedaction.js +314 -0
- package/dist/config/mcpToolCache.d.ts +20 -0
- package/dist/config/mcpToolCache.js +43 -0
- package/dist/config/mergePipelineIncludes.d.ts +2 -0
- package/dist/config/mergePipelineIncludes.js +25 -0
- package/dist/config/resolveStageMcpServers.d.ts +35 -3
- package/dist/config/resolveStageMcpServers.js +124 -9
- package/dist/config/validateCatalog.d.ts +1 -1
- package/dist/config/validateCatalog.js +22 -1
- package/dist/mcp/agentTools.d.ts +8 -0
- package/dist/mcp/agentTools.js +132 -0
- package/dist/mcp/catalogTools.js +17 -10
- package/dist/mcp/deps.d.ts +6 -0
- package/dist/mcp/pipelineAuthorSupport.d.ts +19 -0
- package/dist/mcp/pipelineAuthorSupport.js +10 -0
- package/dist/mcp/pipelineAuthorTools.d.ts +8 -0
- package/dist/mcp/pipelineAuthorTools.js +109 -0
- package/dist/mcp/server.js +5 -0
- package/dist/mcp/tools.js +4 -0
- package/dist/mcp/triggerTools.js +32 -0
- package/dist/memory/access.d.ts +36 -0
- package/dist/memory/access.js +116 -0
- package/dist/memory/agentIssues.d.ts +3 -0
- package/dist/memory/agentIssues.js +23 -0
- package/dist/memory/bashDeny.d.ts +6 -0
- package/dist/memory/bashDeny.js +35 -0
- package/dist/memory/host.d.ts +51 -0
- package/dist/memory/host.js +394 -0
- package/dist/memory/ids.d.ts +15 -0
- package/dist/memory/ids.js +31 -0
- package/dist/memory/worker.d.ts +1 -0
- package/dist/memory/worker.js +84 -0
- package/dist/models/catalog.d.ts +113 -0
- package/dist/models/catalog.js +238 -0
- package/dist/models/response.d.ts +29 -0
- package/dist/models/response.js +25 -0
- package/dist/operatorAgent/agentAuthorContext.d.ts +17 -0
- package/dist/operatorAgent/agentAuthorContext.js +32 -0
- package/dist/operatorAgent/fakeHost.js +2 -0
- package/dist/operatorAgent/host.js +6 -3
- package/dist/operatorAgent/index.d.ts +4 -0
- package/dist/operatorAgent/index.js +4 -0
- package/dist/operatorAgent/piModel.js +2 -0
- package/dist/operatorAgent/profiles/agentAuthor.d.ts +11 -0
- package/dist/operatorAgent/profiles/agentAuthor.js +192 -0
- package/dist/operatorAgent/profiles/agentAuthorPlaybook.d.ts +1 -0
- package/dist/operatorAgent/profiles/agentAuthorPlaybook.js +29 -0
- package/dist/operatorAgent/profiles/workshopAuthor.js +2 -2
- package/dist/operatorAgent/profiles/workshopAuthorTools.js +3 -3
- package/dist/operatorAgent/proposals.d.ts +1 -0
- package/dist/operatorAgent/proposals.js +23 -2
- package/dist/package-meta.d.ts +1 -1
- package/dist/package-meta.js +1 -1
- package/dist/preflight/pipelinePreflight.js +1 -0
- package/dist/runstore/createStore.d.ts +2 -0
- package/dist/runstore/createStore.js +8 -0
- package/dist/runstore/overviewSummary.d.ts +56 -0
- package/dist/runstore/overviewSummary.js +134 -0
- package/dist/runstore/port.d.ts +3 -0
- package/dist/runstore/sqlite/SqliteRunStore.d.ts +2 -0
- package/dist/runstore/sqlite/SqliteRunStore.js +18 -0
- package/dist/runstore/sqlite/migrations/013-agents.d.ts +12 -0
- package/dist/runstore/sqlite/migrations/013-agents.js +156 -0
- package/dist/runstore/sqlite/migrations/014-agent-deliveries.d.ts +11 -0
- package/dist/runstore/sqlite/migrations/014-agent-deliveries.js +32 -0
- package/dist/runstore/sqlite/migrations/index.js +4 -0
- package/dist/runtime/gitCheckoutCapability.d.ts +1 -0
- package/dist/runtime/gitCheckoutCapability.js +1 -1
- package/dist/runtime/gitWorktreeCheckout.d.ts +22 -0
- package/dist/runtime/gitWorktreeCheckout.js +120 -0
- package/dist/runtime/sandboxContainer.d.ts +62 -0
- package/dist/runtime/sandboxContainer.js +151 -0
- package/dist/runtime/sandboxDesk.d.ts +28 -0
- package/dist/runtime/sandboxDesk.js +242 -0
- package/dist/runtime/sandboxFiles.d.ts +20 -0
- package/dist/runtime/sandboxFiles.js +150 -0
- package/dist/runtime/stageAttemptBootstrap.js +15 -1
- package/dist/runtime/stageEnvironment.d.ts +2 -0
- package/dist/runtime/stageEnvironment.js +8 -1
- package/dist/runtime/stageProcessLauncher.js +23 -0
- package/dist/runtime/stageRoots.d.ts +9 -0
- package/dist/runtime/stageRoots.js +14 -0
- package/dist/runtime/stageRunner.d.ts +5 -0
- package/dist/runtime/stageRunner.js +38 -4
- package/dist/runtime/stageSandbox.d.ts +16 -0
- package/dist/runtime/stageSandbox.js +54 -0
- package/dist/runtime/stageSecrets.d.ts +6 -1
- package/dist/runtime/stageSecrets.js +34 -1
- package/dist/runtime/stageWorker.js +138 -105
- package/dist/runtime/workspaceBinding.d.ts +2 -1
- package/dist/runtime/workspaceBinding.js +20 -4
- package/dist/server/agentAuthorRoutes.d.ts +5 -0
- package/dist/server/agentAuthorRoutes.js +64 -0
- package/dist/server/agentRoutes.d.ts +5 -0
- package/dist/server/agentRoutes.js +323 -0
- package/dist/server/bootstrap.d.ts +8 -0
- package/dist/server/bootstrap.js +36 -10
- package/dist/server/controlToken.js +8 -0
- package/dist/server/http.d.ts +8 -1
- package/dist/server/http.js +172 -20
- package/dist/server/mcpHost.js +1 -0
- package/dist/server/memoryRoutes.d.ts +5 -0
- package/dist/server/memoryRoutes.js +146 -0
- package/dist/server/projectMcpRoutes.d.ts +1 -0
- package/dist/server/projectMcpRoutes.js +241 -3
- package/dist/toolkit/guard.d.ts +27 -0
- package/dist/toolkit/guard.js +98 -0
- package/dist/toolkit/index.d.ts +48 -0
- package/dist/toolkit/index.js +82 -0
- package/dist/toolkit/meta.d.ts +36 -0
- package/dist/toolkit/meta.js +117 -0
- package/dist/toolkit/profiles.d.ts +13 -0
- package/dist/toolkit/profiles.js +25 -0
- package/dist/toolkit/record.d.ts +15 -0
- package/dist/toolkit/record.js +17 -0
- package/dist/tools/memoryTools.d.ts +95 -0
- package/dist/tools/memoryTools.js +179 -0
- package/dist/types/pipeline.d.ts +3 -0
- package/dist/types/stage.d.ts +2 -0
- package/dist/types/task.d.ts +11 -1
- package/dist/ui/assets/AssistantRuntimeProvider-CsiPFhCT.js +9 -0
- package/dist/ui/assets/MarkdownText-D7tYaIFO.js +29 -0
- package/dist/ui/assets/ReadonlyStore-BfXqHykm.js +6 -0
- package/dist/ui/assets/Tooltip-DrpoRRNi.js +1 -0
- package/dist/ui/assets/chat-BbLukq9W.js +5 -0
- package/dist/ui/assets/chat-zIZU_bP1.css +2 -0
- package/dist/ui/assets/jsx-runtime-iVoFAvQ_.js +1 -0
- package/dist/ui/assets/{index-Clv4sJpU.css → main-BFo79krC.css} +1 -1
- package/dist/ui/assets/main-CijYMeM0.js +259 -0
- package/dist/ui/chat.html +25 -0
- package/dist/ui/index.html +6 -2
- package/dist/workshop/modelSettings.d.ts +2 -2
- package/dist/workshop/modelSettings.js +5 -11
- package/examples/README.md +1 -0
- package/examples/channels-smoke/README.md +12 -1
- package/examples/github-release/README.md +1 -1
- package/examples/memory/README.md +101 -0
- package/examples/memory/desk-outage.task.yaml +15 -0
- package/examples/memory/desk-refund.task.yaml +15 -0
- package/examples/memory/desk-revisit-outage.task.yaml +11 -0
- package/examples/memory/desk-revisit.pipeline.yaml +123 -0
- package/examples/memory/desk.pipeline.yaml +192 -0
- package/examples/memory/file-card.pipeline.yaml +39 -0
- package/examples/memory/file-ledger.task.yaml +22 -0
- package/examples/memory/file-release.task.yaml +24 -0
- package/examples/memory/file-rich.pipeline.yaml +40 -0
- package/examples/memory/file-ticket.task.yaml +7 -0
- package/examples/memory/file-timeline.task.yaml +28 -0
- package/examples/memory/find-card.pipeline.yaml +46 -0
- package/examples/memory/find-ticket.task.yaml +7 -0
- package/examples/memory/missing.pipeline.yaml +16 -0
- package/examples/memory/probe-bash.task.yaml +3 -0
- package/examples/memory/recall.pipeline.yaml +24 -0
- package/examples/memory/recall.task.yaml +5 -0
- package/examples/memory/remember-ada.task.yaml +6 -0
- package/examples/memory/remember-grace.task.yaml +6 -0
- package/examples/memory/remember.pipeline.yaml +58 -0
- package/examples/memory/sandbox-memory-bash.pipeline.yaml +28 -0
- package/examples/memory/stageflow.yaml +12 -0
- package/examples/memory/update-card.pipeline.yaml +43 -0
- package/examples/memory/update-ledger.task.yaml +23 -0
- package/examples/memory/update-rich.pipeline.yaml +44 -0
- package/examples/memory/update-ticket.task.yaml +7 -0
- package/examples/ship-feature/README.md +15 -0
- package/examples/stage-mcp/README.md +1 -1
- package/package.json +2 -1
- package/dist/ui/assets/index-BOhqnPNg.js +0 -260
|
@@ -15,6 +15,7 @@ export declare function mergePipelineStages(rootPath: string): Promise<LoadOutco
|
|
|
15
15
|
schemas?: PayloadSchemaMap;
|
|
16
16
|
requires?: ToolRequirement[];
|
|
17
17
|
channels?: StageChannel[];
|
|
18
|
+
memories?: string[];
|
|
18
19
|
warnings: LoadIssue[];
|
|
19
20
|
}>>;
|
|
20
21
|
/** Synthetic `declaringPath` for stage entries with no backing file. */
|
|
@@ -27,5 +28,6 @@ export declare function mergeInlinePipelineStages(raw: InlinePipelineDefinition)
|
|
|
27
28
|
schemas?: PayloadSchemaMap;
|
|
28
29
|
requires?: ToolRequirement[];
|
|
29
30
|
channels?: StageChannel[];
|
|
31
|
+
memories?: string[];
|
|
30
32
|
warnings: LoadIssue[];
|
|
31
33
|
}>>;
|
|
@@ -7,6 +7,7 @@ import { readYamlObject } from "./readYamlObject.js";
|
|
|
7
7
|
import { yamlParsePosition } from "./yamlParsePosition.js";
|
|
8
8
|
import { parseToolRequires } from "./toolRequires.js";
|
|
9
9
|
import { parseChannels } from "../channels/stageChannels.js";
|
|
10
|
+
import { parseMemoryIdList } from "../memory/ids.js";
|
|
10
11
|
import { classifyYamlDocument, collectDocumentKeys, mixedDialectIssue, } from "./yamlDialect.js";
|
|
11
12
|
function isPlainObject(value) {
|
|
12
13
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -194,6 +195,10 @@ export async function mergePipelineStages(rootPath) {
|
|
|
194
195
|
if (!channelsOutcome.ok)
|
|
195
196
|
return channelsOutcome;
|
|
196
197
|
const channels = channelsOutcome.value;
|
|
198
|
+
const memoriesOutcome = parsePipelineMemories(raw.memories, `pipeline ${absRoot}`, pipelineId);
|
|
199
|
+
if (!memoriesOutcome.ok)
|
|
200
|
+
return memoriesOutcome;
|
|
201
|
+
const memories = memoriesOutcome.value;
|
|
197
202
|
const idLocations = new Map();
|
|
198
203
|
const entries = [];
|
|
199
204
|
const warnings = [];
|
|
@@ -217,6 +222,7 @@ export async function mergePipelineStages(rootPath) {
|
|
|
217
222
|
...(schemas !== undefined ? { schemas } : {}),
|
|
218
223
|
...(requires !== undefined ? { requires } : {}),
|
|
219
224
|
...(channels !== undefined ? { channels } : {}),
|
|
225
|
+
...(memories !== undefined ? { memories } : {}),
|
|
220
226
|
warnings,
|
|
221
227
|
});
|
|
222
228
|
}
|
|
@@ -283,6 +289,10 @@ export async function mergeInlinePipelineStages(raw) {
|
|
|
283
289
|
if (!channelsOutcome.ok)
|
|
284
290
|
return channelsOutcome;
|
|
285
291
|
const channels = channelsOutcome.value;
|
|
292
|
+
const memoriesOutcome = parsePipelineMemories(raw.memories, `inline pipeline ${pipelineId}`, pipelineId);
|
|
293
|
+
if (!memoriesOutcome.ok)
|
|
294
|
+
return memoriesOutcome;
|
|
295
|
+
const memories = memoriesOutcome.value;
|
|
286
296
|
const entries = [];
|
|
287
297
|
for (let index = 0; index < raw.stages.length; index++) {
|
|
288
298
|
const entry = raw.stages[index];
|
|
@@ -306,9 +316,24 @@ export async function mergeInlinePipelineStages(raw) {
|
|
|
306
316
|
...(schemas !== undefined ? { schemas } : {}),
|
|
307
317
|
...(requires !== undefined ? { requires } : {}),
|
|
308
318
|
...(channels !== undefined ? { channels } : {}),
|
|
319
|
+
...(memories !== undefined ? { memories } : {}),
|
|
309
320
|
warnings: [],
|
|
310
321
|
});
|
|
311
322
|
}
|
|
323
|
+
function parsePipelineMemories(raw, label, pipelineId) {
|
|
324
|
+
const parsed = parseMemoryIdList(raw);
|
|
325
|
+
if (!parsed.ok) {
|
|
326
|
+
return loadFailure([
|
|
327
|
+
{
|
|
328
|
+
code: "pipeline.invalid_memories",
|
|
329
|
+
message: `Invalid ${label}: ${parsed.message}`,
|
|
330
|
+
category: "pipeline",
|
|
331
|
+
pipelineId,
|
|
332
|
+
},
|
|
333
|
+
]);
|
|
334
|
+
}
|
|
335
|
+
return loadSuccess(parsed.value);
|
|
336
|
+
}
|
|
312
337
|
function parsePipelineChannels(raw, label, pipelineId) {
|
|
313
338
|
if (raw === undefined)
|
|
314
339
|
return loadSuccess(undefined);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type StageMcpErrorCode = "missing_catalog" | "unknown_server" | "reserved_name" | "unresolved_var" | "invalid_config" | "untrusted_config_origin" | "connect_failed";
|
|
1
|
+
export type StageMcpErrorCode = "missing_catalog" | "unknown_server" | "reserved_name" | "unresolved_var" | "invalid_config" | "untrusted_config_origin" | "connect_failed" | "host_state_corrupt";
|
|
2
2
|
export declare class StageMcpError extends Error {
|
|
3
3
|
readonly code: StageMcpErrorCode;
|
|
4
4
|
constructor(message: string, code: StageMcpErrorCode);
|
|
@@ -9,18 +9,45 @@ export declare const MCP_CATALOG_FILENAME = ".mcp.json";
|
|
|
9
9
|
export declare const STAGEFLOW_STAGE_ARTIFACTS_DIR_ENV = "STAGEFLOW_STAGE_ARTIFACTS_DIR";
|
|
10
10
|
export declare function stampStagePromptArtifactsDir(prompt: string, artifactsDir: string): string;
|
|
11
11
|
export type McpCatalogServers = Record<string, Record<string, unknown>>;
|
|
12
|
+
export type McpServerOrigin = "project" | "console";
|
|
12
13
|
export type InspectedMcpCatalog = {
|
|
13
14
|
path: string;
|
|
14
15
|
servers: McpCatalogServers;
|
|
16
|
+
/** Where each server came from; `console` = saved from the UI under $STAGEFLOW_HOME. */
|
|
17
|
+
origins: Record<string, McpServerOrigin>;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Who is asking. Without a scope only the project `.mcp.json` is visible; with one, servers saved
|
|
21
|
+
* from the console are merged in (console entry wins on a name clash) and secrets are filled by
|
|
22
|
+
* the rules of that scope:
|
|
23
|
+
* - stage: console secrets whose names the stage declared in `secrets:`, plus the stage env
|
|
24
|
+
* - agent: console secrets plus Host env names listed in STAGEFLOW_MCP_ENV_ALLOW
|
|
25
|
+
* - probe: same as agent, so a probe and an agent run resolve one config identically
|
|
26
|
+
* - validate: placeholders are checked but no secret value is ever read into the output
|
|
27
|
+
* Console secrets always win over Host env for the same name.
|
|
28
|
+
*/
|
|
29
|
+
export type McpResolveScope = {
|
|
30
|
+
kind: "stage";
|
|
31
|
+
secrets: readonly string[];
|
|
32
|
+
} | {
|
|
33
|
+
kind: "agent";
|
|
34
|
+
} | {
|
|
35
|
+
kind: "probe";
|
|
36
|
+
} | {
|
|
37
|
+
kind: "validate";
|
|
38
|
+
};
|
|
39
|
+
export type McpCatalogLoadOptions = {
|
|
40
|
+
scope?: McpResolveScope;
|
|
15
41
|
};
|
|
16
42
|
export declare function mcpCatalogPath(projectRoot: string): string;
|
|
17
43
|
export declare function parseMcpCatalog(raw: string, catalogPath?: string): McpCatalogServers;
|
|
18
|
-
export declare function mcpCatalogExists(projectRoot: string): Promise<boolean>;
|
|
19
|
-
export declare function loadMcpCatalog(projectRoot: string): Promise<InspectedMcpCatalog>;
|
|
44
|
+
export declare function mcpCatalogExists(projectRoot: string, options?: McpCatalogLoadOptions): Promise<boolean>;
|
|
45
|
+
export declare function loadMcpCatalog(projectRoot: string, options?: McpCatalogLoadOptions): Promise<InspectedMcpCatalog>;
|
|
20
46
|
export type ProjectMcpCatalogTransport = "stdio" | "http";
|
|
21
47
|
export type ProjectMcpCatalogEntry = {
|
|
22
48
|
name: string;
|
|
23
49
|
transport: ProjectMcpCatalogTransport;
|
|
50
|
+
origin: McpServerOrigin;
|
|
24
51
|
};
|
|
25
52
|
export type ProjectMcpCatalogListStatus = "ok" | "missing_catalog" | "invalid_config";
|
|
26
53
|
export type ProjectMcpCatalogList = {
|
|
@@ -32,7 +59,10 @@ export declare function assertMcpAllowlistKnown(servers: McpCatalogServers, allo
|
|
|
32
59
|
export declare function resolveStageMcpServers(options: {
|
|
33
60
|
projectRoot: string;
|
|
34
61
|
allowlist?: readonly string[];
|
|
62
|
+
/** Stage env for the project-only and stage scopes; agent/probe/validate do not read it as a secret source. */
|
|
35
63
|
env: NodeJS.ProcessEnv;
|
|
64
|
+
/** Host env that STAGEFLOW_MCP_ENV_ALLOW may draw from (agent/probe scopes). Defaults to process.env. */
|
|
65
|
+
hostEnv?: NodeJS.ProcessEnv;
|
|
36
66
|
stageId?: string;
|
|
37
67
|
bindingKind?: "repository" | "checkout" | "none";
|
|
38
68
|
trustWorkspaceConfig?: string[];
|
|
@@ -40,4 +70,6 @@ export declare function resolveStageMcpServers(options: {
|
|
|
40
70
|
trustProjectRoot?: string;
|
|
41
71
|
/** When true, .mcp.json is treated as workspace-sourced. */
|
|
42
72
|
workspaceSourced?: boolean;
|
|
73
|
+
/** Merge console-saved servers and fill `${VAR}` by the scope's rules. Omit for project-only. */
|
|
74
|
+
scope?: McpResolveScope;
|
|
43
75
|
}): Promise<ResolvedMcpServers>;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { STAGEFLOW_MCP_SERVER_NAME } from "../agent/claudeTools.js";
|
|
4
|
+
import { isMcpHostStateError, mcpEnvAllowNames, readHostMcpServers, readMcpSecrets, } from "./mcpHostCatalog.js";
|
|
5
|
+
import { assertSafeMcpUrl, McpUrlPolicyError } from "./mcpRedaction.js";
|
|
4
6
|
export class StageMcpError extends Error {
|
|
5
7
|
code;
|
|
6
8
|
constructor(message, code) {
|
|
@@ -51,7 +53,31 @@ export function parseMcpCatalog(raw, catalogPath = MCP_CATALOG_FILENAME) {
|
|
|
51
53
|
}
|
|
52
54
|
return servers;
|
|
53
55
|
}
|
|
54
|
-
|
|
56
|
+
/** Console servers; a corrupt host file surfaces as a StageMcpError instead of reading as empty. */
|
|
57
|
+
function hostServersOrThrow() {
|
|
58
|
+
try {
|
|
59
|
+
return readHostMcpServers();
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
if (isMcpHostStateError(err))
|
|
63
|
+
throw new StageMcpError(err.message, "host_state_corrupt");
|
|
64
|
+
throw err;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function hostSecretsOrThrow() {
|
|
68
|
+
try {
|
|
69
|
+
return readMcpSecrets();
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
if (isMcpHostStateError(err))
|
|
73
|
+
throw new StageMcpError(err.message, "host_state_corrupt");
|
|
74
|
+
throw err;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export async function mcpCatalogExists(projectRoot, options = {}) {
|
|
78
|
+
if (options.scope !== undefined && Object.keys(hostServersOrThrow()).length > 0) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
55
81
|
try {
|
|
56
82
|
await readFile(mcpCatalogPath(projectRoot), "utf8");
|
|
57
83
|
return true;
|
|
@@ -63,20 +89,31 @@ export async function mcpCatalogExists(projectRoot) {
|
|
|
63
89
|
throw err;
|
|
64
90
|
}
|
|
65
91
|
}
|
|
66
|
-
export async function loadMcpCatalog(projectRoot) {
|
|
92
|
+
export async function loadMcpCatalog(projectRoot, options = {}) {
|
|
67
93
|
const catalogPath = mcpCatalogPath(projectRoot);
|
|
94
|
+
const hostServers = options.scope !== undefined ? hostServersOrThrow() : {};
|
|
68
95
|
let raw;
|
|
69
96
|
try {
|
|
70
97
|
raw = await readFile(catalogPath, "utf8");
|
|
71
98
|
}
|
|
72
99
|
catch (err) {
|
|
73
100
|
const code = err.code;
|
|
74
|
-
if (code
|
|
101
|
+
if (code !== "ENOENT") {
|
|
102
|
+
throw new StageMcpError(`MCP catalog "${MCP_CATALOG_FILENAME}" could not be read`, "invalid_config");
|
|
103
|
+
}
|
|
104
|
+
if (Object.keys(hostServers).length === 0) {
|
|
75
105
|
throw new StageMcpError(`MCP catalog "${MCP_CATALOG_FILENAME}" is missing`, "missing_catalog");
|
|
76
106
|
}
|
|
77
|
-
throw new StageMcpError(`MCP catalog "${MCP_CATALOG_FILENAME}" could not be read`, "invalid_config");
|
|
78
107
|
}
|
|
79
|
-
|
|
108
|
+
const servers = raw !== undefined ? parseMcpCatalog(raw, catalogPath) : {};
|
|
109
|
+
const origins = {};
|
|
110
|
+
for (const name of Object.keys(servers))
|
|
111
|
+
origins[name] = "project";
|
|
112
|
+
for (const [name, entry] of Object.entries(hostServers)) {
|
|
113
|
+
servers[name] = entry;
|
|
114
|
+
origins[name] = "console";
|
|
115
|
+
}
|
|
116
|
+
return { path: catalogPath, servers, origins };
|
|
80
117
|
}
|
|
81
118
|
function coarseMcpTransport(entry) {
|
|
82
119
|
if (Object.hasOwn(entry, "command")) {
|
|
@@ -89,14 +126,20 @@ function coarseMcpTransport(entry) {
|
|
|
89
126
|
}
|
|
90
127
|
export async function listProjectMcpCatalog(projectRoot) {
|
|
91
128
|
try {
|
|
92
|
-
const catalog = await loadMcpCatalog(projectRoot
|
|
129
|
+
const catalog = await loadMcpCatalog(projectRoot, {
|
|
130
|
+
scope: { kind: "validate" },
|
|
131
|
+
});
|
|
93
132
|
const servers = [];
|
|
94
133
|
for (const [name, entry] of Object.entries(catalog.servers)) {
|
|
95
134
|
const transport = coarseMcpTransport(entry);
|
|
96
135
|
if (transport === undefined) {
|
|
97
136
|
return { status: "invalid_config", servers: [] };
|
|
98
137
|
}
|
|
99
|
-
servers.push({
|
|
138
|
+
servers.push({
|
|
139
|
+
name,
|
|
140
|
+
transport,
|
|
141
|
+
origin: catalog.origins[name] ?? "project",
|
|
142
|
+
});
|
|
100
143
|
}
|
|
101
144
|
return { status: "ok", servers };
|
|
102
145
|
}
|
|
@@ -207,6 +250,69 @@ function interpolateServer(entry, env, serverName, projectRoot, stageId) {
|
|
|
207
250
|
}
|
|
208
251
|
return stampSpawnRoot(resolved, projectRoot, serverName);
|
|
209
252
|
}
|
|
253
|
+
function bagOfStrings(value) {
|
|
254
|
+
return isPlainObject(value) && Object.values(value).every((v) => typeof v === "string");
|
|
255
|
+
}
|
|
256
|
+
/** Shape check for any entry about to be used (project or console). */
|
|
257
|
+
function assertEntryShape(name, entry) {
|
|
258
|
+
const bad = (what) => {
|
|
259
|
+
throw new StageMcpError(`MCP catalog server "${name}" ${what}`, "invalid_config");
|
|
260
|
+
};
|
|
261
|
+
const hasCommand = Object.hasOwn(entry, "command");
|
|
262
|
+
const hasUrl = Object.hasOwn(entry, "url");
|
|
263
|
+
if (hasCommand === hasUrl)
|
|
264
|
+
bad('needs exactly one of "command" or "url"');
|
|
265
|
+
if (hasCommand && (typeof entry.command !== "string" || entry.command.length === 0)) {
|
|
266
|
+
bad('"command" must be a non-empty string');
|
|
267
|
+
}
|
|
268
|
+
if (hasUrl && (typeof entry.url !== "string" || entry.url.length === 0)) {
|
|
269
|
+
bad('"url" must be a non-empty string');
|
|
270
|
+
}
|
|
271
|
+
if (entry.args !== undefined &&
|
|
272
|
+
(!Array.isArray(entry.args) || entry.args.some((a) => typeof a !== "string"))) {
|
|
273
|
+
bad('"args" must be an array of strings');
|
|
274
|
+
}
|
|
275
|
+
for (const key of ["env", "headers"]) {
|
|
276
|
+
if (entry[key] !== undefined && !bagOfStrings(entry[key]))
|
|
277
|
+
bad(`"${key}" must map names to strings`);
|
|
278
|
+
}
|
|
279
|
+
if (entry.cwd !== undefined && typeof entry.cwd !== "string")
|
|
280
|
+
bad('"cwd" must be a string');
|
|
281
|
+
}
|
|
282
|
+
function assertResolvedUrlSafe(name, resolved) {
|
|
283
|
+
if (typeof resolved.url !== "string")
|
|
284
|
+
return;
|
|
285
|
+
try {
|
|
286
|
+
assertSafeMcpUrl(resolved.url);
|
|
287
|
+
}
|
|
288
|
+
catch (err) {
|
|
289
|
+
if (err instanceof McpUrlPolicyError) {
|
|
290
|
+
throw new StageMcpError(`MCP catalog server "${name}": ${err.message}`, "invalid_config");
|
|
291
|
+
}
|
|
292
|
+
throw err;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const VALIDATE_PLACEHOLDER = "<redacted>";
|
|
296
|
+
/** The values `${NAME}` may be filled from, by scope. Console secrets win over Host env. */
|
|
297
|
+
function envForScope(scope, stageEnv, hostEnv) {
|
|
298
|
+
const secrets = hostSecretsOrThrow();
|
|
299
|
+
if (scope.kind === "stage") {
|
|
300
|
+
const declared = new Set(scope.secrets);
|
|
301
|
+
const own = Object.fromEntries(Object.entries(secrets).filter(([n]) => declared.has(n)));
|
|
302
|
+
return { ...stageEnv, ...own };
|
|
303
|
+
}
|
|
304
|
+
const allowed = {};
|
|
305
|
+
for (const n of mcpEnvAllowNames(hostEnv)) {
|
|
306
|
+
if (hostEnv[n] !== undefined)
|
|
307
|
+
allowed[n] = hostEnv[n];
|
|
308
|
+
}
|
|
309
|
+
if (scope.kind === "validate") {
|
|
310
|
+
const names = new Set([...Object.keys(secrets), ...Object.keys(allowed), ...Object.keys(stageEnv)]);
|
|
311
|
+
return Object.fromEntries([...names].map((n) => [n, VALIDATE_PLACEHOLDER]));
|
|
312
|
+
}
|
|
313
|
+
// agent and probe: one rule, so the same config resolves the same way in both.
|
|
314
|
+
return { ...allowed, ...secrets };
|
|
315
|
+
}
|
|
210
316
|
export async function resolveStageMcpServers(options) {
|
|
211
317
|
const allowlist = options.allowlist ?? [];
|
|
212
318
|
if (allowlist.length === 0) {
|
|
@@ -224,11 +330,20 @@ export async function resolveStageMcpServers(options) {
|
|
|
224
330
|
throw new StageMcpError(decision.message ?? "untrusted_config_origin", "untrusted_config_origin");
|
|
225
331
|
}
|
|
226
332
|
}
|
|
227
|
-
|
|
333
|
+
// Workspace-sourced catalogs never see console servers or secrets.
|
|
334
|
+
const scope = options.workspaceSourced === true ? undefined : options.scope;
|
|
335
|
+
const catalog = await loadMcpCatalog(options.projectRoot, scope ? { scope } : {});
|
|
228
336
|
assertMcpAllowlistKnown(catalog.servers, allowlist);
|
|
337
|
+
const env = scope
|
|
338
|
+
? envForScope(scope, options.env, options.hostEnv ?? process.env)
|
|
339
|
+
: options.env;
|
|
229
340
|
const resolved = {};
|
|
230
341
|
for (const name of allowlist) {
|
|
231
|
-
|
|
342
|
+
const entry = catalog.servers[name];
|
|
343
|
+
assertEntryShape(name, entry);
|
|
344
|
+
resolved[name] = interpolateServer(entry, env, name, options.projectRoot, options.stageId);
|
|
345
|
+
if (scope?.kind !== "validate")
|
|
346
|
+
assertResolvedUrlSafe(name, resolved[name]);
|
|
232
347
|
}
|
|
233
348
|
return resolved;
|
|
234
349
|
}
|
|
@@ -4,7 +4,7 @@ import type { LoadIssue } from "./loadOutcome.js";
|
|
|
4
4
|
import type { AnyChannelProvider } from "../channels/types.js";
|
|
5
5
|
export type ValidationSeverity = "error" | "warning";
|
|
6
6
|
export type ValidationCategory = "pipeline" | "stage" | "catalog" | "task" | "trigger";
|
|
7
|
-
export type ValidationFindingCode = "pipeline.invalid_shape" | "pipeline.dag_error" | "pipeline.missing_stage" | "pipeline.load_error" | "pipeline.string_stage_ref" | "pipeline.stage_uses_inline_conflict" | "pipeline.stage_missing_body" | "pipeline.stage_id_mismatch" | "pipeline.stage_wrapper_skill_rejected" | "pipeline.invalid_completion" | "catalog.path_case_mismatch" | "pipeline.invalid_recovery" | "pipeline.include_cycle" | "pipeline.include_invalid" | "pipeline.include_duplicate_stage" | "pipeline.invalid_agent" | "pipeline.invalid_model" | "pipeline.invalid_verify" | "pipeline.io_incompatible" | "pipeline.model_applies" | "pipeline.route_if_invalid" | "pipeline.route_all_gated" | "pipeline.invalid_requires" | "pipeline.requires_conflict" | "pipeline.invalid_channels" | "stage.invalid_shape" | "stage.invalid_model" | "stage.missing_model" | "stage.invalid_payload_schema" | "stage.invalid_clone_input_schema" | "stage.unresolved_schema_ref" | "stage.invalid_clone_actions" | "stage.invalid_gate_kinds" | "stage.invalid_pre_emit_checks" | "stage.invalid_timeout_ms" | "stage.invalid_skill" | "stage.invalid_mcp" | "stage.invalid_channels" | "stage.channel_unavailable" | "stage.invalid_browser" | "stage.invalid_secrets" | "stage.unknown_secret" | "stage.denied_secret" | "stage.invalid_requires" | "stage.invalid_agent" | "stage.invalid_io" | "stage.load_error" | "stage.id_filename_mismatch" | "task.invalid_shape" | "task.load_error" | "task.entry_input_unmet" | "task.binding_conflict" | "task.repository_ref_required" | "task.ref_without_repository" | "task.repository_invalid" | "trigger.invalid_shape" | "trigger.load_error" | "trigger.unknown_pipeline" | "trigger.unknown_task" | "catalog.duplicate_pipeline_id" | "catalog.manifest_missing" | "catalog.manifest_invalid" | "catalog.empty_catalog" | "catalog.manifest_load_error" | "catalog.invalid_mcp" | "catalog.mcp_command_missing" | "catalog.mixed_yaml_dialect" | "catalog.legacy_yaml" | "catalog.stageflow_home_absolute_path";
|
|
7
|
+
export type ValidationFindingCode = "pipeline.invalid_shape" | "pipeline.dag_error" | "pipeline.missing_stage" | "pipeline.load_error" | "pipeline.string_stage_ref" | "pipeline.stage_uses_inline_conflict" | "pipeline.stage_missing_body" | "pipeline.stage_id_mismatch" | "pipeline.stage_wrapper_skill_rejected" | "pipeline.invalid_completion" | "catalog.path_case_mismatch" | "pipeline.invalid_recovery" | "pipeline.include_cycle" | "pipeline.include_invalid" | "pipeline.include_duplicate_stage" | "pipeline.invalid_agent" | "pipeline.invalid_model" | "pipeline.invalid_verify" | "pipeline.io_incompatible" | "pipeline.model_applies" | "pipeline.route_if_invalid" | "pipeline.route_all_gated" | "pipeline.invalid_requires" | "pipeline.requires_conflict" | "pipeline.invalid_channels" | "pipeline.invalid_memories" | "pipeline.memory_unavailable" | "stage.invalid_shape" | "stage.invalid_model" | "stage.missing_model" | "stage.invalid_payload_schema" | "stage.invalid_clone_input_schema" | "stage.unresolved_schema_ref" | "stage.invalid_clone_actions" | "stage.invalid_gate_kinds" | "stage.invalid_pre_emit_checks" | "stage.invalid_timeout_ms" | "stage.invalid_skill" | "stage.invalid_mcp" | "stage.invalid_channels" | "stage.channel_unavailable" | "stage.invalid_browser" | "stage.invalid_secrets" | "stage.unknown_secret" | "stage.denied_secret" | "stage.invalid_requires" | "stage.invalid_agent" | "stage.invalid_io" | "stage.load_error" | "stage.id_filename_mismatch" | "task.invalid_shape" | "task.load_error" | "task.entry_input_unmet" | "task.binding_conflict" | "task.repository_ref_required" | "task.ref_without_repository" | "task.repository_invalid" | "trigger.invalid_shape" | "trigger.load_error" | "trigger.unknown_pipeline" | "trigger.unknown_task" | "catalog.duplicate_pipeline_id" | "catalog.manifest_missing" | "catalog.manifest_invalid" | "catalog.empty_catalog" | "catalog.manifest_load_error" | "catalog.invalid_mcp" | "catalog.mcp_command_missing" | "catalog.mixed_yaml_dialect" | "catalog.legacy_yaml" | "catalog.stageflow_home_absolute_path";
|
|
8
8
|
export type ValidationFinding = {
|
|
9
9
|
severity: ValidationSeverity;
|
|
10
10
|
code: ValidationFindingCode | string;
|
|
@@ -17,6 +17,7 @@ import { findMissingMcpCommands } from "../preflight/mcpCommands.js";
|
|
|
17
17
|
import { ChannelError } from "../channels/errors.js";
|
|
18
18
|
import { ChannelRegistry } from "../channels/registry.js";
|
|
19
19
|
import { validateStageChannels } from "../channels/stageChannels.js";
|
|
20
|
+
import { inspectMemorySync, memoryHome } from "../memory/host.js";
|
|
20
21
|
export function relPath(cwd, absPath) {
|
|
21
22
|
return path.relative(cwd, absPath).replace(/\\/g, "/");
|
|
22
23
|
}
|
|
@@ -276,6 +277,25 @@ function findingsForStageChannels(cwd, pipelinePath, loaded, projectRoot, provid
|
|
|
276
277
|
}
|
|
277
278
|
return users.flatMap((stage) => validateStageChannels(registry, stage).map((issue) => findingStageError(cwd, pipelinePath, issue.message, "stage.channel_unavailable", issue.stageId)));
|
|
278
279
|
}
|
|
280
|
+
const MEMORY_FINDING = {
|
|
281
|
+
MEMORY_NOT_FOUND: (id) => `Memory "${id}" does not exist. Create it before attaching it.`,
|
|
282
|
+
MEMORY_MISMATCH: (id) => `Memory "${id}" does not match the id in its file.`,
|
|
283
|
+
MEMORY_UNREADABLE: (id) => `Memory "${id}" could not be read.`,
|
|
284
|
+
MEMORY_INVALID_ID: (id) => `Memory "${id}" is not a valid id.`,
|
|
285
|
+
};
|
|
286
|
+
function findingsForMemories(cwd, pipelinePath, loaded) {
|
|
287
|
+
const ids = loaded.pipeline.memories;
|
|
288
|
+
if (!ids?.length)
|
|
289
|
+
return [];
|
|
290
|
+
const home = memoryHome();
|
|
291
|
+
return ids.flatMap((id) => {
|
|
292
|
+
const inspected = inspectMemorySync(home, id);
|
|
293
|
+
if (inspected.ok)
|
|
294
|
+
return [];
|
|
295
|
+
const message = (MEMORY_FINDING[inspected.code] ?? MEMORY_FINDING.MEMORY_UNREADABLE)(id);
|
|
296
|
+
return [findingPipelineError(cwd, pipelinePath, message, "pipeline.memory_unavailable", loaded.pipeline.id)];
|
|
297
|
+
});
|
|
298
|
+
}
|
|
279
299
|
function findingsForStageSecrets(cwd, pipelinePath, loaded) {
|
|
280
300
|
const registry = loadSecretRegistry(process.env);
|
|
281
301
|
const findings = [];
|
|
@@ -386,7 +406,7 @@ async function findingsForStageMcpCatalog(cwd, loaded) {
|
|
|
386
406
|
}
|
|
387
407
|
let catalog;
|
|
388
408
|
try {
|
|
389
|
-
catalog = await loadMcpCatalog(projectRoot);
|
|
409
|
+
catalog = await loadMcpCatalog(projectRoot, { scope: { kind: "validate" } });
|
|
390
410
|
}
|
|
391
411
|
catch (err) {
|
|
392
412
|
if (!(err instanceof StageMcpError))
|
|
@@ -459,6 +479,7 @@ async function runPipelineValidation(nameOrPath, options) {
|
|
|
459
479
|
findings.push(...(await findingsForStageMcpCatalog(cwd, outcome.value)));
|
|
460
480
|
findings.push(...findingsForStageSecrets(cwd, pipelinePath, outcome.value));
|
|
461
481
|
findings.push(...findingsForStageChannels(cwd, pipelinePath, outcome.value, projectRoot, options.channelProviders));
|
|
482
|
+
findings.push(...findingsForMemories(cwd, pipelinePath, outcome.value));
|
|
462
483
|
findings.push(...findingsForStageflowHomeAbsolutePaths(cwd, outcome.value));
|
|
463
484
|
if (validateStages && outcome.value.stageSources) {
|
|
464
485
|
for (const source of Object.values(outcome.value.stageSources)) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/server";
|
|
2
|
+
import type { McpToolDeps } from "./deps.js";
|
|
3
|
+
/**
|
|
4
|
+
* Agent management for operators driving Stageflow over MCP. A running agent reaches these
|
|
5
|
+
* only through the Stageflow tool groups it is granted (`agents.*`); the guard in
|
|
6
|
+
* `toolkit/guard.ts` stops it acting on its own sessions.
|
|
7
|
+
*/
|
|
8
|
+
export declare function registerAgentTools(server: McpServer, deps: McpToolDeps): void;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defaultAgentProjectRoot } from "../agents/projectRoot.js";
|
|
3
|
+
import { AGENT_ID_PATTERN } from "../config/loadAgent.js";
|
|
4
|
+
import { AgentError, isAgentError } from "../agents/errors.js";
|
|
5
|
+
import { SESSION_STATUSES } from "../agents/store.js";
|
|
6
|
+
import { textResult } from "./toolResults.js";
|
|
7
|
+
const agentId = z.string().regex(AGENT_ID_PATTERN);
|
|
8
|
+
const MCP_ACTOR = { source: "api", author: "mcp" };
|
|
9
|
+
function required(value, what) {
|
|
10
|
+
if (value === undefined)
|
|
11
|
+
throw new Error(`MCP agent tools require ${what} from host bootstrap`);
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
/** Run a service call and turn domain failures into tool errors with their code and field issues. */
|
|
15
|
+
async function call(work) {
|
|
16
|
+
try {
|
|
17
|
+
return textResult(await work());
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
if (isAgentError(error)) {
|
|
21
|
+
return textResult({ code: error.code, message: error.message, ...(error.issues.length ? { issues: error.issues } : {}) }, true);
|
|
22
|
+
}
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Agent management for operators driving Stageflow over MCP. A running agent reaches these
|
|
28
|
+
* only through the Stageflow tool groups it is granted (`agents.*`); the guard in
|
|
29
|
+
* `toolkit/guard.ts` stops it acting on its own sessions.
|
|
30
|
+
*/
|
|
31
|
+
export function registerAgentTools(server, deps) {
|
|
32
|
+
const service = () => required(deps.agentService, "agentService");
|
|
33
|
+
const chat = () => required(deps.agentChat, "agentChat");
|
|
34
|
+
const root = () => defaultAgentProjectRoot(deps.cwd);
|
|
35
|
+
/** A session must belong to the agent named in the call, or it is not found (as in the HTTP routes). */
|
|
36
|
+
const sessionFor = async (id, sessionId) => {
|
|
37
|
+
const detail = await service().getSession(root(), sessionId);
|
|
38
|
+
if (detail.session.agentId !== id)
|
|
39
|
+
throw new AgentError("agent.session_not_found");
|
|
40
|
+
return detail;
|
|
41
|
+
};
|
|
42
|
+
server.registerTool("list_agents", { description: "List project agents (agents/*.agent.yaml) with status and session stats.", inputSchema: z.object({}) }, () => call(async () => ({ agents: await service().list(root()) })));
|
|
43
|
+
server.registerTool("get_agent", {
|
|
44
|
+
description: "Get one agent: summary, full definition, canonical YAML, and warnings.",
|
|
45
|
+
inputSchema: z.object({ id: agentId }),
|
|
46
|
+
}, ({ id }) => call(() => service().get(root(), id)));
|
|
47
|
+
server.registerTool("validate_agent", {
|
|
48
|
+
description: "Validate an agent definition without saving. Returns field issues, warnings, and canonical YAML.",
|
|
49
|
+
inputSchema: z.object({ definition: z.record(z.string(), z.unknown()) }),
|
|
50
|
+
}, ({ definition }) => call(() => service().validate(root(), definition)));
|
|
51
|
+
server.registerTool("create_agent", {
|
|
52
|
+
description: "Create an agent. It always starts disabled; enable it with enable_agent.",
|
|
53
|
+
inputSchema: z.object({ definition: z.record(z.string(), z.unknown()) }),
|
|
54
|
+
}, ({ definition }) => call(() => service().create(root(), definition, MCP_ACTOR)));
|
|
55
|
+
server.registerTool("save_agent", {
|
|
56
|
+
description: "Save a full agent definition as a new revision. baseRevision must be the current revision (optimistic lock).",
|
|
57
|
+
inputSchema: z.object({
|
|
58
|
+
id: agentId,
|
|
59
|
+
baseRevision: z.number().int().nonnegative(),
|
|
60
|
+
definition: z.record(z.string(), z.unknown()),
|
|
61
|
+
summary: z.string().max(200).optional(),
|
|
62
|
+
}),
|
|
63
|
+
}, ({ id, baseRevision, definition, summary }) => call(() => service().save(root(), id, { baseRevision, definition, summary: summary ?? null }, MCP_ACTOR)));
|
|
64
|
+
server.registerTool("enable_agent", { description: "Enable an agent for new sessions and turns.", inputSchema: z.object({ id: agentId }) }, ({ id }) => call(() => service().setEnabled(root(), id, true, MCP_ACTOR)));
|
|
65
|
+
server.registerTool("disable_agent", {
|
|
66
|
+
description: "Disable an agent. New sessions and turns are refused; a running turn finishes.",
|
|
67
|
+
inputSchema: z.object({ id: agentId }),
|
|
68
|
+
}, ({ id }) => call(() => service().setEnabled(root(), id, false, MCP_ACTOR)));
|
|
69
|
+
server.registerTool("list_agent_revisions", { description: "List an agent's revisions, newest first.", inputSchema: z.object({ id: agentId }) }, ({ id }) => call(async () => ({ revisions: await service().listRevisions(root(), id) })));
|
|
70
|
+
server.registerTool("restore_agent_revision", {
|
|
71
|
+
description: "Save an earlier revision's definition as the newest revision.",
|
|
72
|
+
inputSchema: z.object({
|
|
73
|
+
id: agentId,
|
|
74
|
+
revision: z.number().int().positive(),
|
|
75
|
+
baseRevision: z.number().int().nonnegative(),
|
|
76
|
+
}),
|
|
77
|
+
}, ({ id, revision, baseRevision }) => call(() => service().restore(root(), id, { revision, baseRevision }, MCP_ACTOR)));
|
|
78
|
+
server.registerTool("agent_chat", {
|
|
79
|
+
description: "Send one message to an agent and wait for its reply. Omit sessionId to start a new session.",
|
|
80
|
+
inputSchema: z.object({ id: agentId, message: z.string().min(1), sessionId: z.string().optional() }),
|
|
81
|
+
}, ({ id, message, sessionId }) => call(() => chat().send({
|
|
82
|
+
projectRoot: root(),
|
|
83
|
+
agentId: id,
|
|
84
|
+
text: message,
|
|
85
|
+
source: "mcp",
|
|
86
|
+
...(sessionId ? { sessionId } : {}),
|
|
87
|
+
})));
|
|
88
|
+
server.registerTool("get_agent_session", {
|
|
89
|
+
description: "Fetch a session of an agent: transcript, turns, approvals.",
|
|
90
|
+
inputSchema: z.object({ id: agentId, sessionId: z.string() }),
|
|
91
|
+
}, ({ id, sessionId }) => call(() => sessionFor(id, sessionId)));
|
|
92
|
+
server.registerTool("list_agent_sessions", {
|
|
93
|
+
description: "List an agent's sessions, newest first, with turn counts, tokens and cost.",
|
|
94
|
+
inputSchema: z.object({
|
|
95
|
+
id: agentId,
|
|
96
|
+
status: z.enum(SESSION_STATUSES).optional(),
|
|
97
|
+
search: z.string().optional(),
|
|
98
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
99
|
+
cursor: z.string().optional(),
|
|
100
|
+
}),
|
|
101
|
+
}, ({ id, status, search, limit, cursor }) => call(() => service().listSessions(root(), id, {
|
|
102
|
+
limit: limit ?? 25,
|
|
103
|
+
...(status ? { status } : {}),
|
|
104
|
+
...(search ? { search } : {}),
|
|
105
|
+
...(cursor ? { cursor } : {}),
|
|
106
|
+
})));
|
|
107
|
+
server.registerTool("stop_agent_session", {
|
|
108
|
+
description: "Stop the turn currently running in a session. The session stays open.",
|
|
109
|
+
inputSchema: z.object({ id: agentId, sessionId: z.string() }),
|
|
110
|
+
}, ({ id, sessionId }) => call(async () => {
|
|
111
|
+
await sessionFor(id, sessionId);
|
|
112
|
+
return { stopped: chat().stop(sessionId) };
|
|
113
|
+
}));
|
|
114
|
+
server.registerTool("archive_agent", {
|
|
115
|
+
description: "Archive an agent: its definition leaves the project and new sessions are refused. Past sessions are kept.",
|
|
116
|
+
inputSchema: z.object({ id: agentId }),
|
|
117
|
+
}, ({ id }) => call(async () => { await service().archive(root(), id); return { archived: true }; }));
|
|
118
|
+
server.registerTool("approve_agent_action", {
|
|
119
|
+
description: "Approve the pending ask-first action on a session of an agent, as an operator.",
|
|
120
|
+
inputSchema: z.object({ id: agentId, sessionId: z.string() }),
|
|
121
|
+
}, ({ id, sessionId }) => call(async () => {
|
|
122
|
+
await sessionFor(id, sessionId);
|
|
123
|
+
return service().decideApproval(root(), sessionId, "approve", { kind: "operator" });
|
|
124
|
+
}));
|
|
125
|
+
server.registerTool("deny_agent_action", {
|
|
126
|
+
description: "Deny the pending ask-first action on a session of an agent. The agent is told it was refused.",
|
|
127
|
+
inputSchema: z.object({ id: agentId, sessionId: z.string() }),
|
|
128
|
+
}, ({ id, sessionId }) => call(async () => {
|
|
129
|
+
await sessionFor(id, sessionId);
|
|
130
|
+
return service().decideApproval(root(), sessionId, "deny", { kind: "operator" });
|
|
131
|
+
}));
|
|
132
|
+
}
|
package/dist/mcp/catalogTools.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { buildModelsResponse } from "../models/response.js";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
import { catalogPathErrorBody, CatalogPathError, resolveCatalogRelativePath, resolveCatalogStartInput, selectCatalogRootForStart, } from "../config/catalogRelativePath.js";
|
|
3
|
-
import {
|
|
4
|
+
import { listPipelinesMultiProject, listTasksMultiProject, } from "../config/multiProjectCatalog.js";
|
|
4
5
|
import { resolveCatalogRoots } from "../config/resolveCatalogRoots.js";
|
|
5
6
|
import { describePipeline } from "../config/describePipeline.js";
|
|
6
7
|
import { loadPipeline } from "../config/loadPipeline.js";
|
|
@@ -41,7 +42,17 @@ const taskFileSchema = z.object({
|
|
|
41
42
|
goal: z.string(),
|
|
42
43
|
context: z.string().optional(),
|
|
43
44
|
constraints: z.string().optional(),
|
|
44
|
-
checkout: z
|
|
45
|
+
checkout: z
|
|
46
|
+
.union([
|
|
47
|
+
z.string(),
|
|
48
|
+
z
|
|
49
|
+
.object({
|
|
50
|
+
branch: z.string().optional(),
|
|
51
|
+
base: z.string().optional(),
|
|
52
|
+
})
|
|
53
|
+
.strict(),
|
|
54
|
+
])
|
|
55
|
+
.optional(),
|
|
45
56
|
repository: z.string().optional(),
|
|
46
57
|
ref: z.string().optional(),
|
|
47
58
|
run_branch_template: z.string().optional(),
|
|
@@ -233,14 +244,14 @@ export function registerCatalogTools(server, deps) {
|
|
|
233
244
|
});
|
|
234
245
|
});
|
|
235
246
|
server.registerTool("list_models", {
|
|
236
|
-
description: "List
|
|
247
|
+
description: "List model ids from the providers connected on this host, plus ids pipelines use (same source as GET /api/models). Returns models, providers (grouped), the default model and unlisted ids. Optional project_root filter narrows the pipeline-used ids.",
|
|
237
248
|
inputSchema: z.object({
|
|
238
249
|
project_root: z.string().optional(),
|
|
239
250
|
}),
|
|
240
251
|
}, async ({ project_root }) => {
|
|
241
|
-
const result = await
|
|
252
|
+
const { response, used: result } = await buildModelsResponse({
|
|
242
253
|
store,
|
|
243
|
-
|
|
254
|
+
cwd,
|
|
244
255
|
projectRootFilter: project_root,
|
|
245
256
|
});
|
|
246
257
|
if (result.root_errors.some((e) => e.code === "unknown_project_root") &&
|
|
@@ -251,11 +262,7 @@ export function registerCatalogTools(server, deps) {
|
|
|
251
262
|
root_errors: result.root_errors,
|
|
252
263
|
}, true);
|
|
253
264
|
}
|
|
254
|
-
return textResult(
|
|
255
|
-
models: result.models,
|
|
256
|
-
entries: result.items,
|
|
257
|
-
root_errors: result.root_errors,
|
|
258
|
-
});
|
|
265
|
+
return textResult(response);
|
|
259
266
|
});
|
|
260
267
|
server.registerTool("list_runs", {
|
|
261
268
|
description: "List known pipeline runs. Optional filters: status, since (ISO created_at lower bound), pipeline (id or path), caller_id.",
|
package/dist/mcp/deps.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { BootProviderResult } from "../agent/bootProviderConfig.js";
|
|
2
2
|
import type { ProviderAuthContext } from "../agent/providerAuth.js";
|
|
3
|
+
import type { AgentChatController } from "../agents/chatTurn.js";
|
|
4
|
+
import type { ConversationBackend } from "../agents/sessions.js";
|
|
5
|
+
import type { AgentService } from "../agents/service.js";
|
|
3
6
|
import type { RunStore } from "../runstore/port.js";
|
|
4
7
|
import type { RunChangeBus } from "../runtime/runChangeBus.js";
|
|
5
8
|
import type { RunManager } from "../runtime/runManager.js";
|
|
@@ -11,6 +14,9 @@ export type McpToolDeps = {
|
|
|
11
14
|
providerAuthContext?: ProviderAuthContext;
|
|
12
15
|
projectRoot?: string;
|
|
13
16
|
providerBoot?: BootProviderResult;
|
|
17
|
+
agentService?: AgentService;
|
|
18
|
+
agentChat?: AgentChatController;
|
|
19
|
+
agentConversationBackend?: ConversationBackend;
|
|
14
20
|
};
|
|
15
21
|
export type McpHttpDeps = McpToolDeps & {
|
|
16
22
|
runChangeBus: RunChangeBus;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { loadDraftPackage, validateDraftPackage, type DraftPackage } from "../config/draftPackage.js";
|
|
2
|
+
export { loadDraftPackage, validateDraftPackage, type DraftPackage };
|
|
3
|
+
export type PublishFacadeResult = {
|
|
4
|
+
ok: true;
|
|
5
|
+
pipelinePath: string;
|
|
6
|
+
stagePaths: string[];
|
|
7
|
+
taskPath?: string;
|
|
8
|
+
} | {
|
|
9
|
+
ok: false;
|
|
10
|
+
error: string;
|
|
11
|
+
status: number;
|
|
12
|
+
};
|
|
13
|
+
/** Write a package the way the Workshop's save does, flattening its result shape. */
|
|
14
|
+
export declare function publishDraftPackageFacade(projectRoot: string, input: {
|
|
15
|
+
directory: string;
|
|
16
|
+
draft: DraftPackage;
|
|
17
|
+
pipelineFilename: string;
|
|
18
|
+
allowInvalid?: boolean;
|
|
19
|
+
}, mode: "create" | "overwrite"): Promise<PublishFacadeResult>;
|