@atolis-hq/wake 0.4.47 → 0.4.49

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.
Files changed (77) hide show
  1. package/README.md +3 -0
  2. package/dist/src/activities/agent/agent-activity.js +9 -3
  3. package/dist/src/activities/contracts/activity.d.ts +14 -0
  4. package/dist/src/artifacts/application/artifact-projection.d.ts +3 -0
  5. package/dist/src/artifacts/application/artifact-projection.js +15 -0
  6. package/dist/src/artifacts/application/artifact-repository.d.ts +13 -0
  7. package/dist/src/artifacts/application/artifact-repository.js +19 -0
  8. package/dist/src/artifacts/application/artifact-service.d.ts +40 -0
  9. package/dist/src/artifacts/application/artifact-service.js +128 -0
  10. package/dist/src/artifacts/contracts/config.d.ts +6 -0
  11. package/dist/src/artifacts/contracts/config.js +19 -0
  12. package/dist/src/artifacts/contracts/event-factory.d.ts +6 -0
  13. package/dist/src/artifacts/contracts/event-factory.js +10 -0
  14. package/dist/src/artifacts/contracts/events.d.ts +33 -0
  15. package/dist/src/artifacts/contracts/events.js +59 -0
  16. package/dist/src/artifacts/contracts/identifiers.d.ts +5 -0
  17. package/dist/src/artifacts/contracts/identifiers.js +8 -0
  18. package/dist/src/artifacts/contracts/paths.d.ts +5 -0
  19. package/dist/src/artifacts/contracts/paths.js +9 -0
  20. package/dist/src/artifacts/contracts/streams.d.ts +7 -0
  21. package/dist/src/artifacts/contracts/streams.js +4 -0
  22. package/dist/src/artifacts/contracts/views.d.ts +20 -0
  23. package/dist/src/artifacts/contracts/views.js +1 -0
  24. package/dist/src/artifacts/domain/artifact-work-item.d.ts +4 -0
  25. package/dist/src/artifacts/domain/artifact-work-item.js +42 -0
  26. package/dist/src/artifacts/index.d.ts +14 -0
  27. package/dist/src/artifacts/index.js +14 -0
  28. package/dist/src/artifacts/infrastructure/file-artifact-store.d.ts +22 -0
  29. package/dist/src/artifacts/infrastructure/file-artifact-store.js +51 -0
  30. package/dist/src/artifacts/infrastructure/mcp-server.d.ts +18 -0
  31. package/dist/src/artifacts/infrastructure/mcp-server.js +135 -0
  32. package/dist/src/artifacts/infrastructure/mcp-session-store.d.ts +25 -0
  33. package/dist/src/artifacts/infrastructure/mcp-session-store.js +61 -0
  34. package/dist/src/bootstrap/artifact-mcp.d.ts +23 -0
  35. package/dist/src/bootstrap/artifact-mcp.js +119 -0
  36. package/dist/src/bootstrap/board-projection.d.ts +2 -0
  37. package/dist/src/bootstrap/board-projection.js +27 -11
  38. package/dist/src/bootstrap/composition-root.d.ts +2 -0
  39. package/dist/src/bootstrap/composition-root.js +13 -0
  40. package/dist/src/bootstrap/config/root-schema.d.ts +6 -0
  41. package/dist/src/bootstrap/config/root-schema.js +5 -0
  42. package/dist/src/bootstrap/paths.d.ts +1 -0
  43. package/dist/src/bootstrap/paths.js +1 -0
  44. package/dist/src/bootstrap/projection-runtime.js +2 -0
  45. package/dist/src/bootstrap/runner-registry.js +22 -4
  46. package/dist/src/bootstrap/surface-api-work-applications.js +39 -1
  47. package/dist/src/bootstrap/surface-cli-applications.js +12 -1
  48. package/dist/src/bootstrap/version.js +1 -1
  49. package/dist/src/execution/application/execution-activity.d.ts +11 -0
  50. package/dist/src/execution/application/execution-activity.js +26 -10
  51. package/dist/src/execution/application/execution-service.js +1 -1
  52. package/dist/src/execution/contracts/runner.d.ts +10 -0
  53. package/dist/src/execution/infrastructure/runners/claude.d.ts +1 -0
  54. package/dist/src/execution/infrastructure/runners/claude.js +21 -0
  55. package/dist/src/execution/infrastructure/runners/codex.js +12 -0
  56. package/dist/src/execution/infrastructure/runners/fake.d.ts +1 -0
  57. package/dist/src/execution/infrastructure/runners/fake.js +1 -0
  58. package/dist/src/execution/infrastructure/runners/registry.d.ts +3 -1
  59. package/dist/src/execution/infrastructure/runners/registry.js +5 -3
  60. package/dist/src/surfaces/api/contracts/board.d.ts +2 -0
  61. package/dist/src/surfaces/api/contracts/work.d.ts +10 -0
  62. package/dist/src/surfaces/api/routes/applications.d.ts +5 -0
  63. package/dist/src/surfaces/api/routes/read.js +34 -3
  64. package/dist/src/surfaces/api/routes/work.d.ts +1 -1
  65. package/dist/src/surfaces/api/routes/work.js +1 -0
  66. package/dist/src/surfaces/cli/main.d.ts +8 -0
  67. package/dist/src/surfaces/cli/main.js +31 -0
  68. package/dist/src/surfaces/cli/usage.js +1 -0
  69. package/dist/src/surfaces/index.d.ts +1 -0
  70. package/dist/src/surfaces/index.js +1 -0
  71. package/dist/src/surfaces/mcp/stdio.d.ts +3 -0
  72. package/dist/src/surfaces/mcp/stdio.js +5 -0
  73. package/dist/src/surfaces/web-assets/assets/{index-aTndh5BE.js → index-CuIiUKuh.js} +3 -3
  74. package/dist/src/surfaces/web-assets/assets/index-DKpuc-RV.css +1 -0
  75. package/dist/src/surfaces/web-assets/index.html +2 -2
  76. package/package.json +3 -1
  77. package/dist/src/surfaces/web-assets/assets/index-D1NjO4qa.css +0 -1
@@ -1,11 +1,29 @@
1
1
  import { createClaudeRunner, createCodexRunner, createCommandRunner, createCursorRunner, emptyFakeScenarios, FakeExecutionRunner, RunnerRegistry, } from '../execution/index.js';
2
2
  export function createRunnerRegistry(config, scenarios = emptyFakeScenarios, decorateRunner) {
3
- const runners = Object.fromEntries(Object.entries(config.agentRunners ?? {}).map(([name, runner]) => [
4
- name,
5
- (decorateRunner ?? identity)(createConfiguredRunner(name, runner, scenarios), name),
6
- ]));
3
+ const runners = Object.fromEntries(Object.entries(config.agentRunners ?? {}).map(([name, runner]) => {
4
+ const configured = createConfiguredRunner(name, runner, scenarios);
5
+ const decorated = (decorateRunner ?? identity)(configured, name);
6
+ return [name, preserveRunnerCapabilities(configured, decorated)];
7
+ }));
7
8
  return new RunnerRegistry(config.runnerPools, runners);
8
9
  }
10
+ // Test and operational decorators commonly wrap only `start`; adapter capabilities
11
+ // describe the underlying transport and must survive that wrapper.
12
+ function preserveRunnerCapabilities(configured, decorated) {
13
+ const capabilities = {
14
+ ...(decorated.supportsSessionResume === undefined &&
15
+ configured.supportsSessionResume !== undefined
16
+ ? { supportsSessionResume: configured.supportsSessionResume }
17
+ : {}),
18
+ ...(decorated.supportsEphemeralMcp === undefined &&
19
+ configured.supportsEphemeralMcp !== undefined
20
+ ? { supportsEphemeralMcp: configured.supportsEphemeralMcp }
21
+ : {}),
22
+ };
23
+ if (Object.keys(capabilities).length === 0)
24
+ return decorated;
25
+ return Object.assign(Object.create(decorated), capabilities);
26
+ }
9
27
  function identity(value) {
10
28
  return value;
11
29
  }
@@ -4,7 +4,7 @@ import { conversationIdForWorkItem, ConversationOriginKind, } from '../conversat
4
4
  import { executionProjection, runsByWorkflowInstanceProjection, } from '../execution/index.js';
5
5
  import { ApprovalAuthorityKind, ConversationSurfaceCapability, GroupBudgetExtensionIneligibleError, isGroupBudgetExtensionEligible, OperatorRetryIneligibleError, orchestrationProjection, selectOperatorRetryTarget, workflowsByWorkItemProjection, } from '../orchestration/index.js';
6
6
  import { workCorrelationsProjection, } from '../resources/index.js';
7
- import { ApiCommandStatus, fromWorkItemKey, presentResource, presentRun, presentWorkflowInstance, presentWorkItem, } from '../surfaces/index.js';
7
+ import { ApiCommandStatus, fromWorkItemKey, presentResource, presentRun, presentWorkflowInstance, presentWorkItem, toWorkItemKey, } from '../surfaces/index.js';
8
8
  import { workItemId, WorkStatus } from '../work/index.js';
9
9
  import { primaryExternalRef } from './external-ref.js';
10
10
  import { projectionMeta } from './surface-api-metadata.js';
@@ -29,6 +29,20 @@ export function createSurfaceWorkApplications(root, now) {
29
29
  });
30
30
  },
31
31
  detail: (key) => workDetail(root, key, now),
32
+ async artifact(key, revisionId) {
33
+ const id = decodeWorkItemId(key);
34
+ if (id === undefined)
35
+ return undefined;
36
+ const accepted = new Set((await root.orchestration.listForWorkItem(id)).flatMap((workflow) => workflow.acceptedOutcomes));
37
+ const artifact = await root.artifacts.readAcceptedRevision(id, revisionId, async (activationId) => accepted.has(activationId));
38
+ if (artifact === null)
39
+ return undefined;
40
+ return {
41
+ bytes: artifact.bytes,
42
+ mediaType: safeMediaType(artifact.revision.mediaType),
43
+ filename: artifact.revision.path.split('/').at(-1) ?? 'artifact',
44
+ };
45
+ },
32
46
  async transcript(key, groupId) {
33
47
  const id = decodeWorkItemId(key);
34
48
  if (id === undefined)
@@ -197,6 +211,7 @@ async function workDetail(root, key, now) {
197
211
  transcriptGroups: await transcriptGroups(root.transcriptStore, id, runs),
198
212
  },
199
213
  activities: presentPullRequest(pullRequest?.value),
214
+ artifacts: await presentArtifacts(root, id),
200
215
  conversation: presentConversation(conversation, root.config?.surfaces.api.conversationMessages.enabled === true),
201
216
  };
202
217
  const [projections, correlationFacts] = await Promise.all([
@@ -208,6 +223,29 @@ async function workDetail(root, key, now) {
208
223
  meta: await projectionMeta(root.journal, [...projections, ...correlationFacts, ...(pullRequest == null ? [] : [pullRequest])], now()),
209
224
  };
210
225
  }
226
+ async function presentArtifacts(root, id) {
227
+ const partial = root;
228
+ if (partial.artifacts === undefined || partial.orchestration?.listAll === undefined)
229
+ return [];
230
+ const accepted = new Set((await partial.orchestration.listAll())
231
+ .filter((workflow) => workflow.workItemId === id)
232
+ .flatMap((workflow) => workflow.acceptedOutcomes));
233
+ return (await partial.artifacts.latestPublished(id, async (activationId) => accepted.has(activationId))).map((artifact) => ({
234
+ revisionId: artifact.revisionId,
235
+ producer: artifact.producer,
236
+ path: artifact.path,
237
+ occurredAt: artifact.occurredAt,
238
+ ...(artifact.mediaType === undefined ? {} : { mediaType: artifact.mediaType }),
239
+ ...(artifact.byteLength === undefined ? {} : { byteLength: artifact.byteLength }),
240
+ ...(artifact.digest === undefined ? {} : { digest: artifact.digest }),
241
+ href: `/api/v1/work-items/${encodeURIComponent(toWorkItemKey(id))}/artifacts/${encodeURIComponent(artifact.revisionId)}`,
242
+ }));
243
+ }
244
+ function safeMediaType(value) {
245
+ return value !== undefined && /^[^\r\n]+\/[^\r\n]+$/.test(value)
246
+ ? value
247
+ : 'application/octet-stream';
248
+ }
211
249
  async function conversationForWorkItem(root, id) {
212
250
  // Narrow test/application seams may provide a partial composition root while
213
251
  // the production root always composes Conversations.
@@ -6,11 +6,14 @@ import { join } from 'node:path';
6
6
  import { createInterface } from 'node:readline/promises';
7
7
  import { promisify } from 'node:util';
8
8
  import { BuiltInActivityName, agentActivityDefinition } from '../activities/index.js';
9
+ import { FileArtifactMcpSessionStore } from '../artifacts/index.js';
9
10
  import { IntakeHost, ResidentHost, TickHost, } from '../control-plane/index.js';
10
11
  import { ExecutionFailureCode, RunStatus, isActiveRunStatus, loadPromptTemplate, } from '../execution/index.js';
12
+ import { UlidIdGenerator } from '../kernel/index.js';
11
13
  import { ResourceCorrelationRole, resourceId } from '../resources/index.js';
12
- import { DockerProcessError, createApiDispatcher, createLoggedDockerCli, createPackagedAssetSource, createPairingGrant, createProcessLogSink, createSandboxDockerPort, createSurfaceHttpServer, describeSandboxStartupFailure, drainProcessOutput, loadOrCreateCredentials, promoteSandboxImage, redeemPairingGrant, replaceAccessKey, runDoctor, runSandbox, runSandboxEntrypoint, runSandboxSetup, runSelfUpdateLatestLoop, runTargetSmoke, verifyResidentStart, waitForActiveRuns, waitForever, } from '../surfaces/index.js';
14
+ import { DockerProcessError, createApiDispatcher, createLoggedDockerCli, createPackagedAssetSource, createPairingGrant, createProcessLogSink, createSandboxDockerPort, createSurfaceHttpServer, describeSandboxStartupFailure, drainProcessOutput, loadOrCreateCredentials, promoteSandboxImage, redeemPairingGrant, replaceAccessKey, runDoctor, runSandbox, runSandboxEntrypoint, runSandboxSetup, runSelfUpdateLatestLoop, runTargetSmoke, serveMcpOverStdio, verifyResidentStart, waitForActiveRuns, waitForever, } from '../surfaces/index.js';
13
15
  import { WorkStreamKind, workItemId } from '../work/index.js';
16
+ import { createArtifactMcpServerForSession } from './artifact-mcp.js';
14
17
  import { loadConfig } from './config/load-config.js';
15
18
  import { createNpmUpdatePort } from './npm-update-port.js';
16
19
  import { runtimeProjectionDefinitions } from './projection-runtime.js';
@@ -24,6 +27,8 @@ import { UpdateMaintenancePhase } from './update-maintenance-lease.js';
24
27
  import { resolveWakeVersion, wakePackageVersion } from './version.js';
25
28
  const execFile = promisify(nodeExecFile);
26
29
  export function createSurfaceCliApplications(root, api, now) {
30
+ const artifactMcpSessions = new FileArtifactMcpSessionStore(root.paths.artifactsRoot, now);
31
+ const artifactMcpIds = new UlidIdGenerator();
27
32
  const runnerTick = new TickHost(createOneShotRunnerAdvance(root));
28
33
  const runnerResidentTick = new TickHost(createResidentRunnerAdvance(root));
29
34
  const intakeHost = new IntakeHost((signal) => root.intakePipeline.run(signal));
@@ -84,6 +89,12 @@ export function createSurfaceCliApplications(root, api, now) {
84
89
  },
85
90
  api: { start: (options) => startHttp(options, false) },
86
91
  ui: { start: (options) => startHttp(options, true) },
92
+ mcp: {
93
+ async serve(sessionPath, token) {
94
+ const server = await createArtifactMcpServerForSession(root, artifactMcpSessions, sessionPath, token, { now, nextId: () => artifactMcpIds.next('artifact') });
95
+ await serveMcpOverStdio(server);
96
+ },
97
+ },
87
98
  auth: {
88
99
  async token(accessKey) {
89
100
  if (root.config.surfaces.web.auth.disabled)
@@ -124,6 +124,6 @@ export function resolveWakeVersion(options = {}) {
124
124
  return `g${headHash.slice(0, 7)}`;
125
125
  return '0.1.0-dev';
126
126
  }
127
- export const wakeVersion = "v0.4.47";
127
+ export const wakeVersion = "v0.4.49";
128
128
  /** The registry-installable version used by packaged sandbox images. */
129
129
  export const wakePackageVersion = packageVersion(defaultRepoRoot) ?? wakeVersion;
@@ -4,6 +4,7 @@ import { type Clock, type IdGenerator } from '../../kernel/index.js';
4
4
  import type { ExecutionActivation, ExecutionAttemptContext } from '../contracts/commands.js';
5
5
  import type { ExecutionConfig } from '../contracts/config.js';
6
6
  import type { runId } from '../contracts/identifiers.js';
7
+ import { type RunnerMcpServer } from '../contracts/runner.js';
7
8
  import type { WorkspaceProvider } from '../contracts/workspace.js';
8
9
  import type { RunnerRegistry } from '../infrastructure/runners/registry.js';
9
10
  import type { RunRepository } from './run-repository.js';
@@ -12,6 +13,11 @@ export interface ExecutionDependencies {
12
13
  readonly ids: IdGenerator;
13
14
  readonly workspaces?: WorkspaceProvider;
14
15
  readonly runners?: RunnerRegistry;
16
+ readonly mcp?: (input: {
17
+ readonly runId: string;
18
+ readonly activation: ExecutionActivation;
19
+ readonly context: ExecutionAttemptContext;
20
+ }) => Promise<ExecutionMcpConfiguration>;
15
21
  readonly transcriptRecorder?: ActivityExecutionContext['transcriptRecorder'];
16
22
  readonly logOperationalError?: ActivityExecutionContext['logOperationalError'];
17
23
  readonly reportRunnerQuota?: (input: {
@@ -19,6 +25,11 @@ export interface ExecutionDependencies {
19
25
  readonly message: string;
20
26
  }) => Promise<void>;
21
27
  }
28
+ export interface ExecutionMcpConfiguration {
29
+ readonly servers: readonly RunnerMcpServer[];
30
+ readonly prompt?: string;
31
+ release(): Promise<void>;
32
+ }
22
33
  export interface ExecutionRuntime {
23
34
  readonly activities: ActivityRegistry;
24
35
  readonly config: ExecutionConfig;
@@ -4,10 +4,15 @@ import { ExecutionCancellationReason } from '../contracts/vocabulary.js';
4
4
  import { parseAgentRunnerResponse } from '../infrastructure/agent-runner-adapter.js';
5
5
  import { createRunEvent } from './run-lifecycle.js';
6
6
  import { confirmCancellation, requestCancellation } from './run-liveness-service.js';
7
+ // Execution context assembly intentionally keeps every run-bound capability visible at dispatch.
8
+ // eslint-disable-next-line max-lines-per-function, complexity
7
9
  export async function executeActivity(runtime, currentRunId, request) {
8
10
  const { activation, context, occurredAt, runner } = request;
9
11
  const controller = activityController(runtime, currentRunId);
10
12
  runtime.active.set(currentRunId, controller);
13
+ const mcp = runner === undefined || runtime.dependencies.mcp === undefined
14
+ ? undefined
15
+ : await runtime.dependencies.mcp({ runId: currentRunId, activation, context });
11
16
  const executionContext = {
12
17
  signal: controller.signal,
13
18
  occurredAt,
@@ -20,6 +25,12 @@ export async function executeActivity(runtime, currentRunId, request) {
20
25
  : { reportRunnerStarted: request.reportRunnerStarted }),
21
26
  ...workspaceContext(request.workspace),
22
27
  ...(runner === undefined ? {} : { runner }),
28
+ ...(mcp === undefined
29
+ ? {}
30
+ : {
31
+ mcpServers: mcp.servers,
32
+ ...(mcp.prompt === undefined ? {} : { mcpPrompt: mcp.prompt }),
33
+ }),
23
34
  ...(runtime.dependencies.transcriptRecorder === undefined
24
35
  ? {}
25
36
  : { transcriptRecorder: runtime.dependencies.transcriptRecorder }),
@@ -41,16 +52,21 @@ export async function executeActivity(runtime, currentRunId, request) {
41
52
  reportRunnerResult: runnerResultReporter(runtime, currentRunId, context, activation, request),
42
53
  reportRunnerTimeout: runnerTimeoutReporter(runtime, currentRunId),
43
54
  };
44
- return runtime.activities.execute({
45
- activationId: activation.activationId,
46
- activity: activation.activity,
47
- workItemId: context.workItemId,
48
- workflowInstanceId: context.workflowInstanceId,
49
- orchestrationGroupId: context.orchestrationGroupId,
50
- causationId: activation.activationId,
51
- input: activation.input,
52
- resources: context.resources,
53
- }, executionContext);
55
+ try {
56
+ return await runtime.activities.execute({
57
+ activationId: activation.activationId,
58
+ activity: activation.activity,
59
+ workItemId: context.workItemId,
60
+ workflowInstanceId: context.workflowInstanceId,
61
+ orchestrationGroupId: context.orchestrationGroupId,
62
+ causationId: activation.activationId,
63
+ input: activation.input,
64
+ resources: context.resources,
65
+ }, executionContext);
66
+ }
67
+ finally {
68
+ await mcp?.release();
69
+ }
54
70
  }
55
71
  function activityController(runtime, currentRunId) {
56
72
  return runtime.active.get(currentRunId) ?? new AbortController();
@@ -563,7 +563,7 @@ function resolveRunner(runtime, executionKind, activation, context) {
563
563
  throw new Error(`Unknown execution runner pool: ${runnerPool}`);
564
564
  if (executionKind !== ActivityExecutionKind.Agent)
565
565
  return { runner: undefined };
566
- const resolved = runtime.dependencies.runners?.resolve(runnerPool, context.ineligibleRunners ?? new Set());
566
+ const resolved = runtime.dependencies.runners?.resolve(runnerPool, context.ineligibleRunners ?? new Set(), { ephemeralMcp: runtime.dependencies.mcp !== undefined });
567
567
  return describeResolvedRunner(runtime, runnerPool, resolved);
568
568
  }
569
569
  function describeResolvedRunner(runtime, pool, resolved) {
@@ -1,6 +1,13 @@
1
1
  import type { ExternalExecutionKind } from '../../activities/index.js';
2
2
  import type { FinishedRunStatus, WorkspaceMode } from './vocabulary.js';
3
3
  export declare const ProviderQuotaExceededFailureKind = "provider-quota-exceeded";
4
+ /** A trusted MCP process configured only for this runner invocation. */
5
+ export interface RunnerMcpServer {
6
+ readonly name: string;
7
+ readonly command: string;
8
+ readonly args?: readonly string[];
9
+ readonly env?: Readonly<Record<string, string>>;
10
+ }
4
11
  export interface RunnerRequest {
5
12
  readonly runId: string;
6
13
  readonly prompt: string;
@@ -23,6 +30,7 @@ export interface RunnerRequest {
23
30
  readonly cacheRead?: number;
24
31
  readonly cacheWrite?: number;
25
32
  };
33
+ readonly mcpServers?: readonly RunnerMcpServer[];
26
34
  }
27
35
  export type AgentRunOutcome = 'DONE' | 'REJECTED' | 'BLOCKED' | 'FAILED' | 'NEEDS_CLARIFICATION';
28
36
  export interface AgentRunResponse {
@@ -74,5 +82,7 @@ export interface RunnerExecution {
74
82
  export interface Runner {
75
83
  /** Whether this adapter can continue an earlier session with its opaque session ID. */
76
84
  readonly supportsSessionResume?: boolean;
85
+ /** Whether this adapter can inject trusted MCP configuration per invocation. */
86
+ readonly supportsEphemeralMcp?: boolean;
77
87
  start(request: RunnerRequest, signal: AbortSignal): Promise<RunnerExecution>;
78
88
  }
@@ -14,6 +14,7 @@ export declare function cliRunner(name: string, command: string, args: (request:
14
14
  readonly runnerTimeouts?: ProcessTimeouts;
15
15
  readonly defaultModel?: string;
16
16
  readonly supportsSessionResume?: boolean;
17
+ readonly supportsEphemeralMcp?: boolean;
17
18
  readonly parseSuccessfulOutput?: (stdout: string, request: RunnerRequest) => Partial<AgentRunnerResult>;
18
19
  readonly classifyFailure?: (input: {
19
20
  readonly stdout: string;
@@ -9,6 +9,7 @@ export function createClaudeRunner(options = {}) {
9
9
  parseSuccessfulOutput: parseClaudeOutput,
10
10
  classifyFailure: classifyClaudeFailure,
11
11
  supportsSessionResume: true,
12
+ supportsEphemeralMcp: true,
12
13
  });
13
14
  }
14
15
  export function parseClaudeOutput(stdout, _request) {
@@ -84,15 +85,35 @@ export function claudeCommandArgs(request, passthroughArgs = [], defaults = {})
84
85
  ...(request.allowedTools.length === 0
85
86
  ? []
86
87
  : ['--allowedTools', request.allowedTools.join(' ')]),
88
+ ...(request.mcpServers === undefined || request.mcpServers.length === 0
89
+ ? []
90
+ : ['--mcp-config', JSON.stringify(claudeMcpConfig(request)), '--strict-mcp-config']),
87
91
  ...passthroughArgs,
88
92
  '--',
89
93
  request.prompt,
90
94
  ];
91
95
  }
96
+ function claudeMcpConfig(request) {
97
+ return {
98
+ mcpServers: Object.fromEntries((request.mcpServers ?? []).map((server) => [
99
+ server.name,
100
+ {
101
+ command: server.command,
102
+ ...(server.args === undefined ? {} : { args: server.args }),
103
+ ...(server.env === undefined ? {} : { env: server.env }),
104
+ },
105
+ ])),
106
+ };
107
+ }
92
108
  export function cliRunner(name, command, args, options = {}) {
93
109
  return {
94
110
  supportsSessionResume: options.supportsSessionResume === true,
111
+ supportsEphemeralMcp: options.supportsEphemeralMcp === true,
95
112
  async start(request, signal) {
113
+ if (request.mcpServers !== undefined &&
114
+ request.mcpServers.length > 0 &&
115
+ !options.supportsEphemeralMcp)
116
+ throw new Error(`${name} runner does not support ephemeral MCP configuration`);
96
117
  const process = runProcess(command, args(request), request.workspacePath, signal, {
97
118
  ...options.runnerTimeouts,
98
119
  ...(request.onTimeout === undefined ? {} : { onTimeout: request.onTimeout }),
@@ -42,6 +42,7 @@ export function createCodexRunner(options = {}) {
42
42
  parseSuccessfulOutput: parseCodexOutput,
43
43
  classifyFailure: classifyCodexFailure,
44
44
  supportsSessionResume: true,
45
+ supportsEphemeralMcp: true,
45
46
  });
46
47
  return runner;
47
48
  }
@@ -59,11 +60,22 @@ export function codexCommandArgs(request, passthroughArgs = [], defaults = {}) {
59
60
  ...((request.effort ?? defaults.effort) === undefined
60
61
  ? []
61
62
  : ['-c', `model_reasoning_effort=${request.effort ?? defaults.effort}`]),
63
+ ...codexMcpConfigArgs(request),
62
64
  ...(request.resumeSessionId === undefined ? [] : ['resume', request.resumeSessionId]),
63
65
  ...passthroughArgs,
64
66
  `${request.prompt}\n\n${waitBackgroundInstruction}`,
65
67
  ];
66
68
  }
69
+ function codexMcpConfigArgs(request) {
70
+ return (request.mcpServers ?? []).flatMap((server) => [
71
+ '-c',
72
+ `mcp_servers.${JSON.stringify(server.name)}={command=${JSON.stringify(server.command)},args=${JSON.stringify(server.args ?? [])}${server.env === undefined
73
+ ? ''
74
+ : `,env={${Object.entries(server.env)
75
+ .map(([key, value]) => `${JSON.stringify(key)}=${JSON.stringify(value)}`)
76
+ .join(',')}}`}}`,
77
+ ]);
78
+ }
67
79
  // Work around Codex's missing background-command lifecycle contract (https://github.com/openai/codex/issues/32505, https://github.com/openai/codex/issues/34866).
68
80
  const waitBackgroundInstruction = `When a command you started is still running or lacks a terminal result, do not report a normal Wake outcome. Make your entire final response exactly WAIT_BACKGROUND. Wake will reject that stop attempt and ask you to continue. After that hook prompt, poll the still-running command and report a normal terminal status only after it has settled. Do not use WAIT_BACKGROUND for a human-input or implementation blocker.`;
69
81
  function codexSandboxMode(_workspaceMode) {
@@ -3,6 +3,7 @@ import { type FakeScenarioResolver } from './fake-scenarios.js';
3
3
  export declare class FakeExecutionRunner implements Runner {
4
4
  readonly name: string;
5
5
  private readonly scenarios;
6
+ readonly supportsEphemeralMcp = true;
6
7
  constructor(name?: string, scenarios?: FakeScenarioResolver);
7
8
  start(request: RunnerRequest, signal: AbortSignal): Promise<RunnerExecution>;
8
9
  }
@@ -4,6 +4,7 @@ import { emptyFakeScenarios, } from './fake-scenarios.js';
4
4
  export class FakeExecutionRunner {
5
5
  name;
6
6
  scenarios;
7
+ supportsEphemeralMcp = true;
7
8
  constructor(name = 'fake', scenarios = emptyFakeScenarios) {
8
9
  this.name = name;
9
10
  this.scenarios = scenarios;
@@ -7,7 +7,9 @@ export declare class RunnerRegistry {
7
7
  private readonly runnerPools;
8
8
  private readonly runners;
9
9
  constructor(runnerPools: Readonly<Record<string, readonly string[]>>, runners: Readonly<Record<string, Runner>>);
10
- resolve(runnerPool: string, ineligible?: ReadonlySet<string>): {
10
+ resolve(runnerPool: string, ineligible?: ReadonlySet<string>, requirements?: {
11
+ readonly ephemeralMcp?: boolean;
12
+ }): {
11
13
  readonly name: string;
12
14
  readonly runner: Runner;
13
15
  };
@@ -13,21 +13,23 @@ export class RunnerRegistry {
13
13
  this.runnerPools = runnerPools;
14
14
  this.runners = runners;
15
15
  }
16
- resolve(runnerPool, ineligible = new Set()) {
16
+ resolve(runnerPool, ineligible = new Set(), requirements = {}) {
17
17
  const candidates = this.runnerPools[runnerPool];
18
18
  if (candidates === undefined)
19
19
  throw new Error(`Execution runner pool ${runnerPool} has no runner`);
20
- return selectEligibleCandidate(runnerPool, candidates, this.runners, ineligible);
20
+ return selectEligibleCandidate(runnerPool, candidates, this.runners, ineligible, requirements);
21
21
  }
22
22
  }
23
23
  // Falls sideways to the next candidate on quota ineligibility; never a different runner pool.
24
- function selectEligibleCandidate(runnerPool, candidates, runners, ineligible) {
24
+ function selectEligibleCandidate(runnerPool, candidates, runners, ineligible, requirements) {
25
25
  for (const name of candidates) {
26
26
  if (ineligible.has(name))
27
27
  continue;
28
28
  const runner = runners[name];
29
29
  if (runner === undefined)
30
30
  throw new Error(`Runner ${name} is not registered`);
31
+ if (requirements.ephemeralMcp === true && runner.supportsEphemeralMcp !== true)
32
+ continue;
31
33
  return { name, runner };
32
34
  }
33
35
  throw new NoEligibleRunnerError(runnerPool);
@@ -22,6 +22,8 @@ export interface BoardCardResponse {
22
22
  readonly condition: BoardCardCondition;
23
23
  readonly frozen?: boolean;
24
24
  readonly awaitingApproval?: boolean;
25
+ /** Whether the primary workflow needs a gate-budget extension to continue. */
26
+ readonly extendEligible?: boolean;
25
27
  readonly workflowName?: string;
26
28
  readonly stage?: string;
27
29
  /** Why the primary workflow is blocked, when it requires operator intervention. */
@@ -35,6 +35,16 @@ export interface WorkDetailResponse {
35
35
  readonly activities: {
36
36
  readonly pullRequest?: PullRequestResponse;
37
37
  };
38
+ readonly artifacts: readonly {
39
+ readonly revisionId: string;
40
+ readonly producer: string;
41
+ readonly path: string;
42
+ readonly occurredAt: string;
43
+ readonly mediaType?: string;
44
+ readonly byteLength?: number;
45
+ readonly digest?: string;
46
+ readonly href: string;
47
+ }[];
38
48
  readonly conversation: {
39
49
  /** Whether this authenticated surface may create control-plane entries. */
40
50
  readonly canCreateEntries: boolean;
@@ -56,6 +56,11 @@ export interface ApiApplications {
56
56
  list(query: CollectionQuery): Promise<ApiCollectionPage<WorkItemResponse>>;
57
57
  detail(key: string): Promise<ApiResourceResult<WorkDetailResponse> | undefined>;
58
58
  transcript?(key: string, groupId: string): Promise<ApiResourceResult<WorkItemTranscriptResponse> | undefined>;
59
+ artifact?(key: string, revisionId: string): Promise<{
60
+ readonly bytes: Uint8Array;
61
+ readonly mediaType: string;
62
+ readonly filename: string;
63
+ } | undefined>;
59
64
  freeze?(key: string, command: ApiCommandRequest): Promise<ApiCommandResult>;
60
65
  unfreeze?(key: string, command: ApiCommandRequest): Promise<ApiCommandResult>;
61
66
  delete?(key: string, command: ApiCommandRequest): Promise<ApiCommandResult>;
@@ -108,9 +108,9 @@ async function collectionResult(applications, pathname, query) {
108
108
  }
109
109
  }
110
110
  async function readDetail(applications, pathname) {
111
- const workTranscript = /^\/api\/v1\/work-items\/([^/]+)\/transcripts\/([^/]+)$/.exec(pathname);
112
- if (workTranscript?.[1] !== undefined && workTranscript[2] !== undefined)
113
- return readWorkTranscript(applications, workTranscript[1], workTranscript[2]);
111
+ const workSubresource = await readWorkSubresource(applications, pathname);
112
+ if (workSubresource !== undefined)
113
+ return workSubresource;
114
114
  const transcript = /^\/api\/v1\/runs\/([^/]+)\/transcript$/.exec(pathname)?.[1];
115
115
  if (transcript !== undefined)
116
116
  return readTranscript(applications, transcript);
@@ -120,6 +120,37 @@ async function readDetail(applications, pathname) {
120
120
  const run = /^\/api\/v1\/runs\/([^/]+)$/.exec(pathname)?.[1];
121
121
  return run === undefined ? undefined : readRun(applications, run);
122
122
  }
123
+ async function readWorkSubresource(applications, pathname) {
124
+ const workTranscript = /^\/api\/v1\/work-items\/([^/]+)\/transcripts\/([^/]+)$/.exec(pathname);
125
+ if (workTranscript?.[1] !== undefined && workTranscript[2] !== undefined)
126
+ return readWorkTranscript(applications, workTranscript[1], workTranscript[2]);
127
+ const artifact = /^\/api\/v1\/work-items\/([^/]+)\/artifacts\/([^/]+)$/.exec(pathname);
128
+ if (artifact?.[1] !== undefined && artifact[2] !== undefined)
129
+ return readWorkArtifact(applications, artifact[1], artifact[2]);
130
+ return undefined;
131
+ }
132
+ async function readWorkArtifact(applications, keySegment, revisionSegment) {
133
+ const key = decodePathSegment(keySegment);
134
+ const revisionId = decodePathSegment(revisionSegment);
135
+ if (key instanceof ApiPathError)
136
+ return invalidPath(key.message);
137
+ if (revisionId instanceof ApiPathError)
138
+ return invalidPath(revisionId.message);
139
+ if (applications.work.artifact === undefined)
140
+ return unavailable('artifact');
141
+ const artifact = await applications.work.artifact(key, revisionId);
142
+ if (artifact === undefined)
143
+ return found(undefined);
144
+ return {
145
+ status: 200,
146
+ body: artifact.bytes,
147
+ contentType: artifact.mediaType,
148
+ headers: {
149
+ 'content-disposition': `attachment; filename*=UTF-8''${encodeURIComponent(artifact.filename)}`,
150
+ 'content-length': String(artifact.bytes.byteLength),
151
+ },
152
+ };
153
+ }
123
154
  async function readWorkTranscript(applications, keySegment, groupSegment) {
124
155
  const key = decodePathSegment(keySegment);
125
156
  const groupId = decodePathSegment(groupSegment);
@@ -1 +1 @@
1
- export declare const workRoutes: readonly ["/api/v1/work-items", "/api/v1/work-items/:workItemKey", "/api/v1/work-items/:workItemKey/transcripts/:groupId", "/api/v1/work-items/:workItemKey/commands/freeze", "/api/v1/work-items/:workItemKey/commands/unfreeze", "/api/v1/work-items/:workItemKey/commands/delete", "/api/v1/work-items/:workItemKey/commands/retry", "/api/v1/work-items/:workItemKey/commands/extend", "/api/v1/work-items/:workItemKey/commands/message"];
1
+ export declare const workRoutes: readonly ["/api/v1/work-items", "/api/v1/work-items/:workItemKey", "/api/v1/work-items/:workItemKey/transcripts/:groupId", "/api/v1/work-items/:workItemKey/artifacts/:revisionId", "/api/v1/work-items/:workItemKey/commands/freeze", "/api/v1/work-items/:workItemKey/commands/unfreeze", "/api/v1/work-items/:workItemKey/commands/delete", "/api/v1/work-items/:workItemKey/commands/retry", "/api/v1/work-items/:workItemKey/commands/extend", "/api/v1/work-items/:workItemKey/commands/message"];
@@ -2,6 +2,7 @@ export const workRoutes = [
2
2
  '/api/v1/work-items',
3
3
  '/api/v1/work-items/:workItemKey',
4
4
  '/api/v1/work-items/:workItemKey/transcripts/:groupId',
5
+ '/api/v1/work-items/:workItemKey/artifacts/:revisionId',
5
6
  '/api/v1/work-items/:workItemKey/commands/freeze',
6
7
  '/api/v1/work-items/:workItemKey/commands/unfreeze',
7
8
  '/api/v1/work-items/:workItemKey/commands/delete',
@@ -15,6 +15,11 @@ export type WakeCommand = ({
15
15
  }) | ({
16
16
  readonly kind: 'api' | 'ui';
17
17
  } & HostOptions) | ({
18
+ readonly kind: 'mcp';
19
+ } & {
20
+ readonly sessionPath: string;
21
+ readonly wakeRoot?: string;
22
+ }) | ({
18
23
  readonly kind: 'ui-token';
19
24
  readonly accessKey?: string;
20
25
  } & Pick<HostOptions, 'wakeRoot'>) | {
@@ -84,6 +89,9 @@ export interface WakeCliApplications {
84
89
  readonly ui: {
85
90
  start(options?: HostOptions): Promise<void>;
86
91
  };
92
+ readonly mcp?: {
93
+ serve(sessionPath: string, token: string): Promise<void>;
94
+ };
87
95
  readonly auth?: {
88
96
  token(accessKey?: string): Promise<string>;
89
97
  };
@@ -27,6 +27,8 @@ export function parseWakeCommand(arguments_) {
27
27
  return parseUiCommand(arguments_.slice(1));
28
28
  case 'api':
29
29
  return { kind: command, ...parseHostOptions(arguments_.slice(1)) };
30
+ case 'mcp':
31
+ return parseMcpCommand(arguments_.slice(1));
30
32
  case 'tick':
31
33
  case 'start':
32
34
  case 'stop':
@@ -44,6 +46,27 @@ export function parseWakeCommand(arguments_) {
44
46
  throw new Error(`Unknown wake command: ${command ?? ''}`);
45
47
  }
46
48
  }
49
+ function parseMcpCommand(arguments_) {
50
+ if (arguments_[0] !== 'serve')
51
+ throw new Error(`Unknown mcp command: ${arguments_[0] ?? ''}`);
52
+ let sessionPath;
53
+ let wakeRoot;
54
+ for (let index = 1; index < arguments_.length; index += 2) {
55
+ const flag = arguments_[index];
56
+ const value = requiredArgument(arguments_[index + 1], `value for ${flag ?? 'option'}`);
57
+ if (flag === '--session' && sessionPath === undefined)
58
+ sessionPath = value;
59
+ else if (flag === '--wake-root' && wakeRoot === undefined)
60
+ wakeRoot = value;
61
+ else
62
+ throw new Error(`Unknown or duplicate mcp option: ${flag ?? ''}`);
63
+ }
64
+ return {
65
+ kind: 'mcp',
66
+ sessionPath: requiredArgument(sessionPath, 'mcp --session'),
67
+ ...(wakeRoot === undefined ? {} : { wakeRoot }),
68
+ };
69
+ }
47
70
  function parseUiCommand(arguments_) {
48
71
  if (arguments_[0] !== 'token')
49
72
  return { kind: 'ui', ...parseHostOptions(arguments_) };
@@ -194,6 +217,9 @@ export async function runWakeCommand(command, applications, output, signal) {
194
217
  case 'ui-token':
195
218
  output.write(await formatUiTokenOutput(auth(applications).token(command.accessKey)));
196
219
  return;
220
+ case 'mcp':
221
+ await mcp(applications).serve(command.sessionPath, process.env.WAKE_MCP_TOKEN ?? '');
222
+ return;
197
223
  case 'audit':
198
224
  for (const record of await applications.audit.read(command.workItemId))
199
225
  output.write(`${JSON.stringify(record)}\n`);
@@ -247,6 +273,11 @@ function auth(applications) {
247
273
  throw new Error('UI auth CLI application was not composed');
248
274
  return applications.auth;
249
275
  }
276
+ function mcp(applications) {
277
+ if (applications.mcp === undefined)
278
+ throw new Error('MCP CLI application was not composed');
279
+ return applications.mcp;
280
+ }
250
281
  function writeResult(output, value) {
251
282
  if (value !== undefined)
252
283
  output.write(`${JSON.stringify(value)}\n`);
@@ -14,6 +14,7 @@ export const usage = [
14
14
  ' wake ui token set <key> Replace the UI login access key',
15
15
  ' wake audit Show autonomous decision audit history',
16
16
  ' wake correlate Manually correlate a resource to a work item',
17
+ ' wake mcp serve Run the private run-scoped Wake MCP transport',
17
18
  ' wake run resolve Resolve an escalated ambiguous run',
18
19
  ' wake doctor Diagnose config/GitHub/Docker/sandbox setup problems',
19
20
  ' wake maintenance clear Clear an abandoned failed self-update maintenance lease',
@@ -45,6 +45,7 @@ export * from './cli/infrastructure/process-log.js';
45
45
  export * from './cli/infrastructure/sandbox-startup-failure.js';
46
46
  export * from './cli/main.js';
47
47
  export * from './cli/usage.js';
48
+ export * from './mcp/stdio.js';
48
49
  export * from './contracts/config.js';
49
50
  export * from './web-host/asset-source.js';
50
51
  export * from './web-host/packaged-assets.js';