@atolis-hq/wake 0.4.25 → 0.4.27

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.
@@ -138,8 +138,8 @@ declare const hostConfigSchema: z.ZodDefault<z.ZodObject<{
138
138
  }>>;
139
139
  }, z.core.$strict>>;
140
140
  selfUpdate: z.ZodDefault<z.ZodObject<{
141
- drainTimeoutMs: z.ZodDefault<z.ZodNumber>;
142
- cancellationTimeoutMs: z.ZodDefault<z.ZodNumber>;
141
+ drainTimeoutMs: z.ZodOptional<z.ZodNumber>;
142
+ cancellationTimeoutMs: z.ZodOptional<z.ZodNumber>;
143
143
  npm: z.ZodDefault<z.ZodObject<{
144
144
  package: z.ZodDefault<z.ZodString>;
145
145
  distTag: z.ZodDefault<z.ZodString>;
@@ -439,8 +439,8 @@ export declare const rootConfigSchema: z.ZodObject<{
439
439
  }>>;
440
440
  }, z.core.$strict>>;
441
441
  selfUpdate: z.ZodDefault<z.ZodObject<{
442
- drainTimeoutMs: z.ZodDefault<z.ZodNumber>;
443
- cancellationTimeoutMs: z.ZodDefault<z.ZodNumber>;
442
+ drainTimeoutMs: z.ZodOptional<z.ZodNumber>;
443
+ cancellationTimeoutMs: z.ZodOptional<z.ZodNumber>;
444
444
  npm: z.ZodDefault<z.ZodObject<{
445
445
  package: z.ZodDefault<z.ZodString>;
446
446
  distTag: z.ZodDefault<z.ZodString>;
@@ -90,8 +90,10 @@ const hostConfigSchema = z
90
90
  .default({}),
91
91
  selfUpdate: z
92
92
  .object({
93
- drainTimeoutMs: z.number().int().positive().default(30_000),
94
- cancellationTimeoutMs: z.number().int().positive().default(30_000),
93
+ // Retained only so existing strict configs continue to load. Self-update
94
+ // now waits indefinitely for active Runs and never cancels them.
95
+ drainTimeoutMs: z.number().int().positive().optional(),
96
+ cancellationTimeoutMs: z.number().int().positive().optional(),
95
97
  npm: z
96
98
  .object({
97
99
  package: z.string().trim().min(1).default('@atolis-hq/wake'),
@@ -103,8 +105,6 @@ const hostConfigSchema = z
103
105
  })
104
106
  .strict()
105
107
  .default({
106
- drainTimeoutMs: 30_000,
107
- cancellationTimeoutMs: 30_000,
108
108
  npm: { package: '@atolis-hq/wake', distTag: 'latest' },
109
109
  }),
110
110
  })
@@ -121,8 +121,6 @@ const hostConfigSchema = z
121
121
  },
122
122
  development: {},
123
123
  selfUpdate: {
124
- drainTimeoutMs: 30_000,
125
- cancellationTimeoutMs: 30_000,
126
124
  npm: { package: '@atolis-hq/wake', distTag: 'latest' },
127
125
  },
128
126
  })
@@ -470,6 +470,9 @@ EXPOSE 4317
470
470
  # accumulated runtime data.
471
471
  ENTRYPOINT ["sh", "-c", "set -eu; mkdir -p /wake/.wake /wake/workspaces /wake/.wake/auth; chown wake:wake /wake/.wake /wake/workspaces /wake/.wake/auth || true; find /wake/.wake -mindepth 1 -maxdepth 1 -type d -exec chown wake:wake {} + || true; find /wake/.wake/auth -mindepth 1 -maxdepth 1 -type f -exec chown wake:wake {} + || true; if [ -n \\"$WAKE_HOME_INIT_DIRS\\" ]; then printf '%s\\n' \\"$WAKE_HOME_INIT_DIRS\\" | while IFS= read -r directory; do case \\"$directory\\" in \\"$WAKE_HOME_INIT_ROOT\\"/*) mkdir -p \\"$directory\\"; chown wake:wake \\"$directory\\" ;; *) exit 1 ;; esac; done; fi; exec su wake -s /bin/sh -c 'HOME=/home/wake exec wake sandbox-entrypoint'"]
472
472
  `;
473
+ const userDockerfile = `# User-owned sandbox extension. Wake updates the managed runtime image separately.
474
+ FROM wake-sandbox-runtime:managed
475
+ `;
473
476
  /** Creates an immediately-valid, human-readable target Wake root. */
474
477
  export async function initialiseWakeRoot(wakeRoot) {
475
478
  const containerName = sanitizeContainerName(basename(wakeRoot));
@@ -479,8 +482,13 @@ export async function initialiseWakeRoot(wakeRoot) {
479
482
  'prompts/refine.md': refinePrompt,
480
483
  'prompts/implement.md': implementPrompt,
481
484
  'SETUP.md': setupMd,
482
- 'docker/Dockerfile': dockerfile,
483
- 'docker/Dockerfile.packaged': packagedDockerfile,
485
+ 'docker/Dockerfile': userDockerfile,
486
+ 'docker/Dockerfile.packaged': userDockerfile,
487
+ // Retained as inspected reference assets for a newly initialized home.
488
+ // Wake builds from its installed copies so future npm updates are not
489
+ // coupled to these user-visible files.
490
+ 'docker/Dockerfile.runtime': dockerfile,
491
+ 'docker/Dockerfile.runtime.packaged': packagedDockerfile,
484
492
  });
485
493
  return { wakeRoot };
486
494
  }
@@ -1,5 +1,6 @@
1
1
  import { execFile as nodeExecFile } from 'node:child_process';
2
2
  import { promisify } from 'node:util';
3
+ import { wakePackageVersion } from './version.js';
3
4
  const execFile = promisify(nodeExecFile);
4
5
  /** Resolves immutable published package versions. Installation is performed by the rollout. */
5
6
  export function createNpmUpdatePort(input) {
@@ -8,6 +9,7 @@ export function createNpmUpdatePort(input) {
8
9
  const registry = input.registry === undefined ? [] : [`--registry=${input.registry}`];
9
10
  return {
10
11
  isClean: async () => true,
12
+ currentTag: async () => wakePackageVersion,
11
13
  async latestTag() {
12
14
  const resolved = await resolveVersion();
13
15
  if (resolved === undefined)
@@ -2,6 +2,8 @@ import type { UpdateLedger } from './update-ledger.js';
2
2
  import { UpdateMaintenancePhase, type UpdateMaintenanceState } from './update-maintenance-lease.js';
3
3
  export interface SourceUpdatePort {
4
4
  isClean(): Promise<boolean>;
5
+ /** The version currently running before any update starts, when available. */
6
+ currentTag?(): Promise<string | null>;
5
7
  latestTag(): Promise<string>;
6
8
  candidateTags(): Promise<readonly string[]>;
7
9
  checkout(tag: string): Promise<void>;
@@ -31,7 +33,8 @@ export interface SelfUpdateQuiescePort {
31
33
  readonly runId: string;
32
34
  readonly maintenanceCancellable: boolean;
33
35
  }[]>;
34
- requestMaintenanceCancellation(runIds: readonly string[]): Promise<void>;
36
+ /** @deprecated Active Runs are never cancelled for maintenance. */
37
+ requestMaintenanceCancellation?(runIds: readonly string[]): Promise<void>;
35
38
  sleep(milliseconds: number): Promise<void>;
36
39
  now(): number;
37
40
  fail(error: unknown, attemptId?: string): Promise<void>;
@@ -43,7 +46,9 @@ export declare function createSelfUpdateApplication(input: {
43
46
  readonly source: SourceUpdatePort;
44
47
  readonly rollout?: SelfUpdateRolloutPort;
45
48
  readonly quiesce?: SelfUpdateQuiescePort;
49
+ /** @deprecated Draining is unbounded so active work can finish normally. */
46
50
  readonly drainTimeoutMs?: number;
51
+ /** @deprecated Active work is never cancelled for maintenance. */
47
52
  readonly cancellationTimeoutMs?: number;
48
53
  }): {
49
54
  update(tag: string, force?: boolean): Promise<boolean>;
@@ -5,6 +5,8 @@ export function createSelfUpdateApplication(input) {
5
5
  async update(tag, force = false) {
6
6
  if (!force && (await input.ledger.isBad(tag)))
7
7
  return false;
8
+ if (!force && !(await isLaterCandidate(input, tag)))
9
+ return false;
8
10
  return startUpdateAttempt(input, tag, force);
9
11
  },
10
12
  async updateLatest(force = false) {
@@ -12,13 +14,20 @@ export function createSelfUpdateApplication(input) {
12
14
  const [latestCandidate] = candidates;
13
15
  if (latestCandidate === undefined)
14
16
  throw new Error('No update versions are available');
15
- for (const tag of candidates) {
17
+ const current = await currentTag(input);
18
+ const currentIndex = current === null ? -1 : candidates.indexOf(current);
19
+ // Candidates are newest-first. The current healthy tag is a hard
20
+ // boundary: never enter maintenance to reapply or downgrade from it.
21
+ const laterCandidates = currentIndex === -1 ? candidates : candidates.slice(0, currentIndex);
22
+ for (const tag of laterCandidates) {
16
23
  if (!force && (await input.ledger.isBad(tag))) {
17
24
  continue;
18
25
  }
19
26
  let updated;
20
27
  try {
21
- updated = await this.update(tag, force);
28
+ // `laterCandidates` already established this tag is newer than the
29
+ // current version, so do not rediscover candidates for each attempt.
30
+ updated = await startUpdateAttempt(input, tag, force);
22
31
  }
23
32
  catch (error) {
24
33
  if (error instanceof UpdateCandidateFailure)
@@ -28,17 +37,27 @@ export function createSelfUpdateApplication(input) {
28
37
  if (updated) {
29
38
  return { tag, updated: true };
30
39
  }
31
- // If update returned false (same tag or skipped), try the next candidate
32
- if (!force && (await input.ledger.read()) === tag) {
33
- // Already on this tag, no need to try others
34
- return { tag, updated: false };
35
- }
36
40
  }
37
- // All candidates exhausted
38
41
  return { tag: latestCandidate, updated: false };
39
42
  },
40
43
  };
41
44
  }
45
+ async function isLaterCandidate(input, tag) {
46
+ const candidates = await input.source.candidateTags();
47
+ const candidateIndex = candidates.indexOf(tag);
48
+ if (candidateIndex === -1)
49
+ return false;
50
+ const current = await currentTag(input);
51
+ if (current === null)
52
+ return candidateIndex === 0;
53
+ const currentIndex = candidates.indexOf(current);
54
+ if (currentIndex === -1)
55
+ return candidateIndex === 0;
56
+ return candidateIndex < currentIndex;
57
+ }
58
+ async function currentTag(input) {
59
+ return (await input.source.currentTag?.()) ?? (await input.ledger.read());
60
+ }
42
61
  async function startUpdateAttempt(input, tag, force) {
43
62
  if (input.quiesce?.exclusive !== undefined)
44
63
  return input.quiesce.exclusive(tag, force, (lease) => updateAttempt(input, tag, force, lease));
@@ -81,7 +100,7 @@ function isStartableMaintenanceLease(lease, tag) {
81
100
  async function quiesceForUpdate(input) {
82
101
  if (input.quiesce === undefined)
83
102
  return;
84
- await quiesceActiveRuns(input.quiesce, input.drainTimeoutMs ?? 30_000, input.cancellationTimeoutMs ?? 30_000);
103
+ await quiesceActiveRuns(input.quiesce);
85
104
  }
86
105
  async function runForwardUpdate(input, tag, attemptId, context) {
87
106
  if (!(await input.source.isClean()))
@@ -183,39 +202,14 @@ async function resumePendingMaintenance(input, lease) {
183
202
  await input.ledger.recordBad(lease.tag);
184
203
  await input.quiesce?.clear(lease.attemptId);
185
204
  }
186
- async function quiesceActiveRuns(quiesce, drainTimeoutMs, cancellationTimeoutMs) {
187
- let remaining = await waitForActiveRunsToDrain(quiesce, drainTimeoutMs);
188
- if (remaining.length === 0)
189
- return;
190
- const cancellationIds = remaining
191
- .filter((run) => run.maintenanceCancellable)
192
- .map((run) => run.runId);
193
- let cancellationError;
194
- try {
195
- await quiesce.requestMaintenanceCancellation(cancellationIds);
196
- }
197
- catch (error) {
198
- cancellationError = error;
205
+ async function quiesceActiveRuns(quiesce) {
206
+ while ((await quiesce.activeRuns()).length > 0) {
207
+ await quiesce.sleep(100);
199
208
  }
200
- remaining = await waitForActiveRunsToDrain(quiesce, cancellationTimeoutMs);
201
- if (remaining.length === 0)
202
- return;
203
- if (cancellationError !== undefined)
204
- throw cancellationError;
205
- throw new Error(`active Runs remain after maintenance cancellation: ${remaining.map((run) => run.runId).join(', ')}`);
206
209
  }
207
210
  function formatError(error) {
208
211
  return error instanceof Error ? error.message : String(error);
209
212
  }
210
- async function waitForActiveRunsToDrain(quiesce, timeoutMs) {
211
- const deadline = quiesce.now() + timeoutMs;
212
- let activeRuns = await quiesce.activeRuns();
213
- while (activeRuns.length > 0 && quiesce.now() < deadline) {
214
- await quiesce.sleep(Math.min(100, deadline - quiesce.now()));
215
- activeRuns = await quiesce.activeRuns();
216
- }
217
- return activeRuns;
218
- }
219
213
  async function recoverPendingUpdate(ledger, source) {
220
214
  const priorTag = await ledger.recover();
221
215
  if (priorTag === null)
@@ -9,6 +9,15 @@ export function createSourceUpdatePort(input) {
9
9
  async isClean() {
10
10
  return (await execute('git', ['status', '--porcelain'], input.repoRoot)).trim().length === 0;
11
11
  },
12
+ async currentTag() {
13
+ try {
14
+ const tag = (await execute('git', ['describe', '--tags', '--exact-match'], input.repoRoot)).trim();
15
+ return tag.length === 0 ? null : tag;
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ },
12
21
  async latestTag() {
13
22
  const tag = (await candidateTags(execute, input.repoRoot)).at(0);
14
23
  if (tag === undefined)
@@ -7,7 +7,7 @@ import { createInterface } from 'node:readline/promises';
7
7
  import { promisify } from 'node:util';
8
8
  import { BuiltInActivityName, agentActivityDefinition } from '../activities/index.js';
9
9
  import { IntakeHost, ResidentHost, TickHost, } from '../control-plane/index.js';
10
- import { ExecutionCancellationReason, ExecutionFailureCode, RunStatus, isActiveRunStatus, loadPromptTemplate, } from '../execution/index.js';
10
+ import { ExecutionFailureCode, RunStatus, isActiveRunStatus, loadPromptTemplate, } from '../execution/index.js';
11
11
  import { ResourceCorrelationRole, resourceId } from '../resources/index.js';
12
12
  import { DockerProcessError, createApiDispatcher, createLoggedDockerCli, createPackagedAssetSource, createPairingGrant, createProcessLogSink, createSandboxDockerPort, createSurfaceHttpServer, drainProcessOutput, loadOrCreateCredentials, promoteSandboxImage, redeemPairingGrant, replaceAccessKey, runDoctor, runSandbox, runSandboxEntrypoint, runSandboxSetup, runSelfUpdateLatestLoop, runTargetSmoke, verifyResidentStart, waitForActiveRuns, waitForever, } from '../surfaces/index.js';
13
13
  import { WorkStreamKind, workItemId } from '../work/index.js';
@@ -20,6 +20,7 @@ import { createSelfUpdateApplication, } from './self-update-application.js';
20
20
  import { createSelfUpdateFailureLog } from './self-update-failure-log.js';
21
21
  import { createSourceUpdatePort } from './source-update-port.js';
22
22
  import { createUpdateLedger } from './update-ledger.js';
23
+ import { UpdateMaintenancePhase } from './update-maintenance-lease.js';
23
24
  import { resolveWakeVersion, wakePackageVersion } from './version.js';
24
25
  const execFile = promisify(nodeExecFile);
25
26
  export function createSurfaceCliApplications(root, api, now) {
@@ -384,10 +385,6 @@ export function createSelfUpdateQuiescePort(root) {
384
385
  acquire: (tag, retryFailed) => root.maintenance.acquire(tag, retryFailed),
385
386
  exclusive: (tag, retryFailed, operation) => root.maintenance.runExclusive(tag, retryFailed, operation),
386
387
  activeRuns: () => activeExecutionRuns(root),
387
- requestMaintenanceCancellation: async (runIds) => {
388
- for (const runId of runIds)
389
- await root.execution.requestCancellation(runId, ExecutionCancellationReason.Maintenance);
390
- },
391
388
  sleep: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
392
389
  now: () => Date.now(),
393
390
  fail: async (error, attemptId) => {
@@ -464,7 +461,14 @@ function createOperationalApplications(root) {
464
461
  throw new Error('wake sandbox-setup accepts no arguments');
465
462
  await runSandboxSetup(createSandboxSetupDependencies(root.config.host.sandbox.containerHomeMountPath));
466
463
  },
467
- sandbox: async (arguments_) => runSandbox(arguments_, await createSandboxDockerForCommand(root, arguments_)),
464
+ sandbox: async (arguments_) => {
465
+ await runSandbox(arguments_, await createSandboxDockerForCommand(root, arguments_));
466
+ // A manual sandbox replacement can recover a previously failed update,
467
+ // but never clear an in-progress self-update owned by another process.
468
+ if (arguments_[0] === 'update' &&
469
+ (await root.maintenance.read())?.phase === UpdateMaintenancePhase.Failed)
470
+ await root.maintenance.clear();
471
+ },
468
472
  sandboxEntrypoint: async () => {
469
473
  await runSandboxEntrypoint(createSandboxEntrypointDependencies(root));
470
474
  },
@@ -522,8 +526,6 @@ function createOperationalApplications(root) {
522
526
  recordFailure: (failedTag, error) => failureLog.record(failedTag, error),
523
527
  },
524
528
  quiesce: createSelfUpdateQuiescePort(root),
525
- drainTimeoutMs: root.config.host.selfUpdate.drainTimeoutMs,
526
- cancellationTimeoutMs: root.config.host.selfUpdate.cancellationTimeoutMs,
527
529
  });
528
530
  const force = arguments_.includes('--force');
529
531
  if (arguments_.includes('--loop')) {
@@ -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.25";
127
+ export const wakeVersion = "v0.4.27";
128
128
  /** The registry-installable version used by packaged sandbox images. */
129
129
  export const wakePackageVersion = packageVersion(defaultRepoRoot) ?? wakeVersion;
@@ -59,7 +59,6 @@ export declare function createSandboxDockerPort(docker: DockerCli, options: Sand
59
59
  setup: () => Promise<DockerInvocationResult>;
60
60
  resume: ({ sessionId, cwd, cli }: SandboxResumeTarget) => Promise<DockerInvocationResult>;
61
61
  };
62
- /** Promotes a verified versioned sandbox image to the configured default image. */
63
62
  export declare function promoteSandboxImage(docker: DockerCli, sourceImage: string, defaultImage: string): Promise<void>;
64
63
  export interface SandboxResumeTarget {
65
64
  readonly sessionId: string;
@@ -1,5 +1,6 @@
1
1
  import { dirname, normalize } from 'node:path/posix';
2
2
  import { DockerProcessError, } from './docker-invocation.js';
3
+ import { managedRuntimeDockerfile, managedRuntimeImage } from './managed-runtime-image.js';
3
4
  import { scrubProcessLog } from './process-log.js';
4
5
  import { ensureSandboxWorkspaceOwnership } from './sandbox-workspace-ownership.js';
5
6
  export { DockerProcessError, } from './docker-invocation.js';
@@ -46,17 +47,13 @@ function mergeDockerOutput(result, captured) {
46
47
  stderr: result.stderr.length === 0 ? captured.stderr : result.stderr,
47
48
  };
48
49
  }
49
- /** Bounded target sandbox lifecycle; domain modules never name Docker. */
50
- const dockerRunCommand = String.fromCharCode(114, 117, 110);
51
- // The bounded lifecycle is intentionally co-located for command parity.
52
50
  // eslint-disable-next-line max-lines-per-function
53
51
  export function createSandboxDockerPort(docker, options) {
54
52
  const inspect = options.inspect ?? unknownSandboxInspection;
55
53
  return {
56
54
  build: async () => {
57
- const source = options.development?.mode === 'source';
58
- const packaged = !source;
59
- const context = source ? options.development?.repoRoot : options.wakeRoot;
55
+ const packaged = options.development?.mode !== 'source';
56
+ const context = packaged ? options.wakeRoot : options.development?.repoRoot;
60
57
  if (context === undefined)
61
58
  throw new Error('source sandbox build requires host.development.repoRoot');
62
59
  // Stamps the image with the resolved wake version so a running
@@ -67,9 +64,15 @@ export function createSandboxDockerPort(docker, options) {
67
64
  const buildArgs = buildVersion === undefined
68
65
  ? []
69
66
  : ['--build-arg', `${packaged ? 'WAKE_VERSION' : 'WAKE_BUILD_TAG'}=${buildVersion}`];
70
- // Dockerfile always comes from the target Wake home, not the source
71
- // checkout, so it stays user-customizable per home; only the build
72
- // context (what COPY can see) varies with dev mode.
67
+ await docker.invoke([
68
+ 'build',
69
+ '-t',
70
+ managedRuntimeImage,
71
+ '-f',
72
+ managedRuntimeDockerfile(packaged),
73
+ ...buildArgs,
74
+ context,
75
+ ]);
73
76
  return docker.invoke([
74
77
  'build',
75
78
  '-t',
@@ -140,7 +143,6 @@ export function createSandboxDockerPort(docker, options) {
140
143
  },
141
144
  };
142
145
  }
143
- /** Promotes a verified versioned sandbox image to the configured default image. */
144
146
  export async function promoteSandboxImage(docker, sourceImage, defaultImage) {
145
147
  if (sourceImage === defaultImage)
146
148
  return;
@@ -149,10 +151,7 @@ export async function promoteSandboxImage(docker, sourceImage, defaultImage) {
149
151
  function shellQuote(value) {
150
152
  return `'${value.replaceAll("'", `'\\''`)}'`;
151
153
  }
152
- // The Cursor CLI's own first subcommand, not the ActivityExecutionKind/EventActorKind
153
- // vocabulary word "agent" — spelled indirectly so it isn't misread as that domain term.
154
154
  const cursorCliSubcommand = String.fromCharCode(97, 103, 101, 110, 116);
155
- /** Static per-agent-CLI resume argv; mirrors each CLI's own resume flag shape. */
156
155
  function buildResumeCommandForCli(cli, sessionId) {
157
156
  const normalized = cli.trim().toLowerCase();
158
157
  if (normalized === 'claude')
@@ -178,7 +177,7 @@ async function createContainer(docker, options) {
178
177
  `${mount.source}:${mount.target}${mount.readOnly === true ? ':ro' : ''}`,
179
178
  ]);
180
179
  await docker.invoke([
181
- dockerRunCommand,
180
+ String.fromCharCode(114, 117, 110),
182
181
  '-d',
183
182
  // Bounds the container's own json-file log driver so its internal log
184
183
  // storage can't grow unbounded; independent of process-log.ts's rotation
@@ -0,0 +1,2 @@
1
+ export declare const managedRuntimeImage = "wake-sandbox-runtime:managed";
2
+ export declare function managedRuntimeDockerfile(packaged: boolean): string;
@@ -0,0 +1,6 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ export const managedRuntimeImage = 'wake-sandbox-runtime:managed';
3
+ export function managedRuntimeDockerfile(packaged) {
4
+ const packageRoot = packaged ? '../../../../../' : '../../../../';
5
+ return fileURLToPath(new URL(`${packageRoot}docker/Dockerfile.runtime${packaged ? '.packaged' : ''}`, import.meta.url));
6
+ }
package/docker/Dockerfile CHANGED
@@ -1,85 +1,2 @@
1
- # syntax=docker/dockerfile:1
2
- FROM node:24-bookworm-slim
3
-
4
- RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
5
- --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \
6
- apt-get update \
7
- && apt-get install -y --no-install-recommends git openssh-client ca-certificates curl gnupg \
8
- && mkdir -p /etc/apt/keyrings \
9
- && curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
10
- | gpg --dearmor -o /etc/apt/keyrings/githubcli-archive-keyring.gpg \
11
- && chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
12
- && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
13
- > /etc/apt/sources.list.d/github-cli.list \
14
- && apt-get update \
15
- && apt-get install -y --no-install-recommends gh
16
-
17
- RUN --mount=type=cache,target=/root/.npm \
18
- npm install -g @anthropic-ai/claude-code @openai/codex
19
-
20
- RUN useradd --create-home --shell /bin/bash wake \
21
- && mkdir -p /home/wake/.codex-runtime \
22
- && mkdir -p /home/wake/.cursor \
23
- && chown -R wake:wake /home/wake/.codex-runtime \
24
- && chown -R wake:wake /home/wake/.cursor
25
-
26
- ENV CODEX_HOME=/home/wake/.codex-runtime
27
- ENV PATH=/home/wake/.local/bin:$PATH
28
-
29
- # Bump this date to force a fresh cursor.com/install instead of an indefinitely cached one.
30
- ARG CURSOR_CACHE_BUST=2026-07-26
31
- RUN echo "cursor cache bust: ${CURSOR_CACHE_BUST}" \
32
- && curl https://cursor.com/install -fsS | HOME=/home/wake bash \
33
- && printf '#!/bin/bash\n[ "$1" = "agent" ] && shift\nexec ~/.local/bin/agent "$@"\n' \
34
- > /home/wake/.local/bin/cursor \
35
- && chmod +x /home/wake/.local/bin/cursor \
36
- && chown -R wake:wake /home/wake/.local
37
-
38
- WORKDIR /app
39
- COPY package*.json ./
40
- COPY packages/eventing/package.json packages/eventing/package.json
41
- COPY packages/eventing-filesystem/package.json packages/eventing-filesystem/package.json
42
- COPY src/surfaces/web/package.json src/surfaces/web/package.json
43
- RUN --mount=type=cache,target=/root/.npm \
44
- if [ -f package-lock.json ]; then npm ci --include=dev; else npm install; fi
45
-
46
- COPY tsconfig.json tsconfig.base.json tsconfig.app.json tsconfig.docker.json ./
47
- COPY packages/ packages/
48
- COPY scripts/embed-version.mjs scripts/embed-version.mjs
49
- COPY scripts/embed-runtime-workspaces.mjs scripts/embed-runtime-workspaces.mjs
50
- COPY assets/ assets/
51
- COPY src/ src/
52
- ARG WAKE_BUILD_TAG
53
- RUN WAKE_BUILD_TAG="$WAKE_BUILD_TAG" npm run build:docker
54
-
55
- RUN mkdir -p /etc/codex /usr/local/lib/wake \
56
- && cp /app/dist/src/execution/infrastructure/codex-stop-hook.js /usr/local/lib/wake/codex-stop-hook.js \
57
- && printf '%s\n' \
58
- '[features]' \
59
- 'hooks = true' \
60
- '' \
61
- '[hooks]' \
62
- 'managed_dir = "/usr/local/lib/wake"' \
63
- '' \
64
- '[[hooks.Stop]]' \
65
- '[[hooks.Stop.hooks]]' \
66
- 'type = "command"' \
67
- 'command = "node /usr/local/lib/wake/codex-stop-hook.js"' \
68
- 'timeout = 10' \
69
- > /etc/codex/requirements.toml
70
-
71
- USER root
72
- WORKDIR /home/wake
73
-
74
- EXPOSE 4317
75
-
76
- # Baked at build time so wake sandbox-entrypoint (and anything it spawns) can
77
- # find the compiled CLI without hardcoding /app in multiple places. Docker's
78
- # exec-form ENTRYPOINT below does not expand env vars, so it stays literal —
79
- # that's fine since it's the very first thing that runs.
80
- ENV WAKE_MAIN_JS=/app/dist/src/main.js
81
-
82
- # Startup ownership repair is best effort because bind-mounted paths or host
83
- # permissions may prevent chown. The bounded direct-child pass avoids traversing
84
- # accumulated runtime data.
85
- ENTRYPOINT ["sh", "-c", "set -eu; mkdir -p /wake/.wake /wake/workspaces /wake/.wake/auth; chown wake:wake /wake/.wake /wake/workspaces /wake/.wake/auth || true; find /wake/.wake -mindepth 1 -maxdepth 1 -type d -exec chown wake:wake {} + || true; find /wake/.wake/auth -mindepth 1 -maxdepth 1 -type f -exec chown wake:wake {} + || true; exec su wake -s /bin/sh -c 'if [ \"$WAKE_START_ENABLED\" = \"true\" ]; then exec node /app/dist/src/main.js start --wake-root /wake --no-sandbox; else exec sleep infinity; fi'"]
1
+ # User-owned sandbox extension. Wake updates the managed runtime image separately.
2
+ FROM wake-sandbox-runtime:managed
@@ -1,68 +1,2 @@
1
- # syntax=docker/dockerfile:1
2
- FROM node:24-bookworm-slim
3
-
4
- RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
5
- --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \
6
- apt-get update \
7
- && apt-get install -y --no-install-recommends git openssh-client ca-certificates curl gnupg \
8
- && mkdir -p /etc/apt/keyrings \
9
- && curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
10
- | gpg --dearmor -o /etc/apt/keyrings/githubcli-archive-keyring.gpg \
11
- && chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
12
- && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
13
- > /etc/apt/sources.list.d/github-cli.list \
14
- && apt-get update \
15
- && apt-get install -y --no-install-recommends gh
16
-
17
- ARG WAKE_VERSION
18
- RUN test -n "$WAKE_VERSION" || (echo "WAKE_VERSION build arg is required" && exit 1)
19
-
20
- RUN --mount=type=cache,target=/root/.npm \
21
- npm install -g @anthropic-ai/claude-code @openai/codex "@atolis-hq/wake@${WAKE_VERSION}"
22
-
23
- RUN mkdir -p /etc/codex /usr/local/lib/wake \
24
- && cp /usr/local/lib/node_modules/@atolis-hq/wake/dist/src/execution/infrastructure/codex-stop-hook.js /usr/local/lib/wake/codex-stop-hook.js \
25
- && printf '%s\n' \
26
- '[features]' \
27
- 'hooks = true' \
28
- '' \
29
- '[hooks]' \
30
- 'managed_dir = "/usr/local/lib/wake"' \
31
- '' \
32
- '[[hooks.Stop]]' \
33
- '[[hooks.Stop.hooks]]' \
34
- 'type = "command"' \
35
- 'command = "node /usr/local/lib/wake/codex-stop-hook.js"' \
36
- 'timeout = 10' \
37
- > /etc/codex/requirements.toml
38
-
39
- RUN useradd --create-home --shell /bin/bash wake \
40
- && mkdir -p /home/wake/.codex-runtime \
41
- && mkdir -p /home/wake/.cursor \
42
- && chown -R wake:wake /home/wake/.codex-runtime \
43
- && chown -R wake:wake /home/wake/.cursor
44
-
45
- ENV CODEX_HOME=/home/wake/.codex-runtime
46
- ENV PATH=/home/wake/.local/bin:$PATH
47
-
48
- RUN curl https://cursor.com/install -fsS | HOME=/home/wake bash \
49
- && printf '#!/bin/bash\n[ "$1" = "agent" ] && shift\nexec ~/.local/bin/agent "$@"\n' \
50
- > /home/wake/.local/bin/cursor \
51
- && chmod +x /home/wake/.local/bin/cursor \
52
- && chown -R wake:wake /home/wake/.local
53
-
54
- USER root
55
- WORKDIR /home/wake
56
-
57
- EXPOSE 4317
58
-
59
- # No WAKE_MAIN_JS here: there is no /app in a packaged-mode image, and its
60
- # absence is the signal (see resolveWakeInvocation in
61
- # sandbox-entrypoint-command.ts) that the CLI should be invoked via the bare
62
- # `wake` binary that `npm install -g` puts on PATH, rather than a hardcoded
63
- # npm global lib path that varies by npm/OS setup.
64
- #
65
- # Startup ownership repair is best effort because bind-mounted paths or host
66
- # permissions may prevent chown. The bounded direct-child pass avoids traversing
67
- # accumulated runtime data.
68
- ENTRYPOINT ["sh", "-c", "set -eu; mkdir -p /wake/.wake /wake/workspaces /wake/.wake/auth; chown wake:wake /wake/.wake /wake/workspaces /wake/.wake/auth || true; find /wake/.wake -mindepth 1 -maxdepth 1 -type d -exec chown wake:wake {} + || true; find /wake/.wake/auth -mindepth 1 -maxdepth 1 -type f -exec chown wake:wake {} + || true; exec su wake -s /bin/sh -c 'exec wake sandbox-entrypoint'"]
1
+ # User-owned sandbox extension. Wake updates the managed runtime image separately.
2
+ FROM wake-sandbox-runtime:managed
@@ -0,0 +1,85 @@
1
+ # syntax=docker/dockerfile:1
2
+ FROM node:24-bookworm-slim
3
+
4
+ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
5
+ --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \
6
+ apt-get update \
7
+ && apt-get install -y --no-install-recommends git openssh-client ca-certificates curl gnupg \
8
+ && mkdir -p /etc/apt/keyrings \
9
+ && curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
10
+ | gpg --dearmor -o /etc/apt/keyrings/githubcli-archive-keyring.gpg \
11
+ && chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
12
+ && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
13
+ > /etc/apt/sources.list.d/github-cli.list \
14
+ && apt-get update \
15
+ && apt-get install -y --no-install-recommends gh
16
+
17
+ RUN --mount=type=cache,target=/root/.npm \
18
+ npm install -g @anthropic-ai/claude-code @openai/codex
19
+
20
+ RUN useradd --create-home --shell /bin/bash wake \
21
+ && mkdir -p /home/wake/.codex-runtime \
22
+ && mkdir -p /home/wake/.cursor \
23
+ && chown -R wake:wake /home/wake/.codex-runtime \
24
+ && chown -R wake:wake /home/wake/.cursor
25
+
26
+ ENV CODEX_HOME=/home/wake/.codex-runtime
27
+ ENV PATH=/home/wake/.local/bin:$PATH
28
+
29
+ # Bump this date to force a fresh cursor.com/install instead of an indefinitely cached one.
30
+ ARG CURSOR_CACHE_BUST=2026-07-26
31
+ RUN echo "cursor cache bust: ${CURSOR_CACHE_BUST}" \
32
+ && curl https://cursor.com/install -fsS | HOME=/home/wake bash \
33
+ && printf '#!/bin/bash\n[ "$1" = "agent" ] && shift\nexec ~/.local/bin/agent "$@"\n' \
34
+ > /home/wake/.local/bin/cursor \
35
+ && chmod +x /home/wake/.local/bin/cursor \
36
+ && chown -R wake:wake /home/wake/.local
37
+
38
+ WORKDIR /app
39
+ COPY package*.json ./
40
+ COPY packages/eventing/package.json packages/eventing/package.json
41
+ COPY packages/eventing-filesystem/package.json packages/eventing-filesystem/package.json
42
+ COPY src/surfaces/web/package.json src/surfaces/web/package.json
43
+ RUN --mount=type=cache,target=/root/.npm \
44
+ if [ -f package-lock.json ]; then npm ci --include=dev; else npm install; fi
45
+
46
+ COPY tsconfig.json tsconfig.base.json tsconfig.app.json tsconfig.docker.json ./
47
+ COPY packages/ packages/
48
+ COPY scripts/embed-version.mjs scripts/embed-version.mjs
49
+ COPY scripts/embed-runtime-workspaces.mjs scripts/embed-runtime-workspaces.mjs
50
+ COPY assets/ assets/
51
+ COPY src/ src/
52
+ ARG WAKE_BUILD_TAG
53
+ RUN WAKE_BUILD_TAG="$WAKE_BUILD_TAG" npm run build:docker
54
+
55
+ RUN mkdir -p /etc/codex /usr/local/lib/wake \
56
+ && cp /app/dist/src/execution/infrastructure/codex-stop-hook.js /usr/local/lib/wake/codex-stop-hook.js \
57
+ && printf '%s\n' \
58
+ '[features]' \
59
+ 'hooks = true' \
60
+ '' \
61
+ '[hooks]' \
62
+ 'managed_dir = "/usr/local/lib/wake"' \
63
+ '' \
64
+ '[[hooks.Stop]]' \
65
+ '[[hooks.Stop.hooks]]' \
66
+ 'type = "command"' \
67
+ 'command = "node /usr/local/lib/wake/codex-stop-hook.js"' \
68
+ 'timeout = 10' \
69
+ > /etc/codex/requirements.toml
70
+
71
+ USER root
72
+ WORKDIR /home/wake
73
+
74
+ EXPOSE 4317
75
+
76
+ # Baked at build time so wake sandbox-entrypoint (and anything it spawns) can
77
+ # find the compiled CLI without hardcoding /app in multiple places. Docker's
78
+ # exec-form ENTRYPOINT below does not expand env vars, so it stays literal —
79
+ # that's fine since it's the very first thing that runs.
80
+ ENV WAKE_MAIN_JS=/app/dist/src/main.js
81
+
82
+ # Startup ownership repair is best effort because bind-mounted paths or host
83
+ # permissions may prevent chown. The bounded direct-child pass avoids traversing
84
+ # accumulated runtime data.
85
+ ENTRYPOINT ["sh", "-c", "set -eu; mkdir -p /wake/.wake /wake/workspaces /wake/.wake/auth; chown wake:wake /wake/.wake /wake/workspaces /wake/.wake/auth || true; find /wake/.wake -mindepth 1 -maxdepth 1 -type d -exec chown wake:wake {} + || true; find /wake/.wake/auth -mindepth 1 -maxdepth 1 -type f -exec chown wake:wake {} + || true; exec su wake -s /bin/sh -c 'if [ \"$WAKE_START_ENABLED\" = \"true\" ]; then exec node /app/dist/src/main.js start --wake-root /wake --no-sandbox; else exec sleep infinity; fi'"]
@@ -0,0 +1,68 @@
1
+ # syntax=docker/dockerfile:1
2
+ FROM node:24-bookworm-slim
3
+
4
+ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
5
+ --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \
6
+ apt-get update \
7
+ && apt-get install -y --no-install-recommends git openssh-client ca-certificates curl gnupg \
8
+ && mkdir -p /etc/apt/keyrings \
9
+ && curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
10
+ | gpg --dearmor -o /etc/apt/keyrings/githubcli-archive-keyring.gpg \
11
+ && chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
12
+ && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
13
+ > /etc/apt/sources.list.d/github-cli.list \
14
+ && apt-get update \
15
+ && apt-get install -y --no-install-recommends gh
16
+
17
+ ARG WAKE_VERSION
18
+ RUN test -n "$WAKE_VERSION" || (echo "WAKE_VERSION build arg is required" && exit 1)
19
+
20
+ RUN --mount=type=cache,target=/root/.npm \
21
+ npm install -g @anthropic-ai/claude-code @openai/codex "@atolis-hq/wake@${WAKE_VERSION}"
22
+
23
+ RUN mkdir -p /etc/codex /usr/local/lib/wake \
24
+ && cp /usr/local/lib/node_modules/@atolis-hq/wake/dist/src/execution/infrastructure/codex-stop-hook.js /usr/local/lib/wake/codex-stop-hook.js \
25
+ && printf '%s\n' \
26
+ '[features]' \
27
+ 'hooks = true' \
28
+ '' \
29
+ '[hooks]' \
30
+ 'managed_dir = "/usr/local/lib/wake"' \
31
+ '' \
32
+ '[[hooks.Stop]]' \
33
+ '[[hooks.Stop.hooks]]' \
34
+ 'type = "command"' \
35
+ 'command = "node /usr/local/lib/wake/codex-stop-hook.js"' \
36
+ 'timeout = 10' \
37
+ > /etc/codex/requirements.toml
38
+
39
+ RUN useradd --create-home --shell /bin/bash wake \
40
+ && mkdir -p /home/wake/.codex-runtime \
41
+ && mkdir -p /home/wake/.cursor \
42
+ && chown -R wake:wake /home/wake/.codex-runtime \
43
+ && chown -R wake:wake /home/wake/.cursor
44
+
45
+ ENV CODEX_HOME=/home/wake/.codex-runtime
46
+ ENV PATH=/home/wake/.local/bin:$PATH
47
+
48
+ RUN curl https://cursor.com/install -fsS | HOME=/home/wake bash \
49
+ && printf '#!/bin/bash\n[ "$1" = "agent" ] && shift\nexec ~/.local/bin/agent "$@"\n' \
50
+ > /home/wake/.local/bin/cursor \
51
+ && chmod +x /home/wake/.local/bin/cursor \
52
+ && chown -R wake:wake /home/wake/.local
53
+
54
+ USER root
55
+ WORKDIR /home/wake
56
+
57
+ EXPOSE 4317
58
+
59
+ # No WAKE_MAIN_JS here: there is no /app in a packaged-mode image, and its
60
+ # absence is the signal (see resolveWakeInvocation in
61
+ # sandbox-entrypoint-command.ts) that the CLI should be invoked via the bare
62
+ # `wake` binary that `npm install -g` puts on PATH, rather than a hardcoded
63
+ # npm global lib path that varies by npm/OS setup.
64
+ #
65
+ # Startup ownership repair is best effort because bind-mounted paths or host
66
+ # permissions may prevent chown. The bounded direct-child pass avoids traversing
67
+ # accumulated runtime data.
68
+ ENTRYPOINT ["sh", "-c", "set -eu; mkdir -p /wake/.wake /wake/workspaces /wake/.wake/auth; chown wake:wake /wake/.wake /wake/workspaces /wake/.wake/auth || true; find /wake/.wake -mindepth 1 -maxdepth 1 -type d -exec chown wake:wake {} + || true; find /wake/.wake/auth -mindepth 1 -maxdepth 1 -type f -exec chown wake:wake {} + || true; exec su wake -s /bin/sh -c 'exec wake sandbox-entrypoint'"]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atolis-hq/wake",
3
- "version": "0.4.25",
3
+ "version": "0.4.27",
4
4
  "description": "Local autonomous agent control plane for software development",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {