@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
@@ -0,0 +1,51 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
3
+ import { dirname, join } from 'node:path';
4
+ /**
5
+ * Private filesystem byte store. Its locations are opaque implementation
6
+ * details; durable artifact facts carry the location, digest, and length.
7
+ */
8
+ export class FileArtifactStore {
9
+ root;
10
+ constructor(root) {
11
+ this.root = root;
12
+ }
13
+ async write(input) {
14
+ assertSegment(input.workItemId, 'Work item ID');
15
+ assertSegment(input.revisionId, 'Artifact revision ID');
16
+ const digest = createHash('sha256').update(input.bytes).digest('hex');
17
+ const location = join(input.workItemId, `${input.revisionId}.${digest}`);
18
+ const destination = this.absolute(location);
19
+ await mkdir(dirname(destination), { recursive: true });
20
+ const temporary = `${destination}.${randomUUID()}.pending`;
21
+ try {
22
+ await writeFile(temporary, input.bytes);
23
+ await rename(temporary, destination);
24
+ }
25
+ finally {
26
+ await rm(temporary, { force: true });
27
+ }
28
+ return {
29
+ location,
30
+ byteLength: input.bytes.byteLength,
31
+ digest,
32
+ };
33
+ }
34
+ async read(location) {
35
+ return readFile(this.absolute(location));
36
+ }
37
+ absolute(location) {
38
+ const segments = location.split('/');
39
+ for (const segment of segments)
40
+ assertSegment(segment, 'Artifact store location segment');
41
+ return join(this.root, ...segments);
42
+ }
43
+ }
44
+ function assertSegment(value, label) {
45
+ if (value.length === 0 ||
46
+ value === '.' ||
47
+ value === '..' ||
48
+ value.includes('/') ||
49
+ value.includes('\\'))
50
+ throw new Error(`${label} must be a single filesystem segment`);
51
+ }
@@ -0,0 +1,18 @@
1
+ import type { CommandContext } from '@atolis-hq/eventing';
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import type { WorkItemId } from '../../work/index.js';
4
+ import type { ArtifactService } from '../application/artifact-service.js';
5
+ /** A capability token is resolved into this server-side scope by Execution. */
6
+ export interface ArtifactMcpSession {
7
+ readonly workItemId: WorkItemId;
8
+ readonly producer: string;
9
+ readonly runId: string;
10
+ readonly activationId: string;
11
+ readonly readableProducers: ReadonlySet<string>;
12
+ readonly nextRevisionId: () => string;
13
+ readonly commandContext: () => CommandContext;
14
+ readonly acceptedActivation: (activationId: string) => Promise<boolean>;
15
+ /** Revalidates the run-bound credential for every tool request. */
16
+ readonly assertActive: () => Promise<void>;
17
+ }
18
+ export declare function createArtifactMcpServer(artifacts: ArtifactService, session: ArtifactMcpSession): McpServer;
@@ -0,0 +1,135 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import { artifactPath } from '../contracts/paths.js';
4
+ // Tool registration remains adjacent so the advertised MCP surface is auditable in one place.
5
+ // eslint-disable-next-line max-lines-per-function
6
+ export function createArtifactMcpServer(artifacts, session) {
7
+ const server = new McpServer({ name: 'wake', version: '0.1.0' });
8
+ server.registerTool('wake.artifacts.read', {
9
+ description: 'Read a visible artifact revision as UTF-8 text or base64 bytes.',
10
+ inputSchema: {
11
+ revisionId: z.string().min(1),
12
+ encoding: z.enum(['utf8', 'base64']).default('utf8'),
13
+ offset: z.number().int().nonnegative().default(0),
14
+ length: z.number().int().positive().optional(),
15
+ },
16
+ }, async ({ revisionId, encoding, offset, length }) => {
17
+ await session.assertActive();
18
+ const artifact = await visibleArtifact(artifacts, session, revisionId);
19
+ if (artifact.location === undefined)
20
+ throw new Error('Artifact revision has no content');
21
+ const bytes = (await artifacts.read(artifact.location)).slice(offset, length === undefined ? undefined : offset + length);
22
+ return {
23
+ content: [
24
+ {
25
+ type: 'text',
26
+ text: encoding === 'base64'
27
+ ? Buffer.from(bytes).toString('base64')
28
+ : new TextDecoder('utf-8', { fatal: true }).decode(bytes),
29
+ },
30
+ ],
31
+ };
32
+ });
33
+ server.registerTool('wake.artifacts.list', { description: 'List the current artifact manifest visible to this activation.' }, async () => {
34
+ await session.assertActive();
35
+ return {
36
+ content: [
37
+ {
38
+ type: 'text',
39
+ text: JSON.stringify((await visibleArtifacts(artifacts, session)).map(({ location: _location, ...artifact }) => artifact)),
40
+ },
41
+ ],
42
+ };
43
+ });
44
+ server.registerTool('wake.artifacts.patch', {
45
+ description: 'Atomically replace one exact UTF-8 text fragment in an artifact owned by this producer.',
46
+ inputSchema: {
47
+ revisionId: z.string().min(1),
48
+ find: z.string().min(1),
49
+ replace: z.string(),
50
+ },
51
+ }, async ({ revisionId, find, replace }) => {
52
+ await session.assertActive();
53
+ const artifact = await visibleArtifact(artifacts, session, revisionId);
54
+ if (artifact.producer !== session.producer)
55
+ throw new Error('Only the owning producer may patch an artifact');
56
+ if (artifact.location === undefined)
57
+ throw new Error('Artifact revision has no content');
58
+ const source = new TextDecoder('utf-8', { fatal: true }).decode(await artifacts.read(artifact.location));
59
+ const first = source.indexOf(find);
60
+ if (first < 0 || source.indexOf(find, first + find.length) >= 0)
61
+ throw new Error('Patch target must occur exactly once');
62
+ const next = `${source.slice(0, first)}${replace}${source.slice(first + find.length)}`;
63
+ const nextRevisionId = session.nextRevisionId();
64
+ await artifacts.stageRevision({
65
+ revisionId: nextRevisionId,
66
+ path: artifactPath(artifact.path),
67
+ bytes: new TextEncoder().encode(next),
68
+ ...(artifact.mediaType === undefined ? {} : { mediaType: artifact.mediaType }),
69
+ }, writeScope(session), session.commandContext());
70
+ return { content: [{ type: 'text', text: JSON.stringify({ revisionId: nextRevisionId }) }] };
71
+ });
72
+ server.registerTool('wake.artifacts.write', {
73
+ description: 'Stage UTF-8 content at a path owned by this activation producer.',
74
+ inputSchema: {
75
+ path: z.string().min(1),
76
+ content: z.string(),
77
+ encoding: z.enum(['utf8', 'base64']).default('utf8'),
78
+ mediaType: z.string().min(1).optional(),
79
+ },
80
+ }, async ({ path, content, encoding, mediaType }) => {
81
+ await session.assertActive();
82
+ const revisionId = session.nextRevisionId();
83
+ await artifacts.stageRevision({
84
+ revisionId,
85
+ path: artifactPath(path),
86
+ bytes: decodeContent(content, encoding),
87
+ ...(mediaType === undefined ? {} : { mediaType }),
88
+ }, writeScope(session), session.commandContext());
89
+ return { content: [{ type: 'text', text: JSON.stringify({ revisionId }) }] };
90
+ });
91
+ server.registerTool('wake.artifacts.delete', {
92
+ description: 'Stage deletion of an artifact path owned by this activation producer.',
93
+ inputSchema: { path: z.string().min(1) },
94
+ }, async ({ path }) => {
95
+ await session.assertActive();
96
+ const revisionId = session.nextRevisionId();
97
+ await artifacts.stageTombstone({
98
+ revisionId,
99
+ path: artifactPath(path),
100
+ }, writeScope(session), session.commandContext());
101
+ return { content: [{ type: 'text', text: JSON.stringify({ revisionId }) }] };
102
+ });
103
+ return server;
104
+ }
105
+ async function visibleArtifact(artifacts, session, revisionId) {
106
+ const artifact = (await visibleArtifacts(artifacts, session)).find((candidate) => candidate.revisionId === revisionId && session.readableProducers.has(candidate.producer));
107
+ if (artifact === undefined)
108
+ throw new Error('Artifact revision is not visible to this activation');
109
+ return artifact;
110
+ }
111
+ function visibleArtifacts(artifacts, session) {
112
+ return artifacts.visibleTo({
113
+ workItemId: session.workItemId,
114
+ producer: session.producer,
115
+ runId: session.runId,
116
+ activationId: session.activationId,
117
+ readableProducers: session.readableProducers,
118
+ }, session.acceptedActivation);
119
+ }
120
+ function writeScope(session) {
121
+ return {
122
+ workItemId: session.workItemId,
123
+ producer: session.producer,
124
+ runId: session.runId,
125
+ activationId: session.activationId,
126
+ };
127
+ }
128
+ function decodeContent(content, encoding) {
129
+ if (encoding === 'utf8')
130
+ return new TextEncoder().encode(content);
131
+ const bytes = Buffer.from(content, 'base64');
132
+ if (bytes.toString('base64').replace(/=+$/, '') !== content.replace(/=+$/, ''))
133
+ throw new Error('Artifact base64 content is invalid');
134
+ return bytes;
135
+ }
@@ -0,0 +1,25 @@
1
+ import type { WorkItemId } from '../../work/index.js';
2
+ export interface ArtifactMcpSessionDescriptor {
3
+ readonly workItemId: WorkItemId;
4
+ readonly producer: string;
5
+ readonly runId: string;
6
+ readonly activationId: string;
7
+ readonly readableProducers: readonly string[];
8
+ readonly expiresAt: string;
9
+ }
10
+ /**
11
+ * Ephemeral run-scoped descriptors are intentionally outside a Git workspace.
12
+ * The unpredictable filename is the capability presented by the configured MCP
13
+ * child process; it does not contain artifacts themselves.
14
+ */
15
+ export declare class FileArtifactMcpSessionStore {
16
+ private readonly root;
17
+ private readonly now;
18
+ constructor(root: string, now?: () => string);
19
+ issue(input: ArtifactMcpSessionDescriptor): Promise<{
20
+ readonly path: string;
21
+ readonly token: string;
22
+ }>;
23
+ read(path: string, token: string): Promise<ArtifactMcpSessionDescriptor>;
24
+ revoke(path: string): Promise<void>;
25
+ }
@@ -0,0 +1,61 @@
1
+ import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
2
+ import { mkdir, readFile, unlink, writeFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ /**
5
+ * Ephemeral run-scoped descriptors are intentionally outside a Git workspace.
6
+ * The unpredictable filename is the capability presented by the configured MCP
7
+ * child process; it does not contain artifacts themselves.
8
+ */
9
+ export class FileArtifactMcpSessionStore {
10
+ root;
11
+ now;
12
+ constructor(root, now = () => new Date().toISOString()) {
13
+ this.root = root;
14
+ this.now = now;
15
+ }
16
+ async issue(input) {
17
+ const token = randomBytes(32).toString('base64url');
18
+ const id = createHash('sha256').update(token).digest('hex');
19
+ const directory = join(this.root, 'sessions');
20
+ await mkdir(directory, { recursive: true, mode: 0o700 });
21
+ const path = join(directory, `${id}.json`);
22
+ await writeFile(path, JSON.stringify({
23
+ ...input,
24
+ tokenHash: digest(token),
25
+ }), { encoding: 'utf8', mode: 0o600 });
26
+ return { path, token };
27
+ }
28
+ async read(path, token) {
29
+ const descriptor = JSON.parse(await readFile(path, 'utf8'));
30
+ if (typeof descriptor.tokenHash !== 'string' ||
31
+ !sameDigest(descriptor.tokenHash, digest(token)) ||
32
+ typeof descriptor.workItemId !== 'string' ||
33
+ typeof descriptor.producer !== 'string' ||
34
+ typeof descriptor.runId !== 'string' ||
35
+ typeof descriptor.activationId !== 'string' ||
36
+ !Array.isArray(descriptor.readableProducers) ||
37
+ typeof descriptor.expiresAt !== 'string' ||
38
+ !Number.isFinite(Date.parse(descriptor.expiresAt)) ||
39
+ Date.parse(descriptor.expiresAt) <= Date.parse(this.now()))
40
+ throw new Error('Invalid artifact MCP session');
41
+ const { tokenHash: _tokenHash, ...session } = descriptor;
42
+ return session;
43
+ }
44
+ async revoke(path) {
45
+ try {
46
+ await unlink(path);
47
+ }
48
+ catch (error) {
49
+ if (error.code !== 'ENOENT')
50
+ throw error;
51
+ }
52
+ }
53
+ }
54
+ function digest(value) {
55
+ return createHash('sha256').update(value).digest('hex');
56
+ }
57
+ function sameDigest(left, right) {
58
+ const leftBytes = Buffer.from(left, 'hex');
59
+ const rightBytes = Buffer.from(right, 'hex');
60
+ return leftBytes.byteLength === rightBytes.byteLength && timingSafeEqual(leftBytes, rightBytes);
61
+ }
@@ -0,0 +1,23 @@
1
+ import type { FileArtifactMcpSessionStore } from '../artifacts/index.js';
2
+ import { type ArtifactService } from '../artifacts/index.js';
3
+ import type { ExecutionDependencies } from '../execution/index.js';
4
+ import { type OrchestrationService } from '../orchestration/index.js';
5
+ /** Bootstrap binds Artifact's generic policy to verified Execution and Orchestration facts. */
6
+ export declare function createArtifactMcpProvisioner(input: {
7
+ readonly artifacts: ArtifactService;
8
+ readonly orchestration: OrchestrationService;
9
+ readonly sessions: FileArtifactMcpSessionStore;
10
+ readonly wakeRoot: string;
11
+ readonly now: () => string;
12
+ readonly nextId: () => string;
13
+ }): NonNullable<ExecutionDependencies['mcp']>;
14
+ /** Creates the transport-neutral Wake MCP server for the run-bound CLI command. */
15
+ export declare function createArtifactMcpServerForSession(root: ArtifactMcpRoot, sessions: FileArtifactMcpSessionStore, path: string, token: string, options: {
16
+ readonly now: () => string;
17
+ readonly nextId: () => string;
18
+ }): Promise<import("@modelcontextprotocol/sdk/server/mcp").McpServer>;
19
+ interface ArtifactMcpRoot {
20
+ readonly artifacts: ArtifactService;
21
+ readonly orchestration: OrchestrationService;
22
+ }
23
+ export {};
@@ -0,0 +1,119 @@
1
+ import { EventActorKind, correlationId } from '@atolis-hq/eventing';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { createArtifactMcpServer, } from '../artifacts/index.js';
4
+ import { TransitionTargetKind, } from '../orchestration/index.js';
5
+ const sessionLifetimeMs = 2 * 60 * 60 * 1000;
6
+ /** Bootstrap binds Artifact's generic policy to verified Execution and Orchestration facts. */
7
+ export function createArtifactMcpProvisioner(input) {
8
+ return async ({ runId, activation, context }) => {
9
+ const workflow = await input.orchestration.get(context.workflowInstanceId);
10
+ const producer = producerFor(context.workflowInstanceId, activation.stage, activation.activity);
11
+ const readableProducers = new Set([
12
+ producer,
13
+ ...(workflow === null
14
+ ? []
15
+ : await upstreamProducers(input.orchestration, workflow, context.workflowInstanceId, activation.stage)),
16
+ ]);
17
+ const expiresAt = new Date(Date.parse(input.now()) + sessionLifetimeMs).toISOString();
18
+ const descriptor = {
19
+ workItemId: context.workItemId,
20
+ producer,
21
+ runId,
22
+ activationId: activation.activationId,
23
+ readableProducers: [...readableProducers],
24
+ expiresAt,
25
+ };
26
+ const issued = await input.sessions.issue(descriptor);
27
+ try {
28
+ const manifest = await input.artifacts.visibleTo({ ...descriptor, readableProducers }, acceptedActivation(input.orchestration, context.workItemId));
29
+ return {
30
+ servers: [
31
+ {
32
+ name: 'wake',
33
+ command: 'wake',
34
+ args: ['mcp', 'serve', '--session', issued.path, '--wake-root', input.wakeRoot],
35
+ env: { WAKE_MCP_TOKEN: issued.token },
36
+ },
37
+ ],
38
+ prompt: artifactPrompt(manifest),
39
+ release: () => input.sessions.revoke(issued.path),
40
+ };
41
+ }
42
+ catch (error) {
43
+ await input.sessions.revoke(issued.path);
44
+ throw error;
45
+ }
46
+ };
47
+ }
48
+ /** Creates the transport-neutral Wake MCP server for the run-bound CLI command. */
49
+ export async function createArtifactMcpServerForSession(root, sessions, path, token, options) {
50
+ const descriptor = await sessions.read(path, token);
51
+ const accepted = acceptedActivation(root.orchestration, descriptor.workItemId);
52
+ return createArtifactMcpServer(root.artifacts, {
53
+ ...descriptor,
54
+ readableProducers: new Set(descriptor.readableProducers),
55
+ nextRevisionId: options.nextId,
56
+ commandContext: () => commandContext(options.now(), descriptor),
57
+ acceptedActivation: accepted,
58
+ assertActive: async () => {
59
+ await sessions.read(path, token);
60
+ },
61
+ });
62
+ }
63
+ function commandContext(occurredAt, descriptor) {
64
+ return {
65
+ commandId: `artifact-mcp:${descriptor.runId}:${randomUUID()}`,
66
+ correlationId: correlationId(`artifact:${descriptor.runId}`),
67
+ actor: { kind: EventActorKind.System, id: 'wake-mcp' },
68
+ occurredAt,
69
+ };
70
+ }
71
+ function acceptedActivation(orchestration, workItemId) {
72
+ let accepted;
73
+ return async (activationId) => {
74
+ accepted ??= orchestration
75
+ .listForWorkItem(workItemId)
76
+ .then((workflows) => new Set(workflows.flatMap((workflow) => workflow.acceptedOutcomes)));
77
+ return (await accepted).has(activationId);
78
+ };
79
+ }
80
+ async function upstreamProducers(orchestration, workflow, workflowInstanceId, stage) {
81
+ if (stage === undefined)
82
+ return [];
83
+ const definition = await orchestration.definitionFor(workflow);
84
+ return [...upstreamStages(definition, stage)].map((candidate) => producerFor(workflowInstanceId, candidate));
85
+ }
86
+ function upstreamStages(definition, stage) {
87
+ const predecessors = new Map();
88
+ for (const [source, configured] of Object.entries(definition.stages)) {
89
+ for (const route of Object.values(configured.on)) {
90
+ for (const target of [route.target, route.reentryTarget]) {
91
+ if (target.kind !== TransitionTargetKind.Stage)
92
+ continue;
93
+ const entries = predecessors.get(target.stage) ?? new Set();
94
+ entries.add(source);
95
+ predecessors.set(target.stage, entries);
96
+ }
97
+ }
98
+ }
99
+ const found = new Set();
100
+ const pending = [...(predecessors.get(stage) ?? [])];
101
+ while (pending.length > 0) {
102
+ const candidate = pending.pop();
103
+ if (candidate === stage || found.has(candidate))
104
+ continue;
105
+ found.add(candidate);
106
+ pending.push(...(predecessors.get(candidate) ?? []));
107
+ }
108
+ return found;
109
+ }
110
+ function producerFor(workflowInstanceId, stage, activity) {
111
+ return `${workflowInstanceId}/${stage ?? `activation-${activity ?? ''}`}`;
112
+ }
113
+ function artifactPrompt(manifest) {
114
+ return [
115
+ 'Wake artifacts are available through the trusted Wake MCP tools.',
116
+ 'The manifest contains metadata only; read content explicitly when useful. Write output through Wake tools, not Git.',
117
+ JSON.stringify(manifest.map(({ location: _location, ...artifact }) => artifact)),
118
+ ].join('\n');
119
+ }
@@ -1,5 +1,6 @@
1
1
  import { type CheckpointStore, type EventJournal, type ProcessorRunSerialiser, type ProcessorStateStore, type ProjectionStore } from '@atolis-hq/eventing';
2
2
  import { createPullRequestService, type ActivityRegistry } from '../activities/index.js';
3
+ import { createArtifactService } from '../artifacts/index.js';
3
4
  import { createControlPlaneService, createRunnerControlService, type ActivationScheduler, type ActivationSchedulerSubscriber, type IntakePipeline, type RunnerPipeline, type ScheduleCheckpointStore } from '../control-plane/index.js';
4
5
  import { createConversationService } from '../conversations/index.js';
5
6
  import { RecoveryService, TranscriptStore, createExecutionService, type FakeScenarioResolver, type Runner } from '../execution/index.js';
@@ -48,6 +49,7 @@ export interface CompositionRoot {
48
49
  readonly processorState: ProcessorStateStore;
49
50
  readonly activities: ActivityRegistry;
50
51
  readonly work: ReturnType<typeof createWorkService>;
52
+ readonly artifacts: ReturnType<typeof createArtifactService>;
51
53
  readonly conversations: ReturnType<typeof createConversationService>;
52
54
  readonly resources: ReturnType<typeof createResourceService>;
53
55
  readonly pullRequests: ReturnType<typeof createPullRequestService>;
@@ -1,4 +1,6 @@
1
+ import { acquireFileLock } from '@atolis-hq/eventing-filesystem';
1
2
  import { createPullRequestService } from '../activities/index.js';
3
+ import { FileArtifactMcpSessionStore, FileArtifactStore, createArtifactService, } from '../artifacts/index.js';
2
4
  import { DispatchPolicy, createActivationScheduler, createActivationSchedulerSubscriber, createControlPlaneService, createDurableRunnerIneligibility, createRunnerControlService, } from '../control-plane/index.js';
3
5
  import { createConversationService } from '../conversations/index.js';
4
6
  import { ExecutionCancellationReason, ExternalExecutionState, GitWorkspaceProvider, RecoveryService, TranscriptStore, createExecutionService, createRunnerMemoryProfileDecorator, } from '../execution/index.js';
@@ -9,6 +11,7 @@ import { createResourceLookup, createResourceService, resourceId, } from '../res
9
11
  import { createWorkService } from '../work/index.js';
10
12
  import { createFileActivationSchedulerSerialiser } from './activation-scheduler-serialiser.js';
11
13
  import { createBuiltInActivityRegistry } from './activity-registry.js';
14
+ import { createArtifactMcpProvisioner } from './artifact-mcp.js';
12
15
  import { loadConfig } from './config/load-config.js';
13
16
  import { EventProcessorRuntime } from './event-processor-runtime.js';
14
17
  import { loadFakeScenarios } from './fake-scenarios.js';
@@ -37,6 +40,8 @@ export async function createCompositionRoot(wakeRoot, options = {}) {
37
40
  const { journal, projections, checkpoints, processorState, subscriptionRunSerialiser } = composePersistence(paths, clock, options);
38
41
  const processorRuntime = new EventProcessorRuntime(journal, checkpoints, subscriptionRunSerialiser, clock);
39
42
  const work = createWorkService(journal);
43
+ const artifacts = createArtifactService(journal, new FileArtifactStore(paths.artifactsRoot), config.artifacts);
44
+ const artifactMcpSessions = new FileArtifactMcpSessionStore(paths.artifactsRoot, () => clock.now().toISOString());
40
45
  const conversations = createConversationService(journal);
41
46
  const lookup = createResourceLookup({ journal, projections });
42
47
  const resources = createResourceService(journal, lookup);
@@ -59,6 +64,11 @@ export async function createCompositionRoot(wakeRoot, options = {}) {
59
64
  throw new Error('Workspace resource ' + id + ' does not identify a GitHub repository');
60
65
  return 'https://github.com/' + match[1] + '.git';
61
66
  },
67
+ }, {
68
+ tryAcquire: (path) => acquireFileLock(path, {
69
+ staleAfterMs: 0,
70
+ staleRequiresDeadProcess: true,
71
+ }),
62
72
  }, undefined, undefined, config.execution.workspaceHooks?.prepare);
63
73
  const transcriptStore = config.transcripts.enabled
64
74
  ? (options.transcriptStore ?? new TranscriptStore(paths.transcriptsRoot))
@@ -79,6 +89,14 @@ export async function createCompositionRoot(wakeRoot, options = {}) {
79
89
  clock,
80
90
  ids,
81
91
  runners: createRunnerRegistry(config.execution, fakeScenarios, decorateRunner),
92
+ mcp: createArtifactMcpProvisioner({
93
+ artifacts,
94
+ orchestration,
95
+ sessions: artifactMcpSessions,
96
+ wakeRoot: paths.wakeRoot,
97
+ now: () => clock.now().toISOString(),
98
+ nextId: () => ids.next('artifact'),
99
+ }),
82
100
  reportRunnerQuota: createRunnerQuotaReporter(journal, clock, ids),
83
101
  ...(transcriptStore !== undefined
84
102
  ? {
@@ -174,6 +192,7 @@ export async function createCompositionRoot(wakeRoot, options = {}) {
174
192
  processorState,
175
193
  activities,
176
194
  work,
195
+ artifacts,
177
196
  conversations,
178
197
  resources,
179
198
  pullRequests,
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { type ActivitiesConfig } from '../../activities/index.js';
3
+ import { type ArtifactsConfig } from '../../artifacts/index.js';
3
4
  import { type ControlPlaneConfig } from '../../control-plane/index.js';
4
5
  import { type ExecutionConfig } from '../../execution/index.js';
5
6
  import { type IntegrationsConfig } from '../../integrations/index.js';
@@ -262,6 +263,10 @@ export declare const rootConfigSchema: z.ZodObject<{
262
263
  enabled: z.ZodDefault<z.ZodBoolean>;
263
264
  retentionMs: z.ZodDefault<z.ZodNumber>;
264
265
  }, z.core.$strict>>;
266
+ artifacts: z.ZodDefault<z.ZodObject<{
267
+ maxWriteBytes: z.ZodDefault<z.ZodNumber>;
268
+ maxWorkItemBytes: z.ZodDefault<z.ZodNumber>;
269
+ }, z.core.$strict>>;
265
270
  execution: z.ZodObject<{
266
271
  agentRunners: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodDiscriminatedUnion<[z.ZodObject<{
267
272
  model: z.ZodOptional<z.ZodString>;
@@ -458,6 +463,7 @@ export interface ResolvedWakeModulesConfig {
458
463
  readonly activities: ActivitiesConfig;
459
464
  readonly orchestration: z.infer<typeof orchestrationConfigSchema>;
460
465
  readonly transcripts: z.infer<typeof transcriptsConfigSchema>;
466
+ readonly artifacts: ArtifactsConfig;
461
467
  readonly execution: ExecutionConfig;
462
468
  readonly controlPlane: ControlPlaneConfig;
463
469
  readonly integrations: IntegrationsConfig;
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { activitiesConfigSchema } from '../../activities/index.js';
3
+ import { artifactsConfigSchema } from '../../artifacts/index.js';
3
4
  import { controlPlaneConfigSchema } from '../../control-plane/index.js';
4
5
  import { executionConfigSchema } from '../../execution/index.js';
5
6
  import { integrationsConfigSchema } from '../../integrations/index.js';
@@ -140,6 +141,10 @@ export const rootConfigSchema = z
140
141
  activities: activitiesConfigSchema.default({}),
141
142
  orchestration: orchestrationConfigSchema,
142
143
  transcripts: transcriptsConfigSchema,
144
+ artifacts: artifactsConfigSchema.default({
145
+ maxWriteBytes: 25 * 1024 * 1024,
146
+ maxWorkItemBytes: 250 * 1024 * 1024,
147
+ }),
143
148
  execution: executionConfigSchema,
144
149
  controlPlane: controlPlaneConfigSchema,
145
150
  integrations: integrationsConfigSchema,
@@ -6,6 +6,7 @@ export interface WakePaths {
6
6
  readonly checkpointsRoot: string;
7
7
  readonly locksRoot: string;
8
8
  readonly transcriptsRoot: string;
9
+ readonly artifactsRoot: string;
9
10
  readonly containerHomeRoot: string;
10
11
  readonly workspacesRoot: string;
11
12
  }
@@ -9,6 +9,7 @@ export function resolveWakePaths(wakeRoot) {
9
9
  checkpointsRoot: join(dataRoot, 'checkpoints'),
10
10
  locksRoot: join(dataRoot, 'locks'),
11
11
  transcriptsRoot: join(dataRoot, 'transcripts'),
12
+ artifactsRoot: join(dataRoot, 'artifacts'),
12
13
  containerHomeRoot: join(dataRoot, 'container-home'),
13
14
  workspacesRoot: join(wakeRoot, 'workspaces'),
14
15
  };
@@ -1,6 +1,7 @@
1
1
  import { ProjectionRebuilder, createProjectionProcessor, } from '@atolis-hq/eventing';
2
2
  import { createInMemoryProcessorRunSerialiser } from '@atolis-hq/eventing/memory';
3
3
  import { activityProjectionDefinitions } from '../activities/index.js';
4
+ import { artifactProjection } from '../artifacts/index.js';
4
5
  import { controlPlaneProjectionDefinitions } from '../control-plane/index.js';
5
6
  import { conversationProjection } from '../conversations/index.js';
6
7
  import { executionProjection, runsByWorkflowInstanceProjection } from '../execution/index.js';
@@ -11,6 +12,7 @@ import { workProjection } from '../work/index.js';
11
12
  import { analyticsProjection } from './analytics-projection.js';
12
13
  import { boardProjection } from './board-projection.js';
13
14
  export const runtimeProjectionDefinitions = [
15
+ artifactProjection,
14
16
  conversationProjection,
15
17
  workProjection,
16
18
  resourceProjection,
@@ -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
  }