@atolis-hq/wake 0.4.24 → 0.4.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/bootstrap/config/root-schema.d.ts +4 -4
- package/dist/src/bootstrap/config/root-schema.js +4 -6
- package/dist/src/bootstrap/initialise.js +10 -2
- package/dist/src/bootstrap/npm-update-port.js +2 -0
- package/dist/src/bootstrap/self-update-application.d.ts +6 -1
- package/dist/src/bootstrap/self-update-application.js +31 -37
- package/dist/src/bootstrap/source-update-port.js +9 -0
- package/dist/src/bootstrap/surface-cli-applications.js +10 -8
- package/dist/src/bootstrap/version.js +1 -1
- package/dist/src/orchestration/application/start-workflow.js +12 -2
- package/dist/src/surfaces/cli/infrastructure/docker-cli.d.ts +0 -1
- package/dist/src/surfaces/cli/infrastructure/docker-cli.js +13 -14
- package/dist/src/surfaces/cli/infrastructure/managed-runtime-image.d.ts +2 -0
- package/dist/src/surfaces/cli/infrastructure/managed-runtime-image.js +5 -0
- package/docker/Dockerfile +2 -85
- package/docker/Dockerfile.packaged +2 -68
- package/docker/Dockerfile.runtime +85 -0
- package/docker/Dockerfile.runtime.packaged +68 -0
- package/package.json +1 -1
|
@@ -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.
|
|
142
|
-
cancellationTimeoutMs: z.
|
|
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.
|
|
443
|
-
cancellationTimeoutMs: z.
|
|
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
|
-
|
|
94
|
-
|
|
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':
|
|
483
|
-
'docker/Dockerfile.packaged':
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
187
|
-
|
|
188
|
-
|
|
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 {
|
|
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_) =>
|
|
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.
|
|
127
|
+
export const wakeVersion = "v0.4.26";
|
|
128
128
|
/** The registry-installable version used by packaged sandbox images. */
|
|
129
129
|
export const wakePackageVersion = packageVersion(defaultRepoRoot) ?? wakeVersion;
|
|
@@ -5,6 +5,7 @@ import { WorkflowInstanceKind, WorkflowStatus } from '../contracts/vocabulary.js
|
|
|
5
5
|
import { validateChildProvenance } from '../domain/child-policy.js';
|
|
6
6
|
import { workflowEventData } from '../domain/decision-events.js';
|
|
7
7
|
import { startInstance } from '../domain/interpreter.js';
|
|
8
|
+
import { appendWithIntentRecovery } from './durable-append.js';
|
|
8
9
|
import { WorkflowDefinitionUnavailableError } from './workflow-definition-registry.js';
|
|
9
10
|
const TERMINAL_OR_BLOCKED_STATUSES = new Set([
|
|
10
11
|
WorkflowStatus.Blocked,
|
|
@@ -48,8 +49,17 @@ export class StartWorkflow {
|
|
|
48
49
|
correlationId: context.correlationId,
|
|
49
50
|
causationId: context.commandId,
|
|
50
51
|
});
|
|
51
|
-
if (decision.kind === 'append')
|
|
52
|
-
await
|
|
52
|
+
if (decision.kind === 'append') {
|
|
53
|
+
const recovered = await appendWithIntentRecovery({
|
|
54
|
+
append: async () => {
|
|
55
|
+
await this.repository.append(command.workflowInstanceId, 0, decision.events);
|
|
56
|
+
},
|
|
57
|
+
load: () => this.repository.load(command.workflowInstanceId),
|
|
58
|
+
alreadyApplied: (reloaded) => reloaded.view !== null,
|
|
59
|
+
});
|
|
60
|
+
if (recovered !== undefined)
|
|
61
|
+
return recovered.view;
|
|
62
|
+
}
|
|
53
63
|
return (await this.repository.loadRequired(command.workflowInstanceId)).view;
|
|
54
64
|
}
|
|
55
65
|
async isWorkItemOpen(workItemId) {
|
|
@@ -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
|
|
58
|
-
const
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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,5 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
2
|
+
export const managedRuntimeImage = 'wake-sandbox-runtime:managed';
|
|
3
|
+
export function managedRuntimeDockerfile(packaged) {
|
|
4
|
+
return fileURLToPath(new URL(`../../../../docker/Dockerfile.runtime${packaged ? '.packaged' : ''}`, import.meta.url));
|
|
5
|
+
}
|
package/docker/Dockerfile
CHANGED
|
@@ -1,85 +1,2 @@
|
|
|
1
|
-
#
|
|
2
|
-
FROM
|
|
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
|
-
#
|
|
2
|
-
FROM
|
|
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'"]
|