@atolis-hq/wake 0.4.48 → 0.4.50

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 (76) 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/composition-root.d.ts +2 -0
  37. package/dist/src/bootstrap/composition-root.js +19 -0
  38. package/dist/src/bootstrap/config/root-schema.d.ts +6 -0
  39. package/dist/src/bootstrap/config/root-schema.js +5 -0
  40. package/dist/src/bootstrap/paths.d.ts +1 -0
  41. package/dist/src/bootstrap/paths.js +1 -0
  42. package/dist/src/bootstrap/projection-runtime.js +2 -0
  43. package/dist/src/bootstrap/runner-registry.js +22 -4
  44. package/dist/src/bootstrap/surface-api-work-applications.js +39 -1
  45. package/dist/src/bootstrap/surface-cli-applications.js +12 -1
  46. package/dist/src/bootstrap/version.js +1 -1
  47. package/dist/src/execution/application/execution-activity.d.ts +11 -0
  48. package/dist/src/execution/application/execution-activity.js +26 -10
  49. package/dist/src/execution/application/execution-service.js +1 -1
  50. package/dist/src/execution/application/execution-validation.js +17 -3
  51. package/dist/src/execution/contracts/runner.d.ts +10 -0
  52. package/dist/src/execution/contracts/workspace.d.ts +11 -1
  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/execution/infrastructure/workspace/git-workspace.d.ts +10 -5
  61. package/dist/src/execution/infrastructure/workspace/git-workspace.js +270 -38
  62. package/dist/src/surfaces/api/contracts/work.d.ts +10 -0
  63. package/dist/src/surfaces/api/routes/applications.d.ts +5 -0
  64. package/dist/src/surfaces/api/routes/read.js +34 -3
  65. package/dist/src/surfaces/api/routes/work.d.ts +1 -1
  66. package/dist/src/surfaces/api/routes/work.js +1 -0
  67. package/dist/src/surfaces/cli/main.d.ts +8 -0
  68. package/dist/src/surfaces/cli/main.js +31 -0
  69. package/dist/src/surfaces/cli/usage.js +1 -0
  70. package/dist/src/surfaces/index.d.ts +1 -0
  71. package/dist/src/surfaces/index.js +1 -0
  72. package/dist/src/surfaces/mcp/stdio.d.ts +3 -0
  73. package/dist/src/surfaces/mcp/stdio.js +5 -0
  74. package/dist/src/surfaces/web-assets/assets/{index-CuAZYm7r.js → index-CuIiUKuh.js} +2 -2
  75. package/dist/src/surfaces/web-assets/index.html +1 -1
  76. package/package.json +3 -1
@@ -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.48";
127
+ export const wakeVersion = "v0.4.50";
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) {
@@ -7,12 +7,26 @@ export async function acquireWorkspace(runId, mode, workItemId, resources, provi
7
7
  if (provider === undefined)
8
8
  throw new Error('Workspace provider is required');
9
9
  const repositoryResource = resources.find((resource) => resource.kind === BuiltInResourceKind.Repository) ??
10
- resources.find((resource) => resource.kind === BuiltInResourceKind.Issue ||
11
- resource.kind === BuiltInResourceKind.PullRequest);
10
+ resources.find((resource) => resource.kind === BuiltInResourceKind.PullRequest) ??
11
+ resources.find((resource) => resource.kind === BuiltInResourceKind.Issue);
12
12
  if (repositoryResource === undefined)
13
13
  throw new Error('Repository Resource is required');
14
14
  signal.throwIfAborted();
15
- return provider.acquire({ runId, signal, mode, workItemId, repositoryResource });
15
+ const revision = repositoryResource.revision;
16
+ const checkoutRevision = repositoryResource.kind === BuiltInResourceKind.Repository ||
17
+ (repositoryResource.kind === BuiltInResourceKind.PullRequest &&
18
+ revision !== undefined &&
19
+ /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/i.test(revision))
20
+ ? revision
21
+ : undefined;
22
+ return provider.acquire({
23
+ runId,
24
+ signal,
25
+ mode,
26
+ workItemId,
27
+ repositoryResource,
28
+ ...(checkoutRevision === undefined ? {} : { checkoutRevision }),
29
+ });
16
30
  }
17
31
  export function validateResourceRequirements(requirements, resources) {
18
32
  for (const requirement of requirements) {
@@ -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
  }
@@ -9,6 +9,8 @@ export interface WorkspaceRequest {
9
9
  readonly mode: typeof WorkspaceMode.ReadOnly | typeof WorkspaceMode.Branch;
10
10
  readonly workItemId: WorkItemId;
11
11
  readonly repositoryResource: ResourceView;
12
+ /** Git checkout target, independent of the resource observation revision. */
13
+ readonly checkoutRevision?: string;
12
14
  }
13
15
  export interface WorkspaceLease {
14
16
  readonly workspaceId: string;
@@ -16,7 +18,7 @@ export interface WorkspaceLease {
16
18
  /** Branch actually checked out for this lease, when the provider can attest it. */
17
19
  readonly branch?: string | undefined;
18
20
  readonly mode: WorkspaceRequest['mode'];
19
- /** Ends this Run's use of the workspace; branch-workspace disposal is WorkItem-scoped recovery. */
21
+ /** Ends this Run's use of the workspace; workspace disposal is WorkItem-scoped recovery. */
20
22
  release(): Promise<void>;
21
23
  }
22
24
  export interface WorkspaceProvider {
@@ -46,3 +48,11 @@ export interface WorkspaceRecoveryFailure {
46
48
  readonly path: string;
47
49
  readonly message: string;
48
50
  }
51
+ /** Process-owned exclusion shared by acquisition and recovery. */
52
+ export interface WorkspaceLockProvider {
53
+ tryAcquire(path: string): Promise<WorkspaceLock>;
54
+ }
55
+ export interface WorkspaceLock {
56
+ readonly acquired: boolean;
57
+ release(): Promise<void>;
58
+ }
@@ -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);
@@ -1,11 +1,10 @@
1
- import type { WorkItemId } from '../../../work/index.js';
2
1
  import type { RunView } from '../../contracts/views.js';
3
- import type { WorkspaceProvider, WorkspaceRecovery, WorkspaceRecoveryOptions, WorkspaceRecoveryResult, WorkspaceRequest } from '../../contracts/workspace.js';
2
+ import type { WorkspaceLockProvider, WorkspaceProvider, WorkspaceRecovery, WorkspaceRecoveryOptions, WorkspaceRecoveryResult, WorkspaceRequest } from '../../contracts/workspace.js';
4
3
  import { type WorkspacePrepareHook } from './prepare-workspace.js';
5
4
  export interface RepositoryCloneResolver {
6
5
  cloneLocator(resourceId: string, signal: AbortSignal): Promise<string>;
7
6
  }
8
- export type GitRunner = (arguments_: readonly string[], signal: AbortSignal) => Promise<void>;
7
+ export type GitRunner = (arguments_: readonly string[], signal: AbortSignal) => Promise<string | void>;
9
8
  export interface WorkspaceRecoveryFileSystem {
10
9
  remove(path: string): Promise<void>;
11
10
  canonicalize(path: string): Promise<string>;
@@ -13,17 +12,23 @@ export interface WorkspaceRecoveryFileSystem {
13
12
  export declare class GitWorkspaceProvider implements WorkspaceProvider, WorkspaceRecovery {
14
13
  private readonly root;
15
14
  private readonly resolver;
15
+ private readonly locks;
16
16
  private readonly git;
17
17
  private readonly prepareHook?;
18
18
  private readonly markerRoot;
19
19
  private readonly recoveryFileSystem;
20
- constructor(root: string, resolver: RepositoryCloneResolver, git?: GitRunner, recoveryFileSystem?: WorkspaceRecoveryFileSystem, prepareHook?: WorkspacePrepareHook);
20
+ private readonly pendingReleases;
21
+ constructor(root: string, resolver: RepositoryCloneResolver, locks: WorkspaceLockProvider, git?: GitRunner, recoveryFileSystem?: WorkspaceRecoveryFileSystem, prepareHook?: WorkspacePrepareHook);
21
22
  acquire(request: WorkspaceRequest): Promise<{
22
23
  release: () => Promise<void>;
23
- branch?: WorkItemId;
24
+ branch?: string;
24
25
  workspaceId: string;
25
26
  path: string;
26
27
  mode: "read-only" | "branch";
27
28
  }>;
29
+ private releaseWorkspaceLock;
30
+ private prepareCheckout;
31
+ private reuseCheckout;
32
+ private validateLegacyCheckout;
28
33
  recover(runs: readonly RunView[], options?: WorkspaceRecoveryOptions): Promise<WorkspaceRecoveryResult>;
29
34
  }