@atolis-hq/wake 0.1.23 → 0.2.0
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/README.md +14 -65
- package/dist/src/adapters/docker/docker-cli.js +45 -1
- package/dist/src/adapters/fs/state-store.js +6 -6
- package/dist/src/adapters/github/github-issues-work-source.js +2 -2
- package/dist/src/cli/doctor-command.js +48 -0
- package/dist/src/cli/init-command.js +8 -1
- package/dist/src/cli/sandbox-command.js +43 -13
- package/dist/src/cli/sandbox-entrypoint-command.js +88 -0
- package/dist/src/cli/sandbox-exec-logging.js +7 -0
- package/dist/src/cli/sandbox-resume.js +11 -9
- package/dist/src/cli/sandbox-setup-command.js +31 -0
- package/dist/src/cli/scaffold-assets.js +45 -126
- package/dist/src/cli/self-update-command.js +1 -1
- package/dist/src/cli/startup-preflight.js +5 -1
- package/dist/src/config/load-config.js +5 -1
- package/dist/src/domain/schema.js +1 -0
- package/dist/src/lib/paths.js +26 -23
- package/dist/src/main.js +506 -116
- package/dist/src/version.js +1 -1
- package/docker/Dockerfile +8 -2
- package/docker/Dockerfile.packaged +53 -0
- package/package.json +1 -1
- package/dist/src/cli/sandbox-logging.js +0 -30
package/README.md
CHANGED
|
@@ -63,7 +63,6 @@ conversation forward wherever the work is already happening.
|
|
|
63
63
|
- [Where the Work Happens](#where-the-work-happens)
|
|
64
64
|
- [Supported Agent CLIs](#supported-agent-clis)
|
|
65
65
|
- [Getting Started](#getting-started)
|
|
66
|
-
- [Development](#development)
|
|
67
66
|
- [Documentation](#documentation)
|
|
68
67
|
- [Issues & Feature Requests](#issues--feature-requests)
|
|
69
68
|
- [License](#license)
|
|
@@ -115,8 +114,10 @@ For more detail, see [docs/vision.md](docs/vision.md) and
|
|
|
115
114
|
- **Event-sourced and restart-safe.** The durable record is an append-only event
|
|
116
115
|
log; projections can be rebuilt, and the loop can crash and resume without
|
|
117
116
|
losing its place.
|
|
118
|
-
- **Local and inspectable.**
|
|
119
|
-
|
|
117
|
+
- **Local and inspectable.** Everything lives in a plain-file Wake home
|
|
118
|
+
directory: `config.json`, `prompts/`, and `workspaces/` at the top level for
|
|
119
|
+
what you edit or browse day-to-day, with durable/internal state (events,
|
|
120
|
+
projections, runs, logs, sandbox auth) nested under a hidden `.wake/`.
|
|
120
121
|
- **Sandbox-oriented execution.** Wake can run from a persistent Docker sandbox
|
|
121
122
|
with durable auth state and mounted Wake home data.
|
|
122
123
|
- **Runner agnostic.** Claude Code, Codex, Cursor, and fake runners sit behind
|
|
@@ -162,82 +163,30 @@ for capability differences between runners.
|
|
|
162
163
|
|
|
163
164
|
## Getting Started
|
|
164
165
|
|
|
165
|
-
Wake is distributed as the `@atolis-hq/wake` npm package. You can run the CLI
|
|
166
|
-
with `npx` or install it globally:
|
|
167
|
-
|
|
168
|
-
```sh
|
|
169
|
-
npx @atolis-hq/wake init ./wake-home
|
|
170
|
-
```
|
|
171
|
-
|
|
172
166
|
```sh
|
|
173
167
|
npm install -g @atolis-hq/wake
|
|
168
|
+
cd ~/
|
|
174
169
|
wake init ./wake-home
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
`wake init` creates a Wake home directory with `config.json`, prompt templates,
|
|
178
|
-
Docker sandbox assets, runtime directories, and shell launchers:
|
|
179
|
-
|
|
180
|
-
- `wake.sh` for bash, Git Bash, WSL, and similar shells.
|
|
181
|
-
- `wake.ps1` for PowerShell.
|
|
182
|
-
|
|
183
|
-
Use the generated launcher from the Wake home for day-to-day operation. The
|
|
184
|
-
launcher runs host setup commands locally and forwards runtime commands into the
|
|
185
|
-
sandbox with the correct Wake home mounted at `/wake`.
|
|
186
|
-
|
|
187
|
-
```sh
|
|
188
170
|
cd ./wake-home
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
./wake.sh start
|
|
171
|
+
wake sandbox build
|
|
172
|
+
wake sandbox up
|
|
173
|
+
wake sandbox setup
|
|
174
|
+
wake start
|
|
194
175
|
```
|
|
195
176
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
then rebuild with `./wake.sh sandbox build`. Wake writes the package location to
|
|
200
|
-
`config.json` as `dev.repoRoot` so sandbox rebuilds use the same bundled assets;
|
|
201
|
-
editing your generated Dockerfile or prompts is expected and future package
|
|
202
|
-
upgrades should not overwrite that Wake home.
|
|
203
|
-
|
|
204
|
-
Common commands:
|
|
205
|
-
|
|
206
|
-
```sh
|
|
207
|
-
./wake.sh ui
|
|
208
|
-
./wake.sh tick
|
|
209
|
-
./wake.sh start
|
|
210
|
-
./wake.sh stop
|
|
211
|
-
./wake.sh sandbox resume <session-id> --cwd "/wake/workspaces/<workId>"
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
For a source checkout development workflow, use:
|
|
215
|
-
|
|
216
|
-
- [docs/development.md](docs/development.md)
|
|
217
|
-
|
|
218
|
-
Recommended local practices:
|
|
219
|
-
|
|
220
|
-
- Use a separate git identity for Wake-managed agent work so automated commits
|
|
221
|
-
and human commits are easy to distinguish.
|
|
222
|
-
- Prefer the prebuilt sandbox flow when running real agent work locally.
|
|
223
|
-
- Treat the default Dockerfile as a starting point. It includes common tooling
|
|
224
|
-
such as Node, but it is expected to be edited for the repositories and agents
|
|
225
|
-
you want Wake to operate on.
|
|
226
|
-
|
|
227
|
-
## Development
|
|
228
|
-
|
|
229
|
-
Local setup, commands, sandbox operation, auth setup, UI notes, and GitHub
|
|
230
|
-
polling details are documented in [docs/development.md](docs/development.md).
|
|
177
|
+
Full setup instructions are in
|
|
178
|
+
[docs/getting-started.md](docs/getting-started.md). Run `wake --help` at
|
|
179
|
+
any time for the full command list.
|
|
231
180
|
|
|
232
181
|
## Documentation
|
|
233
182
|
|
|
183
|
+
- [docs/getting-started.md](docs/getting-started.md) — packaged-install setup, sandbox lifecycle, `wake doctor`.
|
|
234
184
|
- [docs/vision.md](docs/vision.md) — the rationale and long-term direction for Wake.
|
|
235
185
|
- [docs/architecture.md](docs/architecture.md) — module boundaries and the event-sourced core.
|
|
236
|
-
- [docs/implementation.md](docs/implementation.md) — the accepted implementation plan.
|
|
237
186
|
- [docs/workflows.md](docs/workflows.md) — how stages, prompts, and runner routes are configured.
|
|
238
187
|
- [docs/prompts.md](docs/prompts.md) — how prompt templates map to workflow stages.
|
|
239
188
|
- [docs/configuration.md](docs/configuration.md) — `config.json` options and the operator correlation escape hatch.
|
|
240
|
-
- [docs/development.md](docs/development.md) —
|
|
189
|
+
- [docs/development.md](docs/development.md) — source-checkout dev setup (`wake-dev`), npm scripts, formatting, self-update, GitHub polling.
|
|
241
190
|
- [docs/runner-comparison.md](docs/runner-comparison.md) — capability differences between supported runners.
|
|
242
191
|
|
|
243
192
|
## Issues & Feature Requests
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline';
|
|
2
|
+
import { scrubSecrets } from '../../cli/sandbox-exec-logging.js';
|
|
1
3
|
function buildStopArgs(containerName, timeoutSeconds) {
|
|
2
4
|
return [
|
|
3
5
|
'stop',
|
|
@@ -50,7 +52,19 @@ function buildRunArgs(input) {
|
|
|
50
52
|
export function createDockerCli(deps) {
|
|
51
53
|
return {
|
|
52
54
|
async build(input) {
|
|
53
|
-
|
|
55
|
+
const buildArgFlags = Object.entries(input.buildArgs ?? {}).flatMap(([key, value]) => [
|
|
56
|
+
'--build-arg',
|
|
57
|
+
`${key}=${value}`,
|
|
58
|
+
]);
|
|
59
|
+
await deps.run([
|
|
60
|
+
'build',
|
|
61
|
+
'-t',
|
|
62
|
+
input.image,
|
|
63
|
+
'-f',
|
|
64
|
+
input.dockerfile,
|
|
65
|
+
...buildArgFlags,
|
|
66
|
+
input.contextDir,
|
|
67
|
+
]);
|
|
54
68
|
},
|
|
55
69
|
async up(input) {
|
|
56
70
|
const imageExists = await deps.inspectImage(input.image);
|
|
@@ -90,6 +104,36 @@ export function createDockerCli(deps) {
|
|
|
90
104
|
? ['exec', interactive ? '-it' : '-i', containerName, ...command]
|
|
91
105
|
: ['exec', '-it', containerName, 'bash']);
|
|
92
106
|
},
|
|
107
|
+
/**
|
|
108
|
+
* Runs `docker exec -i <containerName> <command>` with piped (not
|
|
109
|
+
* inherited) stdio, line-buffers stdout/stderr, scrubs secrets from each
|
|
110
|
+
* line, and forwards it to the caller's handlers in real time. Used by
|
|
111
|
+
* `sandbox exec` so live output is observed and redacted on the host
|
|
112
|
+
* instead of being wrapped by a mounted in-container script.
|
|
113
|
+
*/
|
|
114
|
+
async execCaptured(containerName, command, handlers) {
|
|
115
|
+
if (deps.spawnExec === undefined) {
|
|
116
|
+
throw new Error('docker cli adapter was not configured with spawnExec');
|
|
117
|
+
}
|
|
118
|
+
const args = ['exec', '-i', containerName, ...command];
|
|
119
|
+
const child = deps.spawnExec(args);
|
|
120
|
+
const stdoutReader = createInterface({ input: child.stdout });
|
|
121
|
+
stdoutReader.on('line', (line) => handlers.onStdout(scrubSecrets(line)));
|
|
122
|
+
const stderrReader = createInterface({ input: child.stderr });
|
|
123
|
+
stderrReader.on('line', (line) => handlers.onStderr(scrubSecrets(line)));
|
|
124
|
+
await new Promise((resolveExec, reject) => {
|
|
125
|
+
child.on('error', reject);
|
|
126
|
+
child.on('close', (exitCode) => {
|
|
127
|
+
stdoutReader.close();
|
|
128
|
+
stderrReader.close();
|
|
129
|
+
if (exitCode === 0) {
|
|
130
|
+
resolveExec();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
reject(new Error(`docker ${args.join(' ')} failed with exit code ${exitCode ?? 1}`));
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
},
|
|
93
137
|
async logs(containerName, tailLines) {
|
|
94
138
|
await deps.run(['logs', '--tail', String(tailLines), containerName]);
|
|
95
139
|
},
|
|
@@ -58,7 +58,7 @@ function shouldArchiveIssueState(item, options) {
|
|
|
58
58
|
return Number.isFinite(ageMs) && ageMs > options.archiveFreshnessDays * 24 * 60 * 60 * 1000;
|
|
59
59
|
}
|
|
60
60
|
export async function listRunRecords(wakeRoot) {
|
|
61
|
-
const runsRoot = join(wakeRoot, 'runs');
|
|
61
|
+
const runsRoot = join(createWakePaths(wakeRoot).dataRoot, 'runs');
|
|
62
62
|
const recordsById = new Map();
|
|
63
63
|
try {
|
|
64
64
|
const files = (await readdir(runsRoot)).filter((file) => file.endsWith('.json')).sort();
|
|
@@ -93,7 +93,7 @@ export async function listRunRecords(wakeRoot) {
|
|
|
93
93
|
return [...recordsById.values()].sort((left, right) => left.startedAt.localeCompare(right.startedAt));
|
|
94
94
|
}
|
|
95
95
|
async function listRunRecordsForDate(wakeRoot, date) {
|
|
96
|
-
const runsRoot = join(wakeRoot, 'runs');
|
|
96
|
+
const runsRoot = join(createWakePaths(wakeRoot).dataRoot, 'runs');
|
|
97
97
|
const recordsById = new Map();
|
|
98
98
|
const bucketFiles = (await readdir(join(runsRoot, 'by-date', date)).catch(() => []))
|
|
99
99
|
.filter((file) => file.endsWith('.json'))
|
|
@@ -118,7 +118,7 @@ async function listRunRecordsForDate(wakeRoot, date) {
|
|
|
118
118
|
return [...recordsById.values()].sort((left, right) => left.startedAt.localeCompare(right.startedAt));
|
|
119
119
|
}
|
|
120
120
|
async function listRecentRunRecords(wakeRoot, limit) {
|
|
121
|
-
const runsRoot = join(wakeRoot, 'runs');
|
|
121
|
+
const runsRoot = join(createWakePaths(wakeRoot).dataRoot, 'runs');
|
|
122
122
|
const recordsById = new Map();
|
|
123
123
|
const dateDirs = (await readdir(join(runsRoot, 'by-date')).catch(() => [])).sort().reverse();
|
|
124
124
|
for (const dateDir of dateDirs) {
|
|
@@ -227,7 +227,7 @@ export function createStateStore({ wakeRoot }) {
|
|
|
227
227
|
}
|
|
228
228
|
},
|
|
229
229
|
async listIssueStates(options = {}) {
|
|
230
|
-
const stateRoot = join(
|
|
230
|
+
const stateRoot = join(paths.dataRoot, 'state');
|
|
231
231
|
try {
|
|
232
232
|
const items = [];
|
|
233
233
|
const includeArchived = options.includeArchived ?? false;
|
|
@@ -284,7 +284,7 @@ export function createStateStore({ wakeRoot }) {
|
|
|
284
284
|
}
|
|
285
285
|
},
|
|
286
286
|
async listEventEnvelopes() {
|
|
287
|
-
const eventsRoot = join(
|
|
287
|
+
const eventsRoot = join(paths.dataRoot, 'events');
|
|
288
288
|
try {
|
|
289
289
|
const files = (await readdir(eventsRoot)).sort();
|
|
290
290
|
const envelopes = [];
|
|
@@ -299,7 +299,7 @@ export function createStateStore({ wakeRoot }) {
|
|
|
299
299
|
},
|
|
300
300
|
async listRecentEventEnvelopes(filter = {}) {
|
|
301
301
|
const limit = filter.limit ?? 200;
|
|
302
|
-
const eventsRoot = join(
|
|
302
|
+
const eventsRoot = join(paths.dataRoot, 'events');
|
|
303
303
|
const files = (await readdir(eventsRoot).catch(() => []))
|
|
304
304
|
.filter((file) => file.endsWith('.jsonl'))
|
|
305
305
|
.sort()
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import { resolve } from 'node:path';
|
|
3
2
|
import { defaultAgentIdentity } from '../../domain/schema.js';
|
|
4
3
|
import { buildResourceUri } from '../../domain/resource-uri.js';
|
|
5
4
|
import { wakeStageLabelPrefix } from '../../domain/stages.js';
|
|
6
5
|
import { wakeWorkflowLabelPrefix } from '../../domain/workflows.js';
|
|
7
6
|
import { createEventEnvelope, createUnkeyedEventEnvelope } from '../../lib/event-log.js';
|
|
7
|
+
import { createWakePaths } from '../../lib/paths.js';
|
|
8
8
|
import { wakeVersion } from '../../version.js';
|
|
9
9
|
import { buildResumeCommandForCli } from '../runner/runner-cli-adapter.js';
|
|
10
10
|
const wakeStatusLabelPrefix = 'wake:status.';
|
|
@@ -173,7 +173,7 @@ function formatControlPlaneLink(url) {
|
|
|
173
173
|
}
|
|
174
174
|
export async function readControlPlaneUiUrl(wakeRoot) {
|
|
175
175
|
try {
|
|
176
|
-
const raw = await readFile(
|
|
176
|
+
const raw = await readFile(createWakePaths(wakeRoot).controlPlaneUiUrlFile, 'utf8');
|
|
177
177
|
return formatControlPlaneLink(raw.trim()) ?? undefined;
|
|
178
178
|
}
|
|
179
179
|
catch {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export async function runDoctorCommand(config, deps) {
|
|
2
|
+
const failures = [...(await deps.collectPreflightFailures(config))];
|
|
3
|
+
const notices = [];
|
|
4
|
+
if (config.sources.github.enabled) {
|
|
5
|
+
try {
|
|
6
|
+
await deps.resolveGitHubToken();
|
|
7
|
+
}
|
|
8
|
+
catch (error) {
|
|
9
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
10
|
+
failures.push(`GitHub token could not be resolved: ${message}`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
if (await deps.hasDockerfile(deps.wakeRoot)) {
|
|
14
|
+
const reachable = await deps.dockerReachable();
|
|
15
|
+
if (!reachable) {
|
|
16
|
+
failures.push('Docker daemon is not reachable');
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
const imageExists = await deps.inspectImage(deps.image);
|
|
20
|
+
if (!imageExists) {
|
|
21
|
+
failures.push(`sandbox image "${deps.image}" not found — run \`wake sandbox build\``);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
if (await deps.containerRunning()) {
|
|
27
|
+
const sandboxVersion = await deps.execVersionInContainer();
|
|
28
|
+
if (sandboxVersion !== '' && sandboxVersion !== deps.installedVersion) {
|
|
29
|
+
notices.push(`sandbox is running version ${sandboxVersion}, installed CLI is ${deps.installedVersion} — run \`wake sandbox build && wake sandbox update\` to sync`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
35
|
+
notices.push(`could not check sandbox version: ${message}`);
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const driftedFiles = await deps.diffPromptsAndDockerfile();
|
|
39
|
+
for (const file of driftedFiles) {
|
|
40
|
+
notices.push(`${file} differs from the currently-shipped default (not auto-overwritten)`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
45
|
+
notices.push(`could not check prompt/Dockerfile drift: ${message}`);
|
|
46
|
+
}
|
|
47
|
+
return { failures, notices };
|
|
48
|
+
}
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { resolve } from 'node:path';
|
|
2
2
|
import { assertEmptyDirectory, scaffoldWakeHome } from './scaffold-assets.js';
|
|
3
3
|
export async function runInitCommand(input) {
|
|
4
|
-
const
|
|
4
|
+
const positionalArgs = input.args.filter((arg) => arg !== '--dev' && arg !== '--packaged');
|
|
5
|
+
const wakeRoot = resolve(input.cwd, positionalArgs[0] ?? '.');
|
|
6
|
+
const devModeOverride = input.args.includes('--dev')
|
|
7
|
+
? 'source'
|
|
8
|
+
: input.args.includes('--packaged')
|
|
9
|
+
? 'packaged'
|
|
10
|
+
: undefined;
|
|
5
11
|
await assertEmptyDirectory(wakeRoot);
|
|
6
12
|
await scaffoldWakeHome({
|
|
7
13
|
wakeRoot,
|
|
8
14
|
repoRoot: input.repoRoot,
|
|
15
|
+
...(devModeOverride === undefined ? {} : { devModeOverride }),
|
|
9
16
|
});
|
|
10
17
|
return { wakeRoot };
|
|
11
18
|
}
|
|
@@ -1,10 +1,25 @@
|
|
|
1
|
-
import { mkdir } from 'node:fs/promises';
|
|
1
|
+
import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { posix, resolve } from 'node:path';
|
|
3
3
|
import { createRunnerCliAdapter } from '../adapters/runner/runner-cli-adapter.js';
|
|
4
4
|
import { runSandboxResumeCommand } from './sandbox-resume.js';
|
|
5
5
|
import { runSelfUpdateCommand, runSelfUpdateLoop } from './self-update-command.js';
|
|
6
6
|
import { runStopCommand } from './stop-command.js';
|
|
7
|
-
import {
|
|
7
|
+
import { wakeVersion } from '../version.js';
|
|
8
|
+
async function ensureDockerfile(input) {
|
|
9
|
+
const targetPath = resolve(input.wakeRoot, 'docker', 'Dockerfile');
|
|
10
|
+
try {
|
|
11
|
+
await access(targetPath);
|
|
12
|
+
return; // already present — user-owned, never overwritten
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
// fall through to write it
|
|
16
|
+
}
|
|
17
|
+
const mode = input.devMode ?? 'packaged';
|
|
18
|
+
const templatePath = resolve(input.packagedTemplatesRoot, mode === 'source' ? 'Dockerfile' : 'Dockerfile.packaged');
|
|
19
|
+
const content = await readFile(templatePath, 'utf8');
|
|
20
|
+
await mkdir(resolve(input.wakeRoot, 'docker'), { recursive: true });
|
|
21
|
+
await writeFile(targetPath, content, 'utf8');
|
|
22
|
+
}
|
|
8
23
|
async function ensureContainerHomeMountParents(input) {
|
|
9
24
|
for (const mount of input.extraMounts) {
|
|
10
25
|
const relativeTarget = posix.relative(input.containerHomeMountPath, mount.target);
|
|
@@ -47,10 +62,17 @@ export async function runSandboxCommand(input) {
|
|
|
47
62
|
if (repoRoot === undefined || repoRoot.length === 0) {
|
|
48
63
|
throw new Error('Sandbox build requires config.dev.repoRoot');
|
|
49
64
|
}
|
|
65
|
+
const effectiveDevMode = input.config.dev?.mode ?? 'packaged';
|
|
66
|
+
await ensureDockerfile({
|
|
67
|
+
wakeRoot: input.wakeRoot,
|
|
68
|
+
devMode: effectiveDevMode,
|
|
69
|
+
packagedTemplatesRoot: input.packagedTemplatesRoot,
|
|
70
|
+
});
|
|
50
71
|
await input.docker.build({
|
|
51
72
|
image: input.config.sandbox.image,
|
|
52
73
|
dockerfile: resolve(input.wakeRoot, 'docker', 'Dockerfile'),
|
|
53
74
|
contextDir: repoRoot,
|
|
75
|
+
...(effectiveDevMode === 'packaged' ? { buildArgs: { WAKE_VERSION: wakeVersion } } : {}),
|
|
54
76
|
});
|
|
55
77
|
return;
|
|
56
78
|
}
|
|
@@ -113,7 +135,8 @@ export async function runSandboxCommand(input) {
|
|
|
113
135
|
throw new Error('Sandbox self-update requires config.dev.repoRoot');
|
|
114
136
|
}
|
|
115
137
|
if (input.selfUpdate === undefined) {
|
|
116
|
-
throw new Error('Sandbox self-update requires
|
|
138
|
+
throw new Error('Sandbox self-update requires dev.mode: "source". For a packaged install, update instead with:\n' +
|
|
139
|
+
' npm install -g @atolis-hq/wake@latest && wake sandbox build && wake sandbox update');
|
|
117
140
|
}
|
|
118
141
|
const selfUpdateArgs = input.args.slice(1);
|
|
119
142
|
const runSelfUpdate = selfUpdateArgs.includes('--loop')
|
|
@@ -146,7 +169,10 @@ export async function runSandboxCommand(input) {
|
|
|
146
169
|
return;
|
|
147
170
|
}
|
|
148
171
|
if (subcommand === 'setup') {
|
|
149
|
-
|
|
172
|
+
const setupCommand = input.config.dev?.mode === 'source'
|
|
173
|
+
? ['node', '/app/dist/src/main.js', 'sandbox-setup']
|
|
174
|
+
: ['wake', 'sandbox-setup'];
|
|
175
|
+
await input.docker.exec(input.config.sandbox.containerName, setupCommand, {
|
|
150
176
|
interactive: true,
|
|
151
177
|
});
|
|
152
178
|
return;
|
|
@@ -154,15 +180,18 @@ export async function runSandboxCommand(input) {
|
|
|
154
180
|
if (subcommand === 'exec') {
|
|
155
181
|
const commandArgs = input.args.slice(1);
|
|
156
182
|
const wrappedCommand = commandArgs[0] === '--' ? commandArgs.slice(1) : commandArgs;
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
183
|
+
if (wrappedCommand.length === 0) {
|
|
184
|
+
// No command given: drop into an interactive shell with a real TTY,
|
|
185
|
+
// same as `docker exec -it ... bash`. That's a genuine interactive
|
|
186
|
+
// use case, so it stays on the inherited-stdio path rather than the
|
|
187
|
+
// piped/scrubbed one below (which would break TTY behavior).
|
|
188
|
+
await input.docker.exec(input.config.sandbox.containerName, []);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
await input.docker.execCaptured(input.config.sandbox.containerName, wrappedCommand, {
|
|
192
|
+
onStdout: (line) => input.logger.info(line),
|
|
193
|
+
onStderr: (line) => (input.logger.error ?? input.logger.info)(line),
|
|
194
|
+
});
|
|
166
195
|
return;
|
|
167
196
|
}
|
|
168
197
|
if (subcommand === 'logs') {
|
|
@@ -186,6 +215,7 @@ export async function runSandboxCommand(input) {
|
|
|
186
215
|
wakeRoot: input.wakeRoot,
|
|
187
216
|
containerHomeRoot: input.containerHomeRoot,
|
|
188
217
|
buildResumeCommand: runnerAdapter.buildResumeCommand,
|
|
218
|
+
logger: input.logger,
|
|
189
219
|
});
|
|
190
220
|
return;
|
|
191
221
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { createWakePaths } from '../lib/paths.js';
|
|
2
|
+
// The entrypoint always runs against the fixed in-container mount point
|
|
3
|
+
// (/wake), so a literal wakeRoot here is safe.
|
|
4
|
+
const CONTROL_PLANE_UI_URL_FILE = createWakePaths('/wake').controlPlaneUiUrlFile;
|
|
5
|
+
const DEFAULT_UI_PORT = '4317';
|
|
6
|
+
const DEFAULT_START_RESTART_DELAY_SECONDS = 10;
|
|
7
|
+
/**
|
|
8
|
+
* Resolves how to invoke the wake CLI from inside the container.
|
|
9
|
+
*
|
|
10
|
+
* Source-mode images (docker/Dockerfile) bake `WAKE_MAIN_JS` at build time,
|
|
11
|
+
* pointing at the compiled entrypoint under /app. Packaged-mode images
|
|
12
|
+
* (docker/Dockerfile.packaged) have no /app directory at all — the CLI is
|
|
13
|
+
* installed globally via npm and exposed as a `wake` binary on PATH, so the
|
|
14
|
+
* absence of WAKE_MAIN_JS is the signal to invoke it bare.
|
|
15
|
+
*/
|
|
16
|
+
function resolveWakeInvocation(env) {
|
|
17
|
+
const mainJs = env.WAKE_MAIN_JS;
|
|
18
|
+
return mainJs !== undefined
|
|
19
|
+
? { command: 'node', argsPrefix: [mainJs] }
|
|
20
|
+
: { command: 'wake', argsPrefix: [] };
|
|
21
|
+
}
|
|
22
|
+
async function discoverAndWriteNgrokUrl(deps) {
|
|
23
|
+
await deps.removeFile(CONTROL_PLANE_UI_URL_FILE);
|
|
24
|
+
const publicUrl = await deps.discoverNgrokUrl();
|
|
25
|
+
if (publicUrl !== undefined) {
|
|
26
|
+
await deps.writeFile(CONTROL_PLANE_UI_URL_FILE, `${publicUrl}\n`);
|
|
27
|
+
deps.log(`wake ui: ngrok tunnel available at ${publicUrl}`);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
deps.log('wake ui: ngrok tunnel started but public URL was not discovered; see /wake/.wake/logs/ngrok.log');
|
|
31
|
+
}
|
|
32
|
+
async function superviseWakeStart(deps, restartDelaySeconds) {
|
|
33
|
+
for (;;) {
|
|
34
|
+
deps.log('wake start: starting resident loop');
|
|
35
|
+
const { command, argsPrefix } = resolveWakeInvocation(deps.env);
|
|
36
|
+
// --no-sandbox: this already IS the sandbox — docker/Dockerfile is
|
|
37
|
+
// bind-mounted at /wake, so without this the process's own
|
|
38
|
+
// dispatchMainCommand would see it and try to auto-delegate into
|
|
39
|
+
// `docker exec`, which doesn't exist inside the container itself.
|
|
40
|
+
const { pid } = deps.spawnDetached(command, [...argsPrefix, 'start', '--wake-root', '/wake', '--no-sandbox'], { logFile: '/wake/.wake/logs/start.log' });
|
|
41
|
+
await deps.writeFile('/wake/.wake/logs/start.pid', String(pid));
|
|
42
|
+
const exitCode = await deps.waitForExit(pid);
|
|
43
|
+
deps.log(`wake start: resident loop exited with status ${exitCode}; restarting in ${restartDelaySeconds}s`);
|
|
44
|
+
await deps.sleep(restartDelaySeconds * 1000);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export async function runSandboxEntrypointCommand(deps) {
|
|
48
|
+
const { env } = deps;
|
|
49
|
+
await deps.ensureDir('/wake/.wake/logs');
|
|
50
|
+
if (env.WAKE_UI_ENABLED === 'true') {
|
|
51
|
+
const port = env.WAKE_UI_PORT ?? DEFAULT_UI_PORT;
|
|
52
|
+
deps.log(`wake ui: starting on 0.0.0.0:${port}`);
|
|
53
|
+
const { command, argsPrefix } = resolveWakeInvocation(env);
|
|
54
|
+
// --no-sandbox: same reason as the `wake start` spawn below — without
|
|
55
|
+
// it, this process would try to `docker exec` itself from inside the
|
|
56
|
+
// container it's already running in.
|
|
57
|
+
const uiArgs = [
|
|
58
|
+
...argsPrefix,
|
|
59
|
+
'ui',
|
|
60
|
+
'--wake-root',
|
|
61
|
+
'/wake',
|
|
62
|
+
'--host',
|
|
63
|
+
'0.0.0.0',
|
|
64
|
+
'--port',
|
|
65
|
+
port,
|
|
66
|
+
'--no-sandbox',
|
|
67
|
+
];
|
|
68
|
+
if (env.WAKE_UI_TOKEN) {
|
|
69
|
+
uiArgs.push('--token', env.WAKE_UI_TOKEN);
|
|
70
|
+
}
|
|
71
|
+
deps.spawnDetached(command, uiArgs, { logFile: '/wake/.wake/logs/ui.log' });
|
|
72
|
+
if (env.WAKE_UI_TUNNEL_ENABLED === 'true') {
|
|
73
|
+
if (env.NGROK_AUTHTOKEN) {
|
|
74
|
+
const { pid } = deps.spawnDetached('ngrok', ['config', 'add-authtoken', env.NGROK_AUTHTOKEN], { logFile: '/wake/.wake/logs/ngrok.log' });
|
|
75
|
+
await deps.waitForExit(pid);
|
|
76
|
+
}
|
|
77
|
+
deps.log(`wake ui: starting ngrok tunnel for 127.0.0.1:${port}`);
|
|
78
|
+
deps.spawnDetached('ngrok', ['http', `127.0.0.1:${port}`, '--log=stdout'], {
|
|
79
|
+
logFile: '/wake/.wake/logs/ngrok.log',
|
|
80
|
+
});
|
|
81
|
+
void discoverAndWriteNgrokUrl(deps);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (env.WAKE_START_ENABLED === 'true') {
|
|
85
|
+
const restartDelaySeconds = Number(env.WAKE_START_RESTART_DELAY_SECONDS ?? DEFAULT_START_RESTART_DELAY_SECONDS);
|
|
86
|
+
void superviseWakeStart(deps, restartDelaySeconds);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
const ENV_SECRET_PATTERN = /([A-Za-z0-9_]*(?:TOKEN|SECRET|PASSWORD|PASS|KEY)[A-Za-z0-9_]*=)[^\s]+/gi;
|
|
2
|
+
const GITHUB_TOKEN_PATTERN = /(?:gho|ghp|github_pat)_[A-Za-z0-9_]+/g;
|
|
3
|
+
export function scrubSecrets(line) {
|
|
4
|
+
return line
|
|
5
|
+
.replace(ENV_SECRET_PATTERN, '$1[REDACTED]')
|
|
6
|
+
.replace(GITHUB_TOKEN_PATTERN, '[REDACTED]');
|
|
7
|
+
}
|
|
@@ -2,7 +2,13 @@ import { stdin as input, stdout as output } from 'node:process';
|
|
|
2
2
|
import { createInterface } from 'node:readline/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { listRunRecords } from '../adapters/fs/state-store.js';
|
|
5
|
-
|
|
5
|
+
function shellQuote(value) {
|
|
6
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
7
|
+
}
|
|
8
|
+
function buildCwdWrappedShellCommand(cwd, command) {
|
|
9
|
+
const shellCommand = `cd ${shellQuote(cwd)} && ${command.map(shellQuote).join(' ')}`;
|
|
10
|
+
return ['sh', '-c', shellCommand];
|
|
11
|
+
}
|
|
6
12
|
function readFlag(name, args) {
|
|
7
13
|
const index = args.indexOf(name);
|
|
8
14
|
if (index === -1) {
|
|
@@ -66,12 +72,8 @@ export async function runSandboxResumeCommand(input) {
|
|
|
66
72
|
}
|
|
67
73
|
throw new Error('No resumable sandbox session selected.');
|
|
68
74
|
}
|
|
69
|
-
await input.docker.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
containerHomeRoot: input.containerHomeRoot,
|
|
74
|
-
cwd: target.workspacePath,
|
|
75
|
-
command: input.buildResumeCommand({ sessionId: target.sessionId }),
|
|
76
|
-
}));
|
|
75
|
+
await input.docker.execCaptured(input.config.sandbox.containerName, buildCwdWrappedShellCommand(target.workspacePath, input.buildResumeCommand({ sessionId: target.sessionId })), {
|
|
76
|
+
onStdout: (line) => input.logger.info(line),
|
|
77
|
+
onStderr: (line) => (input.logger.error ?? input.logger.info)(line),
|
|
78
|
+
});
|
|
77
79
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export async function runSandboxSetupCommand(deps) {
|
|
2
|
+
deps.log('Wake sandbox setup starting.');
|
|
3
|
+
await deps.prepareCodexHome();
|
|
4
|
+
await deps.ensureSshKey();
|
|
5
|
+
if (await deps.prompt('Configure GitHub auth? [y/N]')) {
|
|
6
|
+
deps.log('Optional best practice: sign in with a dedicated GitHub identity for Wake-managed agent work, rather than your main personal account. Make sure it has only the repository access Wake needs.');
|
|
7
|
+
await deps.runInteractive('gh', ['auth', 'login']);
|
|
8
|
+
await deps.runInteractive('gh', ['auth', 'setup-git']);
|
|
9
|
+
}
|
|
10
|
+
else {
|
|
11
|
+
deps.log('Skipping GitHub auth setup.');
|
|
12
|
+
}
|
|
13
|
+
if (await deps.prompt('Configure Claude auth? [y/N]')) {
|
|
14
|
+
await deps.runInteractive('claude', ['auth', 'login', '--claudeai']);
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
deps.log('Skipping Claude auth setup.');
|
|
18
|
+
}
|
|
19
|
+
if (await deps.prompt('Configure Codex auth? [y/N]')) {
|
|
20
|
+
await deps.runInteractive('codex', ['login']);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
deps.log('Skipping Codex auth setup.');
|
|
24
|
+
}
|
|
25
|
+
if (await deps.prompt('Configure Cursor auth? [y/N]')) {
|
|
26
|
+
await deps.runInteractive('agent', ['login']);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
deps.log('Skipping Cursor auth setup.');
|
|
30
|
+
}
|
|
31
|
+
}
|