stageflow 0.36.0 → 0.39.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 +18 -8
- package/dist/agent/claudeAdapter.js +6 -0
- package/dist/agent/piAdapter.d.ts +9 -1
- package/dist/agent/piAdapter.js +21 -2
- 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/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 +7 -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/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 +6 -9
- 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/stageAttemptBootstrap.js +15 -1
- package/dist/runtime/stageEnvironment.d.ts +2 -0
- package/dist/runtime/stageEnvironment.js +6 -1
- package/dist/runtime/stageProcessLauncher.js +23 -0
- package/dist/runtime/stageSecrets.d.ts +6 -1
- package/dist/runtime/stageSecrets.js +34 -1
- 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/tools/readEmail.d.ts +3 -3
- package/dist/types/pipeline.d.ts +3 -0
- package/dist/types/stage.d.ts +2 -0
- 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 +99 -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/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/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/stage-mcp/README.md +1 -1
- package/package.json +2 -1
- package/dist/ui/assets/index-BOhqnPNg.js +0 -260
|
@@ -11,6 +11,8 @@ import { MIGRATION_009 } from "./009-trigger-next-run.js";
|
|
|
11
11
|
import { MIGRATION_010 } from "./010-trigger-adapter-state.js";
|
|
12
12
|
import { MIGRATION_011 } from "./011-email-dispatch-key.js";
|
|
13
13
|
import { MIGRATION_012 } from "./012-trigger-fires.js";
|
|
14
|
+
import { MIGRATION_013 } from "./013-agents.js";
|
|
15
|
+
import { MIGRATION_014 } from "./014-agent-deliveries.js";
|
|
14
16
|
const DEFAULT_MIGRATIONS = [
|
|
15
17
|
MIGRATION_001,
|
|
16
18
|
MIGRATION_002,
|
|
@@ -24,6 +26,8 @@ const DEFAULT_MIGRATIONS = [
|
|
|
24
26
|
MIGRATION_010,
|
|
25
27
|
MIGRATION_011,
|
|
26
28
|
MIGRATION_012,
|
|
29
|
+
MIGRATION_013,
|
|
30
|
+
MIGRATION_014,
|
|
27
31
|
];
|
|
28
32
|
export const CURRENT_SCHEMA_VERSION = DEFAULT_MIGRATIONS[DEFAULT_MIGRATIONS.length - 1].version;
|
|
29
33
|
function throwStoreSchemaTooNew(db, version, maxKnown) {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { emailHostFor, stageEmail, workerStageEmail, validateStageEmailAccounts } from "../email/host.js";
|
|
2
2
|
import { ChannelError } from "../channels/errors.js";
|
|
3
3
|
import { channelHostFor, stageChannels, validateStageChannelConnections, workerStageChannels } from "../channels/host.js";
|
|
4
|
+
import { bindStageMemory } from "../memory/access.js";
|
|
5
|
+
import { memoryHostFor } from "../memory/host.js";
|
|
6
|
+
import { workerMemoryForStage } from "../memory/worker.js";
|
|
7
|
+
import { memoryPrompt } from "../tools/memoryTools.js";
|
|
4
8
|
import { EmailError } from "../email/port.js";
|
|
5
9
|
import { bindingKindForOrigin, catalogOrSeededOrigin, decideWorkspaceConfigTrust, } from "../config/configOrigin.js";
|
|
6
10
|
import { loginCheckPromptBlock, readStageLoginCheck, } from "../browser/loginCheck.js";
|
|
@@ -98,12 +102,13 @@ async function resolveAttemptMcpServers(allowlist, factoryCwd, artifactsDir, opt
|
|
|
98
102
|
};
|
|
99
103
|
const stageIdOpt = options.stageId !== undefined ? { stageId: options.stageId } : {};
|
|
100
104
|
const bindingKind = bindingKindForOrigin(options.bindingKind);
|
|
101
|
-
if (factoryCwd !== undefined && (await mcpCatalogExists(factoryCwd))) {
|
|
105
|
+
if (factoryCwd !== undefined && (await mcpCatalogExists(factoryCwd, { scope: { kind: "validate" } }))) {
|
|
102
106
|
await mkdir(artifactsDir, { recursive: true });
|
|
103
107
|
const resolved = await resolveStageMcpServers({
|
|
104
108
|
projectRoot: factoryCwd,
|
|
105
109
|
allowlist: names,
|
|
106
110
|
env,
|
|
111
|
+
scope: { kind: "stage", secrets: options.declaredSecrets ?? [] },
|
|
107
112
|
bindingKind,
|
|
108
113
|
trustWorkspaceConfig: options.trustWorkspaceConfig,
|
|
109
114
|
...stageIdOpt,
|
|
@@ -187,6 +192,7 @@ async function openStageWithOperatorCatalog(agent, input, catalog, factoryCwd, a
|
|
|
187
192
|
trustWorkspaceConfig: resolveOptions.trustWorkspaceConfig,
|
|
188
193
|
stageEnv: resolveOptions.stageEnv,
|
|
189
194
|
stageId: input.stageId,
|
|
195
|
+
declaredSecrets: (input.stage.secrets ?? []).map((d) => d.name),
|
|
190
196
|
});
|
|
191
197
|
resolvedMcpServers = mcp.servers;
|
|
192
198
|
origins.push(...mcp.origins);
|
|
@@ -462,6 +468,11 @@ export async function openStageAttempt(input) {
|
|
|
462
468
|
throw error;
|
|
463
469
|
}
|
|
464
470
|
}
|
|
471
|
+
const stageMemory = input.stage.memories?.length
|
|
472
|
+
? process.env.SF_STAGE_WORKER === "1"
|
|
473
|
+
? await workerMemoryForStage(input.stage.memories)
|
|
474
|
+
: await bindStageMemory(memoryHostFor(), input.stage.memories)
|
|
475
|
+
: undefined;
|
|
465
476
|
const opened = await openStageWithOperatorCatalog(input.agent, {
|
|
466
477
|
roots,
|
|
467
478
|
stage: input.stage,
|
|
@@ -481,6 +492,9 @@ export async function openStageAttempt(input) {
|
|
|
481
492
|
channels: process.env.SF_STAGE_WORKER === "1" ? workerStageChannels()
|
|
482
493
|
: stageChannels(channelHostFor(input.factoryCwd ?? process.cwd()), input.stage, input.runId),
|
|
483
494
|
} : {}),
|
|
495
|
+
...(stageMemory
|
|
496
|
+
? { memory: stageMemory, memoryPrompt: await memoryPrompt(stageMemory) }
|
|
497
|
+
: {}),
|
|
484
498
|
...(input.sessionMode !== undefined ? { sessionMode: input.sessionMode } : {}),
|
|
485
499
|
onActivity: (event) => {
|
|
486
500
|
input.onActivity?.(input.stage.browser !== undefined
|
|
@@ -2,6 +2,7 @@ export declare const STAGE_ENV_PASSTHROUGH = "STAGEFLOW_STAGE_ENV_PASSTHROUGH";
|
|
|
2
2
|
export declare const STAGE_ENV_ALLOW = "STAGEFLOW_STAGE_ENV_ALLOW";
|
|
3
3
|
/** Names copied from Host when present (exact-name allowlist; no prefix wildcards). */
|
|
4
4
|
export declare const STAGE_ENV_ALLOWLIST: readonly string[];
|
|
5
|
+
export declare const PROVIDER_AND_CLOUD_NAMES: readonly ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "OPENAI_API_KEY_FILE", "GOOGLE_API_KEY", "GEMINI_API_KEY", "GROQ_API_KEY", "MISTRAL_API_KEY", "DEEPSEEK_API_KEY", "TOGETHER_API_KEY", "FIREWORKS_API_KEY", "PERPLEXITY_API_KEY", "XAI_API_KEY", "COHERE_API_KEY", "OPENROUTER_API_KEY", "AZURE_OPENAI_API_KEY", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN", "AWS_SECURITY_TOKEN", "AWS_PROFILE", "AWS_DEFAULT_REGION", "AWS_REGION", "GOOGLE_APPLICATION_CREDENTIALS", "GCLOUD_PROJECT", "AZURE_CLIENT_SECRET", "AZURE_TENANT_ID", "AZURE_CLIENT_ID", "NPM_TOKEN", "NODE_AUTH_TOKEN", "SSH_AUTH_SOCK", "SSH_AGENT_PID", "GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN_FILE", "GH_TOKEN_FILE"];
|
|
5
6
|
/** Never grantable — even via secrets `as: env`. */
|
|
6
7
|
export declare const FOREVER_DENIED_SECRET_NAMES: ReadonlySet<string>;
|
|
7
8
|
/** Blocked from ambient / ALLOW / PASSTHROUGH; `as: env` grants may still inject. */
|
|
@@ -29,6 +30,7 @@ export type BuildStageEnvironmentResult = {
|
|
|
29
30
|
passthroughActive: boolean;
|
|
30
31
|
};
|
|
31
32
|
export declare function isForeverDeniedSecret(name: string): boolean;
|
|
33
|
+
export declare function isProviderOrCloudName(name: string): boolean;
|
|
32
34
|
export declare function isAmbientBlockedEnv(name: string): boolean;
|
|
33
35
|
/**
|
|
34
36
|
* Construct the stage child environment. Exact-name allowlist only — never a
|
|
@@ -27,7 +27,7 @@ export const STAGE_ENV_ALLOWLIST = [
|
|
|
27
27
|
"STAGEFLOW_HOME",
|
|
28
28
|
"STAGEFLOW_CURSOR_EXTENSION",
|
|
29
29
|
];
|
|
30
|
-
const PROVIDER_AND_CLOUD_NAMES = [
|
|
30
|
+
export const PROVIDER_AND_CLOUD_NAMES = [
|
|
31
31
|
"ANTHROPIC_API_KEY",
|
|
32
32
|
"OPENAI_API_KEY",
|
|
33
33
|
"OPENAI_API_KEY_FILE",
|
|
@@ -70,6 +70,8 @@ export const FOREVER_DENIED_SECRET_NAMES = new Set([
|
|
|
70
70
|
"STAGEFLOW_CONTROL_TOKEN_FILE",
|
|
71
71
|
"STAGEFLOW_READ_TOKEN",
|
|
72
72
|
"STAGEFLOW_READ_TOKEN_FILE",
|
|
73
|
+
// Host-generated key for the MCP OAuth token store; never hand it to a stage or an MCP server.
|
|
74
|
+
"PI_MCP_ADAPTER_OAUTH_FILE_KEY",
|
|
73
75
|
]);
|
|
74
76
|
/** Blocked from ambient / ALLOW / PASSTHROUGH; `as: env` grants may still inject. */
|
|
75
77
|
export const AMBIENT_BLOCKED_ENV_NAMES = new Set([
|
|
@@ -94,6 +96,9 @@ export function isForeverDeniedSecret(name) {
|
|
|
94
96
|
return true;
|
|
95
97
|
return name.startsWith("STAGEFLOW_CONTROL_TOKEN_");
|
|
96
98
|
}
|
|
99
|
+
export function isProviderOrCloudName(name) {
|
|
100
|
+
return PROVIDER_AND_CLOUD_NAMES.includes(name);
|
|
101
|
+
}
|
|
97
102
|
export function isAmbientBlockedEnv(name) {
|
|
98
103
|
return isForeverDeniedSecret(name) || AMBIENT_BLOCKED_ENV_NAMES.has(name);
|
|
99
104
|
}
|
|
@@ -15,6 +15,8 @@ import { SF_STAGE_WORKER, STAGE_WORKER_EXIT, } from "./stageWorkerProtocol.js";
|
|
|
15
15
|
import { emailHostFor, emailWorkerEnvironment, stageEmail, validateStageEmailAccounts } from "../email/host.js";
|
|
16
16
|
import { ChannelError } from "../channels/errors.js";
|
|
17
17
|
import { channelFault, channelHostFor, channelsWorkerEnvironment, stageChannels, validateStageChannelConnections } from "../channels/host.js";
|
|
18
|
+
import { MemoryError, memoryHostFor } from "../memory/host.js";
|
|
19
|
+
import { isMemoryVerb, runMemoryRequest } from "../memory/access.js";
|
|
18
20
|
import { EmailError } from "../email/port.js";
|
|
19
21
|
import { overlayStageBindingEnv, } from "./stageRoots.js";
|
|
20
22
|
function defaultStageCliEntry() {
|
|
@@ -443,6 +445,27 @@ export class StageProcessLauncher {
|
|
|
443
445
|
})();
|
|
444
446
|
return;
|
|
445
447
|
}
|
|
448
|
+
const memoryVerb = request?.type;
|
|
449
|
+
if (!settled && isMemoryVerb(memoryVerb)) {
|
|
450
|
+
void (async () => {
|
|
451
|
+
if (typeof request.requestId !== "string" || request.requestId.length > 100)
|
|
452
|
+
return;
|
|
453
|
+
let response;
|
|
454
|
+
try {
|
|
455
|
+
const stage = input.stage;
|
|
456
|
+
if (!stage || settled)
|
|
457
|
+
throw new MemoryError("MEMORY_NOT_ATTACHED");
|
|
458
|
+
response = await runMemoryRequest(memoryHostFor(), stage.memories ?? [], memoryVerb, request.input);
|
|
459
|
+
}
|
|
460
|
+
catch (error) {
|
|
461
|
+
const code = error instanceof MemoryError ? error.code : "MEMORY_UNREADABLE";
|
|
462
|
+
response = { error: { code } };
|
|
463
|
+
}
|
|
464
|
+
if (child.connected && !settled)
|
|
465
|
+
child.send({ type: "memory.response", requestId: request.requestId, ...response });
|
|
466
|
+
})();
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
446
469
|
if (!settled && request?.type === "channel.send") {
|
|
447
470
|
void (async () => {
|
|
448
471
|
if (typeof request.requestId !== "string" || request.requestId.length > 100)
|
|
@@ -9,6 +9,11 @@ export type SecretRegistryEntry = {
|
|
|
9
9
|
name: string;
|
|
10
10
|
source: string;
|
|
11
11
|
pointerVar: string;
|
|
12
|
+
}
|
|
13
|
+
/** A secret saved from the console (Settings → Project MCP). It only fills `${NAME}` in MCP servers; never the stage environment. */
|
|
14
|
+
| {
|
|
15
|
+
kind: "console";
|
|
16
|
+
name: string;
|
|
12
17
|
};
|
|
13
18
|
export type SecretRegistry = Map<string, SecretRegistryEntry>;
|
|
14
19
|
export declare class SecretUnavailableError extends Error {
|
|
@@ -35,6 +40,6 @@ export type ResolveStageSecretsResult = {
|
|
|
35
40
|
credentialDirs: string[];
|
|
36
41
|
};
|
|
37
42
|
/** Presence-only check (no materialisation) for preflight / start gates. */
|
|
38
|
-
export declare function assertSecretPresent(name: string, registry: SecretRegistry, hostEnv: NodeJS.ProcessEnv): void;
|
|
43
|
+
export declare function assertSecretPresent(name: string, registry: SecretRegistry, hostEnv: NodeJS.ProcessEnv, home?: string): void;
|
|
39
44
|
export declare function resolveStageSecrets(input: ResolveStageSecretsInput): ResolveStageSecretsResult;
|
|
40
45
|
export { cleanupCredentialsDir };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { shouldRegisterValue } from "../logging/redact.js";
|
|
3
|
+
import { readMcpSecrets } from "../config/mcpHostCatalog.js";
|
|
3
4
|
import { globalStageflowHome } from "../project/globalHome.js";
|
|
4
5
|
import { attemptCredentialsDir, cleanupCredentialsDir, ensureStageAskpassHelper, materialiseCredentialFile, materialiseSecretBytes, readFileCredentialRegistry, } from "./credentialMaterialisation.js";
|
|
5
6
|
import { defaultSecretDelivery, } from "./stageSecretDecl.js";
|
|
@@ -66,10 +67,20 @@ export function loadSecretRegistry(env = process.env, home = globalStageflowHome
|
|
|
66
67
|
continue;
|
|
67
68
|
registry.set(name, { kind: "env", name });
|
|
68
69
|
}
|
|
70
|
+
// Console-stored MCP secrets: a stage may declare them so its `mcp:` servers can be filled.
|
|
71
|
+
try {
|
|
72
|
+
for (const name of Object.keys(readMcpSecrets(home))) {
|
|
73
|
+
if (!isForeverDeniedSecret(name) && !registry.has(name))
|
|
74
|
+
registry.set(name, { kind: "console", name });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// A corrupt host-state file is reported by the MCP routes; it must not stop other stages.
|
|
79
|
+
}
|
|
69
80
|
return registry;
|
|
70
81
|
}
|
|
71
82
|
/** Presence-only check (no materialisation) for preflight / start gates. */
|
|
72
|
-
export function assertSecretPresent(name, registry, hostEnv) {
|
|
83
|
+
export function assertSecretPresent(name, registry, hostEnv, home) {
|
|
73
84
|
if (isForeverDeniedSecret(name)) {
|
|
74
85
|
throw new Error(`secret "${name}" is permanently denied`);
|
|
75
86
|
}
|
|
@@ -86,11 +97,24 @@ export function assertSecretPresent(name, registry, hostEnv) {
|
|
|
86
97
|
}
|
|
87
98
|
return;
|
|
88
99
|
}
|
|
100
|
+
if (entry.kind === "console") {
|
|
101
|
+
if (!readMcpSecretValue(name, home))
|
|
102
|
+
throw new SecretUnavailableError(name);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
89
105
|
const value = readEnvSecretValue(name, hostEnv);
|
|
90
106
|
if (value === undefined || value === "") {
|
|
91
107
|
throw new SecretUnavailableError(name);
|
|
92
108
|
}
|
|
93
109
|
}
|
|
110
|
+
function readMcpSecretValue(name, home) {
|
|
111
|
+
try {
|
|
112
|
+
return readMcpSecrets(home)[name];
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
94
118
|
function materialiseGitAskpassGrant(grantEnv, credentialDirs, opts) {
|
|
95
119
|
const destDir = attemptCredentialsDir(opts.attemptDir, opts.secretName);
|
|
96
120
|
const tokenFile = materialiseSecretBytes({
|
|
@@ -119,6 +143,15 @@ export function resolveStageSecrets(input) {
|
|
|
119
143
|
if (entry === undefined) {
|
|
120
144
|
throw new Error(`stage.unknown_secret: "${decl.name}"`);
|
|
121
145
|
}
|
|
146
|
+
if (entry.kind === "console") {
|
|
147
|
+
const stored = readMcpSecretValue(decl.name, input.home);
|
|
148
|
+
if (!stored)
|
|
149
|
+
throw new SecretUnavailableError(decl.name);
|
|
150
|
+
if (shouldRegisterValue(stored))
|
|
151
|
+
knownValues.push({ name: decl.name, value: stored });
|
|
152
|
+
warnings.push(`secrets: ${decl.name} is a console secret; it fills MCP servers only and is not set in the stage environment`);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
122
155
|
if (entry.kind === "file") {
|
|
123
156
|
const destDir = attemptCredentialsDir(input.attemptDir, decl.name);
|
|
124
157
|
const homeRoot = input.home ?? globalStageflowHome();
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import type { OperatorAgentHost } from "../operatorAgent/index.js";
|
|
3
|
+
type Json = (res: ServerResponse, status: number, body: unknown) => void;
|
|
4
|
+
export declare function handleAgentAuthorRoutes(req: IncomingMessage, method: string, pathname: string, projectRoot: string, host: OperatorAgentHost, res: ServerResponse, json: Json): Promise<boolean>;
|
|
5
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { parseAgentAuthorDraft, runAgentAuthorTurn, undoAgentAuthorTurn } from "../agents/authorChat.js";
|
|
2
|
+
async function readBody(req) {
|
|
3
|
+
const chunks = [];
|
|
4
|
+
let size = 0;
|
|
5
|
+
for await (const chunk of req) {
|
|
6
|
+
const buffer = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
7
|
+
size += buffer.length;
|
|
8
|
+
if (size > 65536)
|
|
9
|
+
return { __tooLarge: true };
|
|
10
|
+
chunks.push(buffer);
|
|
11
|
+
}
|
|
12
|
+
if (chunks.length === 0)
|
|
13
|
+
return {};
|
|
14
|
+
try {
|
|
15
|
+
const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
16
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
|
|
17
|
+
? parsed
|
|
18
|
+
: { __invalid: true };
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return { __invalid: true };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export async function handleAgentAuthorRoutes(req, method, pathname, projectRoot, host, res, json) {
|
|
25
|
+
const chat = pathname === "/api/agent-author/chat";
|
|
26
|
+
const undo = pathname === "/api/agent-author/undo";
|
|
27
|
+
if (!chat && !undo)
|
|
28
|
+
return false;
|
|
29
|
+
if (method !== "POST") {
|
|
30
|
+
json(res, 405, { code: "agent.invalid" });
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
const body = await readBody(req);
|
|
34
|
+
if (body.__tooLarge || body.__invalid) {
|
|
35
|
+
json(res, 400, { code: "agent.invalid" });
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
if (undo) {
|
|
40
|
+
const sessionId = typeof body.sessionId === "string" ? body.sessionId.trim() : "";
|
|
41
|
+
if (!sessionId) {
|
|
42
|
+
json(res, 400, { code: "agent.invalid" });
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
json(res, 200, { draft: undoAgentAuthorTurn(sessionId) });
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
const message = typeof body.message === "string" ? body.message : "";
|
|
49
|
+
const result = await runAgentAuthorTurn({
|
|
50
|
+
host,
|
|
51
|
+
projectRoot,
|
|
52
|
+
message,
|
|
53
|
+
...(typeof body.sessionId === "string" ? { sessionId: body.sessionId } : {}),
|
|
54
|
+
draft: parseAgentAuthorDraft(body.draft),
|
|
55
|
+
});
|
|
56
|
+
json(res, 200, result);
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
const code = err instanceof Error ? err.message : "agent.internal";
|
|
60
|
+
const status = code === "agent.session_not_found" ? 404 : code === "agent.conflict" ? 409 : 400;
|
|
61
|
+
json(res, status, { code: code.startsWith("agent.") ? code : "agent.internal" });
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import type { AgentHostContext } from "../agents/hostContext.js";
|
|
3
|
+
type Json = (res: ServerResponse, status: number, body: unknown) => void;
|
|
4
|
+
export declare function handleAgentRoutes(req: IncomingMessage, method: string, pathname: string, host: AgentHostContext, projectRoot: string, res: ServerResponse, json: Json): Promise<boolean>;
|
|
5
|
+
export {};
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { AgentError, isAgentError } from "../agents/errors.js";
|
|
2
|
+
import { describeToolkit } from "../toolkit/index.js";
|
|
3
|
+
import { isSessionStatus } from "../agents/store.js";
|
|
4
|
+
import { BodyError, readJsonBody, writeFrame } from "../agents/ndjson.js";
|
|
5
|
+
const MAX_BODY_BYTES = 256 * 1024;
|
|
6
|
+
const CONSOLE_ACTOR = { source: "console" };
|
|
7
|
+
async function readBody(req) {
|
|
8
|
+
try {
|
|
9
|
+
return await readJsonBody(req, MAX_BODY_BYTES);
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
if (!(error instanceof BodyError))
|
|
13
|
+
throw error;
|
|
14
|
+
throw new AgentError("agent.invalid", error.reason === "too_large" ? "Request body is too large" : error.reason === "not_json" ? "Request body is not JSON" : "Request body must be a JSON object");
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function requireInt(value, name) {
|
|
18
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
19
|
+
throw new AgentError("agent.invalid", `${name} must be a non-negative integer`);
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
function requireText(value, name) {
|
|
24
|
+
if (typeof value !== "string" || value.trim().length === 0)
|
|
25
|
+
throw new AgentError("agent.invalid", `${name} is required`);
|
|
26
|
+
return value.trim();
|
|
27
|
+
}
|
|
28
|
+
function optionalText(value) {
|
|
29
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
30
|
+
}
|
|
31
|
+
function sessionQuery(url) {
|
|
32
|
+
const status = url.searchParams.get("status") ?? undefined;
|
|
33
|
+
if (status !== undefined && !isSessionStatus(status)) {
|
|
34
|
+
throw new AgentError("agent.invalid", "Unknown session status");
|
|
35
|
+
}
|
|
36
|
+
const limit = Number(url.searchParams.get("limit") ?? 25);
|
|
37
|
+
return {
|
|
38
|
+
limit: Number.isInteger(limit) && limit > 0 && limit <= 100 ? limit : 25,
|
|
39
|
+
...(status && isSessionStatus(status) ? { status } : {}),
|
|
40
|
+
...(url.searchParams.get("source") ? { source: url.searchParams.get("source") } : {}),
|
|
41
|
+
...(url.searchParams.get("q")?.trim() ? { search: url.searchParams.get("q").trim() } : {}),
|
|
42
|
+
...(url.searchParams.get("cursor") ? { cursor: url.searchParams.get("cursor") } : {}),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** A session must belong to the agent named in the path, or the request is a 404. */
|
|
46
|
+
async function sessionFor(ctx, agentId, sessionId) {
|
|
47
|
+
const detail = await ctx.host.service.getSession(ctx.projectRoot, sessionId);
|
|
48
|
+
if (detail.session.agentId !== agentId)
|
|
49
|
+
throw new AgentError("agent.session_not_found");
|
|
50
|
+
return detail;
|
|
51
|
+
}
|
|
52
|
+
async function chat(ctx, agentId) {
|
|
53
|
+
const body = await readBody(ctx.req);
|
|
54
|
+
const message = requireText(body.message, "message");
|
|
55
|
+
const sessionId = optionalText(body.sessionId);
|
|
56
|
+
const wantsStream = body.stream === true || String(ctx.req.headers.accept ?? "").includes("application/x-ndjson");
|
|
57
|
+
const send = (onDelta, onSessionOpened) => ctx.host.chat.send({
|
|
58
|
+
projectRoot: ctx.projectRoot,
|
|
59
|
+
agentId,
|
|
60
|
+
text: message,
|
|
61
|
+
source: "console",
|
|
62
|
+
...(sessionId ? { sessionId } : {}),
|
|
63
|
+
...(onDelta ? { onDelta } : {}),
|
|
64
|
+
...(onSessionOpened ? { onSessionOpened } : {}),
|
|
65
|
+
});
|
|
66
|
+
if (!wantsStream) {
|
|
67
|
+
ctx.json(ctx.res, 200, await send());
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
ctx.res.writeHead(200, { "Content-Type": "application/x-ndjson; charset=utf-8", "Cache-Control": "no-store" });
|
|
71
|
+
let writes = Promise.resolve();
|
|
72
|
+
let finished = false;
|
|
73
|
+
let openedId = sessionId;
|
|
74
|
+
ctx.res.once("close", () => {
|
|
75
|
+
if (finished)
|
|
76
|
+
return;
|
|
77
|
+
if (openedId)
|
|
78
|
+
ctx.host.chat.stop(openedId);
|
|
79
|
+
else
|
|
80
|
+
ctx.host.chat.stopOpening(agentId);
|
|
81
|
+
});
|
|
82
|
+
try {
|
|
83
|
+
const detail = await send((text) => {
|
|
84
|
+
writes = writes.then(() => writeFrame(ctx.res, { type: "delta", text }));
|
|
85
|
+
}, (id) => {
|
|
86
|
+
openedId = id;
|
|
87
|
+
writes = writes.then(() => writeFrame(ctx.res, { type: "session", sessionId: id }));
|
|
88
|
+
});
|
|
89
|
+
finished = true;
|
|
90
|
+
await writes;
|
|
91
|
+
await writeFrame(ctx.res, { type: "done", session: detail });
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
finished = true;
|
|
95
|
+
await writes.catch(() => undefined);
|
|
96
|
+
await writeFrame(ctx.res, {
|
|
97
|
+
type: "error",
|
|
98
|
+
code: isAgentError(error) ? error.code : "agent.internal",
|
|
99
|
+
...(isAgentError(error) && error.message !== error.code ? { message: error.message } : {}),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
if (!ctx.res.writableEnded && !ctx.res.destroyed)
|
|
104
|
+
ctx.res.end();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const routes = [
|
|
108
|
+
{
|
|
109
|
+
method: "GET",
|
|
110
|
+
pattern: /^\/api\/agents$/,
|
|
111
|
+
handle: async (ctx) => ctx.json(ctx.res, 200, { agents: await ctx.host.service.list(ctx.projectRoot) }),
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
method: "POST",
|
|
115
|
+
pattern: /^\/api\/agents$/,
|
|
116
|
+
handle: async (ctx) => {
|
|
117
|
+
const body = await readBody(ctx.req);
|
|
118
|
+
ctx.json(ctx.res, 201, await ctx.host.service.create(ctx.projectRoot, body.definition, CONSOLE_ACTOR));
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
method: "POST",
|
|
123
|
+
pattern: /^\/api\/agents\/validate$/,
|
|
124
|
+
handle: async (ctx) => {
|
|
125
|
+
const body = await readBody(ctx.req);
|
|
126
|
+
ctx.json(ctx.res, 200, await ctx.host.service.validate(ctx.projectRoot, body.definition));
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
method: "GET",
|
|
131
|
+
pattern: /^\/api\/agents\/stageflow-tools$/,
|
|
132
|
+
handle: async ({ res, json }) => json(res, 200, describeToolkit()),
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
method: "GET",
|
|
136
|
+
pattern: /^\/api\/agents\/([^/]+)\/channel$/,
|
|
137
|
+
handle: async (ctx) => {
|
|
138
|
+
const detail = await ctx.host.service.get(ctx.projectRoot, ctx.params[0]);
|
|
139
|
+
const connectionId = detail.definition?.channel?.connection_id ?? null;
|
|
140
|
+
// `listener` is absent for webhook providers and for agents that are disabled.
|
|
141
|
+
const listener = connectionId ? ctx.host.channelListeners?.status(connectionId) : undefined;
|
|
142
|
+
const failed = ctx.host.service.latestUndelivered(ctx.projectRoot, ctx.params[0]);
|
|
143
|
+
ctx.json(ctx.res, 200, {
|
|
144
|
+
connectionId,
|
|
145
|
+
...(listener ? { listener } : {}),
|
|
146
|
+
...(failed ? { lastUndelivered: { turnId: failed.turnId, sessionId: failed.sessionId, state: failed.state, code: failed.errorCode, message: failed.errorMessage, at: failed.updatedAt } } : {}),
|
|
147
|
+
});
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
method: "POST",
|
|
152
|
+
pattern: /^\/api\/agents\/([^/]+)\/sessions\/([^/]+)\/turns\/([^/]+)\/delivery\/(resend|delivered)$/,
|
|
153
|
+
handle: async (ctx) => {
|
|
154
|
+
const [agentId, sessionId, turnId, action] = ctx.params;
|
|
155
|
+
const session = await ctx.host.service.getSession(ctx.projectRoot, sessionId);
|
|
156
|
+
if (session.session.agentId !== agentId || !session.turns.some((turn) => turn.id === turnId))
|
|
157
|
+
throw new AgentError("agent.session_not_found");
|
|
158
|
+
if (action === "resend")
|
|
159
|
+
await ctx.host.delivery.resend(turnId);
|
|
160
|
+
else
|
|
161
|
+
ctx.host.delivery.markDelivered(turnId);
|
|
162
|
+
ctx.json(ctx.res, 200, await ctx.host.service.getSession(ctx.projectRoot, sessionId));
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
method: "GET",
|
|
167
|
+
pattern: /^\/api\/agents\/([^/]+)\/channel\/contact$/,
|
|
168
|
+
handle: async (ctx) => {
|
|
169
|
+
const detail = await ctx.host.service.get(ctx.projectRoot, ctx.params[0]);
|
|
170
|
+
const connectionId = detail.definition?.channel?.connection_id;
|
|
171
|
+
const found = connectionId ? await ctx.host.channelContact(connectionId) : undefined;
|
|
172
|
+
ctx.json(ctx.res, 200, { connectionId: connectionId ?? null, ...(found ?? {}) });
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
method: "GET",
|
|
177
|
+
pattern: /^\/api\/agents\/sessions$/,
|
|
178
|
+
handle: async (ctx) => ctx.json(ctx.res, 200, await ctx.host.service.listAllSessions(ctx.projectRoot, sessionQuery(ctx.url), ctx.url.searchParams.get("agent") || undefined)),
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
method: "GET",
|
|
182
|
+
pattern: /^\/api\/agents\/approvals$/,
|
|
183
|
+
handle: async (ctx) => ctx.json(ctx.res, 200, { approvals: await ctx.host.service.pendingApprovals(ctx.projectRoot) }),
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
method: "GET",
|
|
187
|
+
pattern: /^\/api\/agents\/([^/]+)$/,
|
|
188
|
+
handle: async (ctx) => ctx.json(ctx.res, 200, await ctx.host.service.get(ctx.projectRoot, ctx.params[0])),
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
method: "POST",
|
|
192
|
+
pattern: /^\/api\/agents\/([^/]+)\/save$/,
|
|
193
|
+
handle: async (ctx) => {
|
|
194
|
+
const body = await readBody(ctx.req);
|
|
195
|
+
ctx.json(ctx.res, 200, await ctx.host.service.save(ctx.projectRoot, ctx.params[0], {
|
|
196
|
+
baseRevision: requireInt(body.baseRevision, "baseRevision"),
|
|
197
|
+
definition: body.definition,
|
|
198
|
+
summary: optionalText(body.summary) ?? null,
|
|
199
|
+
}, CONSOLE_ACTOR));
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
method: "POST",
|
|
204
|
+
pattern: /^\/api\/agents\/([^/]+)\/(enable|disable)$/,
|
|
205
|
+
handle: async (ctx) => ctx.json(ctx.res, 200, await ctx.host.service.setEnabled(ctx.projectRoot, ctx.params[0], ctx.params[1] === "enable", CONSOLE_ACTOR)),
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
method: "POST",
|
|
209
|
+
pattern: /^\/api\/agents\/([^/]+)\/archive$/,
|
|
210
|
+
handle: async (ctx) => {
|
|
211
|
+
await ctx.host.service.archive(ctx.projectRoot, ctx.params[0]);
|
|
212
|
+
ctx.json(ctx.res, 200, { archived: true });
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
method: "GET",
|
|
217
|
+
pattern: /^\/api\/agents\/([^/]+)\/revisions$/,
|
|
218
|
+
handle: async (ctx) => ctx.json(ctx.res, 200, { revisions: await ctx.host.service.listRevisions(ctx.projectRoot, ctx.params[0]) }),
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
method: "GET",
|
|
222
|
+
pattern: /^\/api\/agents\/([^/]+)\/revisions\/(\d+)$/,
|
|
223
|
+
handle: async (ctx) => ctx.json(ctx.res, 200, await ctx.host.service.getRevision(ctx.projectRoot, ctx.params[0], Number(ctx.params[1]))),
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
method: "GET",
|
|
227
|
+
pattern: /^\/api\/agents\/([^/]+)\/revisions\/(\d+)\/diff\/(\d+)$/,
|
|
228
|
+
handle: async (ctx) => ctx.json(ctx.res, 200, {
|
|
229
|
+
lines: await ctx.host.service.diffRevisions(ctx.projectRoot, ctx.params[0], Number(ctx.params[1]), Number(ctx.params[2])),
|
|
230
|
+
}),
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
method: "POST",
|
|
234
|
+
pattern: /^\/api\/agents\/([^/]+)\/revisions\/(\d+)\/restore$/,
|
|
235
|
+
handle: async (ctx) => {
|
|
236
|
+
const body = await readBody(ctx.req);
|
|
237
|
+
ctx.json(ctx.res, 200, await ctx.host.service.restore(ctx.projectRoot, ctx.params[0], { revision: Number(ctx.params[1]), baseRevision: requireInt(body.baseRevision, "baseRevision") }, CONSOLE_ACTOR));
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
method: "GET",
|
|
242
|
+
pattern: /^\/api\/agents\/([^/]+)\/sessions$/,
|
|
243
|
+
handle: async (ctx) => ctx.json(ctx.res, 200, await ctx.host.service.listSessions(ctx.projectRoot, ctx.params[0], sessionQuery(ctx.url))),
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
method: "GET",
|
|
247
|
+
pattern: /^\/api\/agents\/([^/]+)\/sessions\/([^/]+)$/,
|
|
248
|
+
handle: async (ctx) => ctx.json(ctx.res, 200, await sessionFor(ctx, ctx.params[0], ctx.params[1])),
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
method: "GET",
|
|
252
|
+
pattern: /^\/api\/agents\/([^/]+)\/sessions\/([^/]+)\/otlp$/,
|
|
253
|
+
handle: async (ctx) => {
|
|
254
|
+
await sessionFor(ctx, ctx.params[0], ctx.params[1]);
|
|
255
|
+
ctx.res.setHeader("Content-Disposition", `attachment; filename="${ctx.params[1]}.otlp.json"`);
|
|
256
|
+
ctx.json(ctx.res, 200, ctx.host.service.exportSessionOtlp(ctx.projectRoot, ctx.params[1]));
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
method: "POST",
|
|
261
|
+
pattern: /^\/api\/agents\/([^/]+)\/sessions\/([^/]+)\/approval\/(approve|deny)$/,
|
|
262
|
+
handle: async (ctx) => {
|
|
263
|
+
await sessionFor(ctx, ctx.params[0], ctx.params[1]);
|
|
264
|
+
const approval = await ctx.host.service.decideApproval(ctx.projectRoot, ctx.params[1], ctx.params[2] === "approve" ? "approve" : "deny", { kind: "operator" });
|
|
265
|
+
ctx.json(ctx.res, 200, { approval, session: await sessionFor(ctx, ctx.params[0], ctx.params[1]) });
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
method: "POST",
|
|
270
|
+
pattern: /^\/api\/agents\/([^/]+)\/chat$/,
|
|
271
|
+
handle: async (ctx) => chat(ctx, ctx.params[0]),
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
method: "POST",
|
|
275
|
+
pattern: /^\/api\/agents\/([^/]+)\/chat\/stop$/,
|
|
276
|
+
handle: async (ctx) => {
|
|
277
|
+
const body = await readBody(ctx.req);
|
|
278
|
+
const sessionId = requireText(body.sessionId, "sessionId");
|
|
279
|
+
await sessionFor(ctx, ctx.params[0], sessionId);
|
|
280
|
+
ctx.json(ctx.res, 200, { stopped: ctx.host.chat.stop(sessionId) });
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
];
|
|
284
|
+
export async function handleAgentRoutes(req, method, pathname, host, projectRoot, res, json) {
|
|
285
|
+
for (const route of routes) {
|
|
286
|
+
const match = route.pattern.exec(pathname);
|
|
287
|
+
if (!match)
|
|
288
|
+
continue;
|
|
289
|
+
if (route.method !== method)
|
|
290
|
+
continue;
|
|
291
|
+
try {
|
|
292
|
+
const params = match.slice(1).map((segment) => decodeURIComponent(segment));
|
|
293
|
+
await route.handle({ req, res, json, host, projectRoot, params, url: new URL(req.url ?? pathname, "http://agents") });
|
|
294
|
+
}
|
|
295
|
+
catch (error) {
|
|
296
|
+
if (res.headersSent) {
|
|
297
|
+
if (!res.writableEnded)
|
|
298
|
+
res.end();
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
if (error instanceof URIError) {
|
|
302
|
+
json(res, 400, { code: "agent.invalid", message: "Malformed path" });
|
|
303
|
+
}
|
|
304
|
+
else if (isAgentError(error)) {
|
|
305
|
+
json(res, error.httpStatus, {
|
|
306
|
+
code: error.code,
|
|
307
|
+
...(error.message !== error.code ? { message: error.message } : {}),
|
|
308
|
+
...(error.issues.length > 0 ? { issues: error.issues } : {}),
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
const known = routes.some((route) => route.pattern.test(pathname));
|
|
318
|
+
if (known) {
|
|
319
|
+
json(res, 405, { code: "agent.method_not_allowed" });
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
return false;
|
|
323
|
+
}
|