@tanstack/ai-sandbox 0.2.3 → 0.3.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.
Files changed (158) hide show
  1. package/dist/esm/agents-file.js +53 -34
  2. package/dist/esm/agents-file.js.map +1 -1
  3. package/dist/esm/align.d.ts +121 -0
  4. package/dist/esm/align.js +197 -0
  5. package/dist/esm/align.js.map +1 -0
  6. package/dist/esm/approvals.js +63 -29
  7. package/dist/esm/approvals.js.map +1 -1
  8. package/dist/esm/attach-preflight.d.ts +85 -0
  9. package/dist/esm/attach-preflight.js +189 -0
  10. package/dist/esm/attach-preflight.js.map +1 -0
  11. package/dist/esm/bootstrap.js +103 -117
  12. package/dist/esm/bootstrap.js.map +1 -1
  13. package/dist/esm/bridge-events.js +96 -71
  14. package/dist/esm/bridge-events.js.map +1 -1
  15. package/dist/esm/capabilities.d.ts +0 -5
  16. package/dist/esm/capabilities.js +32 -28
  17. package/dist/esm/capabilities.js.map +1 -1
  18. package/dist/esm/chunk-identity.d.ts +52 -0
  19. package/dist/esm/chunk-identity.js +102 -0
  20. package/dist/esm/chunk-identity.js.map +1 -0
  21. package/dist/esm/claim.d.ts +187 -0
  22. package/dist/esm/claim.js +349 -0
  23. package/dist/esm/claim.js.map +1 -0
  24. package/dist/esm/contracts.d.ts +13 -0
  25. package/dist/esm/driver.d.ts +83 -0
  26. package/dist/esm/driver.js +138 -0
  27. package/dist/esm/driver.js.map +1 -0
  28. package/dist/esm/durability.d.ts +263 -0
  29. package/dist/esm/durability.js +230 -0
  30. package/dist/esm/durability.js.map +1 -0
  31. package/dist/esm/errors.js +28 -24
  32. package/dist/esm/errors.js.map +1 -1
  33. package/dist/esm/file-diff.js +151 -135
  34. package/dist/esm/file-diff.js.map +1 -1
  35. package/dist/esm/git-exec.js +51 -62
  36. package/dist/esm/git-exec.js.map +1 -1
  37. package/dist/esm/harness-cwd.js +24 -19
  38. package/dist/esm/harness-cwd.js.map +1 -1
  39. package/dist/esm/index.d.ts +30 -8
  40. package/dist/esm/index.js +23 -91
  41. package/dist/esm/instance-store.d.ts +88 -0
  42. package/dist/esm/instance-store.js +67 -0
  43. package/dist/esm/instance-store.js.map +1 -0
  44. package/dist/esm/journal-bytes.d.ts +67 -0
  45. package/dist/esm/journal-bytes.js +110 -0
  46. package/dist/esm/journal-bytes.js.map +1 -0
  47. package/dist/esm/journal-reader.d.ts +66 -0
  48. package/dist/esm/journal-reader.js +228 -0
  49. package/dist/esm/journal-reader.js.map +1 -0
  50. package/dist/esm/journal-sweep.d.ts +113 -0
  51. package/dist/esm/journal-sweep.js +309 -0
  52. package/dist/esm/journal-sweep.js.map +1 -0
  53. package/dist/esm/journal.d.ts +542 -0
  54. package/dist/esm/journal.js +679 -0
  55. package/dist/esm/journal.js.map +1 -0
  56. package/dist/esm/key.js +36 -33
  57. package/dist/esm/key.js.map +1 -1
  58. package/dist/esm/middleware.d.ts +50 -2
  59. package/dist/esm/middleware.js +335 -208
  60. package/dist/esm/middleware.js.map +1 -1
  61. package/dist/esm/ngrok.js +75 -49
  62. package/dist/esm/ngrok.js.map +1 -1
  63. package/dist/esm/policy.js +43 -34
  64. package/dist/esm/policy.js.map +1 -1
  65. package/dist/esm/projection.js +16 -8
  66. package/dist/esm/projection.js.map +1 -1
  67. package/dist/esm/reap.d.ts +238 -0
  68. package/dist/esm/reap.js +355 -0
  69. package/dist/esm/reap.js.map +1 -0
  70. package/dist/esm/reclaim.d.ts +84 -0
  71. package/dist/esm/reclaim.js +106 -0
  72. package/dist/esm/reclaim.js.map +1 -0
  73. package/dist/esm/remote-tools.js +73 -62
  74. package/dist/esm/remote-tools.js.map +1 -1
  75. package/dist/esm/run.d.ts +93 -25
  76. package/dist/esm/run.js +274 -79
  77. package/dist/esm/run.js.map +1 -1
  78. package/dist/esm/runner.d.ts +119 -2
  79. package/dist/esm/runner.js +270 -51
  80. package/dist/esm/runner.js.map +1 -1
  81. package/dist/esm/sandbox.d.ts +3 -2
  82. package/dist/esm/sandbox.js +139 -123
  83. package/dist/esm/sandbox.js.map +1 -1
  84. package/dist/esm/secrets.js +39 -47
  85. package/dist/esm/secrets.js.map +1 -1
  86. package/dist/esm/setup-plan.js +22 -14
  87. package/dist/esm/setup-plan.js.map +1 -1
  88. package/dist/esm/shell.d.ts +8 -0
  89. package/dist/esm/shell.js +197 -158
  90. package/dist/esm/shell.js.map +1 -1
  91. package/dist/esm/testkit/conformance.d.ts +16 -0
  92. package/dist/esm/testkit/conformance.js +97 -0
  93. package/dist/esm/testkit/conformance.js.map +1 -0
  94. package/dist/esm/testkit/durable-run-fields-conformance.d.ts +4 -0
  95. package/dist/esm/testkit/durable-run-fields-conformance.js +95 -0
  96. package/dist/esm/testkit/durable-run-fields-conformance.js.map +1 -0
  97. package/dist/esm/testkit/journal-conformance.d.ts +51 -0
  98. package/dist/esm/testkit/journal-conformance.js +378 -0
  99. package/dist/esm/testkit/journal-conformance.js.map +1 -0
  100. package/dist/esm/testkit/reaper-conformance.d.ts +37 -0
  101. package/dist/esm/testkit/reaper-conformance.js +847 -0
  102. package/dist/esm/testkit/reaper-conformance.js.map +1 -0
  103. package/dist/esm/testkit/shell-spawn.d.ts +2 -0
  104. package/dist/esm/testkit/shell-spawn.js +60 -0
  105. package/dist/esm/testkit/shell-spawn.js.map +1 -0
  106. package/dist/esm/testkit/takeover-conformance.d.ts +24 -0
  107. package/dist/esm/testkit/takeover-conformance.js +685 -0
  108. package/dist/esm/testkit/takeover-conformance.js.map +1 -0
  109. package/dist/esm/tool-bridge.js +227 -180
  110. package/dist/esm/tool-bridge.js.map +1 -1
  111. package/dist/esm/tool-history.d.ts +62 -0
  112. package/dist/esm/tool-history.js +171 -0
  113. package/dist/esm/tool-history.js.map +1 -0
  114. package/dist/esm/watch.js +310 -236
  115. package/dist/esm/watch.js.map +1 -1
  116. package/dist/esm/workspace.d.ts +1 -1
  117. package/dist/esm/workspace.js +49 -28
  118. package/dist/esm/workspace.js.map +1 -1
  119. package/package.json +16 -6
  120. package/skills/ai-sandbox/SKILL.md +658 -20
  121. package/src/align.ts +297 -0
  122. package/src/attach-preflight.ts +292 -0
  123. package/src/capabilities.ts +4 -13
  124. package/src/chunk-identity.ts +154 -0
  125. package/src/claim.ts +479 -0
  126. package/src/contracts.ts +13 -0
  127. package/src/driver.ts +205 -0
  128. package/src/durability.ts +380 -0
  129. package/src/index.ts +212 -27
  130. package/src/instance-store.ts +122 -0
  131. package/src/journal-bytes.ts +136 -0
  132. package/src/journal-reader.ts +359 -0
  133. package/src/journal-sweep.ts +406 -0
  134. package/src/journal.ts +875 -0
  135. package/src/middleware.ts +470 -30
  136. package/src/reap.ts +723 -0
  137. package/src/reclaim.ts +191 -0
  138. package/src/run.ts +365 -75
  139. package/src/runner.ts +347 -3
  140. package/src/sandbox.ts +38 -8
  141. package/src/shell.ts +106 -38
  142. package/src/testkit/conformance.ts +117 -0
  143. package/src/testkit/durable-run-fields-conformance.ts +147 -0
  144. package/src/testkit/journal-conformance.ts +676 -0
  145. package/src/testkit/reaper-conformance.ts +1201 -0
  146. package/src/testkit/shell-spawn.ts +67 -0
  147. package/src/testkit/takeover-conformance.ts +1040 -0
  148. package/src/tool-history.ts +245 -0
  149. package/src/workspace.ts +1 -1
  150. package/dist/esm/index.js.map +0 -1
  151. package/dist/esm/run-log.d.ts +0 -81
  152. package/dist/esm/run-log.js +0 -107
  153. package/dist/esm/run-log.js.map +0 -1
  154. package/dist/esm/store.d.ts +0 -53
  155. package/dist/esm/store.js +0 -34
  156. package/dist/esm/store.js.map +0 -1
  157. package/src/run-log.ts +0 -224
  158. package/src/store.ts +0 -83
@@ -0,0 +1,85 @@
1
+ import { JournalPaths } from './journal.js';
2
+ import { SandboxHandle } from './contracts.js';
3
+ import { InternalLogger } from '@tanstack/ai/adapter-internals';
4
+ import { RunStore } from '@tanstack/ai';
5
+ /**
6
+ * How long an attach waits for a live run's journal to appear before failing.
7
+ *
8
+ * User-relevant, hence exported: this bounds how long an attach REQUEST can sit
9
+ * before it answers, so an application that fronts the attach route with its own
10
+ * timeout needs to know the number. Generous relative to the gap between a
11
+ * driver claiming a run and its first journal write (a `spawn` plus one line),
12
+ * and short relative to any sane HTTP timeout. Override per run with
13
+ * `SandboxDurabilityOptions.attachWaitMs`.
14
+ */
15
+ export declare const DEFAULT_ATTACH_JOURNAL_WAIT_MS = 10000;
16
+ /**
17
+ * How often the bounded wait re-probes for the journal. Not user-facing: it
18
+ * trades a `test -f` per interval for attach latency, and neither number is
19
+ * something an application tunes.
20
+ */
21
+ export declare const DEFAULT_ATTACH_PROBE_INTERVAL_MS = 100;
22
+ /**
23
+ * Which of the three hopeless-attach cases was hit. Exported so a consumer can
24
+ * branch (a 404 for `'unknown-run'`, a 410 for `'terminal-run'`, a 504 for
25
+ * `'journal-timeout'`) instead of matching on message text.
26
+ */
27
+ export type AttachUnavailableReason = 'unknown-run' | 'terminal-run' | 'journal-timeout'
28
+ /**
29
+ * The journal EXISTS but produced no bytes at all within the deadline, so no
30
+ * sentinel can be coming and the follow would tail an empty (or abandoned)
31
+ * file forever. Raised by `journal-reader.ts`, not by the preflight: the
32
+ * preflight cannot see this state, because `test -f` succeeds for it.
33
+ *
34
+ * A 504 at an attach route, exactly like `'journal-timeout'`, which is why it
35
+ * shares {@link JournalAttachUnavailableError} — but a distinct value, because
36
+ * the cause is different: `'journal-timeout'` means nobody created the
37
+ * journal, `'journal-stalled'` means somebody did and then stopped (a
38
+ * SIGKILLed agent shell, a destroyed sandbox, a reader that created the file
39
+ * itself on a fail-open path).
40
+ */
41
+ | 'journal-stalled';
42
+ /**
43
+ * An attach cannot succeed, and waiting longer would not change that.
44
+ *
45
+ * One class with a {@link AttachUnavailableReason} discriminant rather than three
46
+ * classes: every consumer of this path handles all three cases at the same seam
47
+ * (the attach route), so one `instanceof` plus a `switch (error.reason)` is the
48
+ * shape that is actually written, while the message names the specific case for a
49
+ * human reading a log.
50
+ */
51
+ export declare class JournalAttachUnavailableError extends Error {
52
+ readonly runId: string;
53
+ readonly reason: AttachUnavailableReason;
54
+ constructor(runId: string, reason: AttachUnavailableReason, detail: string);
55
+ }
56
+ export interface AwaitAttachableJournalOptions {
57
+ /** The run's journal paths, as {@link journalPaths} derived them. */
58
+ paths: JournalPaths;
59
+ /** Run id, for the store lookup and the error messages. */
60
+ runId: string;
61
+ /**
62
+ * The authoritative run record store. Omitted only by a caller with no store
63
+ * wired, which loses the unknown/terminal classification but keeps the bound.
64
+ */
65
+ runs?: RunStore;
66
+ /** Bounded wait. Defaults to {@link DEFAULT_ATTACH_JOURNAL_WAIT_MS}. */
67
+ waitMs?: number;
68
+ /** Re-probe interval. Defaults to {@link DEFAULT_ATTACH_PROBE_INTERVAL_MS}. */
69
+ probeIntervalMs?: number;
70
+ /**
71
+ * The consumer's abort. An aborted wait returns rather than throwing: the
72
+ * caller stopped caring, which is not a diagnosis about the run.
73
+ */
74
+ signal?: AbortSignal;
75
+ logger?: InternalLogger;
76
+ }
77
+ /**
78
+ * Resolve once the run's journal can be tailed, or reject with a
79
+ * {@link JournalAttachUnavailableError} explaining why it never will be.
80
+ *
81
+ * Call this BEFORE the first follow/poll read of an attach, never on a fresh
82
+ * run: a fresh run's journal is created by its own `journaledCommand` spawn,
83
+ * which has not happened yet, so gating it would fail every new run.
84
+ */
85
+ export declare function awaitAttachableJournal(handle: SandboxHandle, options: AwaitAttachableJournalOptions): Promise<void>;
@@ -0,0 +1,189 @@
1
+ import { journalExistsCommand } from "./journal.js";
2
+ import { isTerminalRunStatus } from "@tanstack/ai";
3
+ //#region src/attach-preflight.ts
4
+ /**
5
+ * The gate that turns a HOPELESS attach into an error instead of an infinite
6
+ * wait.
7
+ *
8
+ * `journalFollowCommand` creates the journal before tailing it (`: >> file`),
9
+ * because `tail -f` on a missing path prints a diagnostic and EXITS rather than
10
+ * waiting — a defect that made a legitimate attach racing the driver's first
11
+ * write deliver zero lines. But creating the file has a cost: an attach for a
12
+ * `runId` that never had a journal creates an EMPTY one and tails it forever. No
13
+ * `{"__exit":N}` sentinel can ever arrive, so the caller waits indefinitely with
14
+ * no error, no timeout, and no log line — for what is the single most likely
15
+ * mistake on this path (a stale link, a typo, a run whose journal was cleaned up
16
+ * after completing).
17
+ *
18
+ * Absence of the journal alone cannot decide the question, which is exactly why
19
+ * `: >> file` exists: "not written YET" and "will never be written" look
20
+ * identical on the filesystem. The RUN RECORD is what distinguishes them, and it
21
+ * is authoritative — `runs.get(runId)` says whether the run exists at all,
22
+ * whether it is terminal, and (via `detachedSince`) whether anyone is expected
23
+ * to be driving it. So the policy is:
24
+ *
25
+ * | journal | record | decision |
26
+ * | --------- | ----------------------- | ------------------------------------- |
27
+ * | exists | (not consulted) | attach, under the reader's own bound |
28
+ * | absent | unknown (`null`) | fail fast, `'unknown-run'` |
29
+ * | absent | terminal | fail fast, `'terminal-run'` |
30
+ * | absent | running / interrupted | BOUNDED wait, then `'journal-timeout'`|
31
+ * | unusable | running / interrupted | BOUNDED wait, then `'journal-timeout'`|
32
+ *
33
+ * Four deliberate choices in that table:
34
+ *
35
+ * 1. **An existing journal short-circuits this gate**, before the store is read
36
+ * at all — because gating it would make a perfectly readable journal
37
+ * unreadable whenever a store lost its record. It does NOT mean the read is
38
+ * unbounded: this module used to justify the short-circuit with "a journal
39
+ * that exists either carries a sentinel or is still being appended to, neither
40
+ * hangs", and that trichotomy was FALSE. `journalFollowCommand`'s first act is
41
+ * `: >> file`, so the reader itself manufactures the third state — a file that
42
+ * exists, receives nothing, and can never receive a sentinel — and the same
43
+ * state is independently reachable by SIGKILL/OOM of the agent's shell before
44
+ * its `printf`. The bound for it lives where it belongs, on the read:
45
+ * `journal-reader.ts` fails a follow/poll that receives no bytes at all within
46
+ * {@link DEFAULT_ATTACH_JOURNAL_WAIT_MS} with `'journal-stalled'`.
47
+ * 2. **A terminal record with no journal fails rather than waiting.** Nothing
48
+ * will ever be appended: the run is over and `journalCleanupCommand` deletes a
49
+ * terminal run's files by design. Its transcript lives in the event log, which
50
+ * the resume response serves independently of this path.
51
+ * 3. **A live or detached record waits, but not forever.** A driver that has
52
+ * claimed the run and not yet written its first line is the normal case, not
53
+ * the unlucky one, so failing fast here would break the very race
54
+ * `journalFollowCommand` was fixed to tolerate. `detachedSince` does NOT
55
+ * change the decision — a detached run's journal is exactly what a successor
56
+ * is supposed to read, and a driver that died before its first write leaves an
57
+ * identical filesystem state — but it IS reported in the timeout message,
58
+ * since "detached with no journal after N ms" and "attached with no journal
59
+ * after N ms" point at different causes.
60
+ * 4. **An UNUSABLE probe falls through to the bounded wait; it does not skip the
61
+ * gate.** This module used to fail open here — `if (existence === 'unknown')
62
+ * return` — on the reasoning that a diagnostic gate must not break an attach
63
+ * that would otherwise have worked. That reasoning inverted the actual risk.
64
+ * Returning handed control to a reader whose very first act CREATES the
65
+ * journal (`journalFollowCommand`'s `: >> file`) and then tails it forever, so
66
+ * the fail-open path did not preserve a working attach — it manufactured the
67
+ * exact infinite wait this module exists to prevent. Worse, it was
68
+ * self-perpetuating: the file it created made `test -f` succeed from then on,
69
+ * so every LATER attach short-circuited at choice 1 and hung too, permanently,
70
+ * long after the transient probe failure had cleared. An unanswerable probe is
71
+ * precisely when a deadline matters most, so an unusable probe is re-polled
72
+ * (it may recover) and, failing that, times out. The store checks still run
73
+ * first and need no probe, so an unknown or terminal `runId` still fails fast.
74
+ */
75
+ /**
76
+ * How long an attach waits for a live run's journal to appear before failing.
77
+ *
78
+ * User-relevant, hence exported: this bounds how long an attach REQUEST can sit
79
+ * before it answers, so an application that fronts the attach route with its own
80
+ * timeout needs to know the number. Generous relative to the gap between a
81
+ * driver claiming a run and its first journal write (a `spawn` plus one line),
82
+ * and short relative to any sane HTTP timeout. Override per run with
83
+ * `SandboxDurabilityOptions.attachWaitMs`.
84
+ */
85
+ var DEFAULT_ATTACH_JOURNAL_WAIT_MS = 1e4;
86
+ /**
87
+ * How often the bounded wait re-probes for the journal. Not user-facing: it
88
+ * trades a `test -f` per interval for attach latency, and neither number is
89
+ * something an application tunes.
90
+ */
91
+ var DEFAULT_ATTACH_PROBE_INTERVAL_MS = 100;
92
+ /**
93
+ * An attach cannot succeed, and waiting longer would not change that.
94
+ *
95
+ * One class with a {@link AttachUnavailableReason} discriminant rather than three
96
+ * classes: every consumer of this path handles all three cases at the same seam
97
+ * (the attach route), so one `instanceof` plus a `switch (error.reason)` is the
98
+ * shape that is actually written, while the message names the specific case for a
99
+ * human reading a log.
100
+ */
101
+ var JournalAttachUnavailableError = class extends Error {
102
+ runId;
103
+ reason;
104
+ constructor(runId, reason, detail) {
105
+ super(`cannot attach to run ${runId}: ${detail}`);
106
+ this.runId = runId;
107
+ this.reason = reason;
108
+ this.name = "JournalAttachUnavailableError";
109
+ }
110
+ };
111
+ /**
112
+ * Shell `test -f`, never `handle.fs.exists` — `journal.ts` rule 3: on
113
+ * local-process the two resolve `/tmp` differently, so `fs.exists` would probe a
114
+ * path the journal was never written to and report `false` for every run.
115
+ */
116
+ async function probeJournal(handle, options) {
117
+ try {
118
+ return (await handle.process.exec(journalExistsCommand(options.paths))).exitCode === 0 ? "yes" : "no";
119
+ } catch (error) {
120
+ options.logger?.provider(`attach preflight: journal existence probe failed for run ${options.runId}; re-probing under the bounded wait rather than attaching blind`, {
121
+ runId: options.runId,
122
+ error
123
+ });
124
+ return "unknown";
125
+ }
126
+ }
127
+ /**
128
+ * `null` means the store answered "no such run" — a real, actionable fact.
129
+ * `undefined` means there is no answer to be had (no store, or `get` threw), and
130
+ * the caller must not treat that as "unknown run".
131
+ */
132
+ async function readRecord(options) {
133
+ if (options.runs === void 0) return void 0;
134
+ try {
135
+ return await options.runs.get(options.runId);
136
+ } catch (error) {
137
+ options.logger?.errors(`attach preflight: reading the run record failed for run ${options.runId}`, {
138
+ runId: options.runId,
139
+ error
140
+ });
141
+ return;
142
+ }
143
+ }
144
+ function sleep(ms, signal) {
145
+ if (ms <= 0) return Promise.resolve();
146
+ return new Promise((resolve) => {
147
+ const timer = setTimeout(finish, ms);
148
+ function finish() {
149
+ clearTimeout(timer);
150
+ signal?.removeEventListener("abort", finish);
151
+ resolve();
152
+ }
153
+ signal?.addEventListener("abort", finish, { once: true });
154
+ });
155
+ }
156
+ function describeRecord(record) {
157
+ return record.detachedSince === void 0 ? `status '${record.status}' with a viewer attached` : `status '${record.status}', detached since ${new Date(record.detachedSince).toISOString()}`;
158
+ }
159
+ /**
160
+ * Resolve once the run's journal can be tailed, or reject with a
161
+ * {@link JournalAttachUnavailableError} explaining why it never will be.
162
+ *
163
+ * Call this BEFORE the first follow/poll read of an attach, never on a fresh
164
+ * run: a fresh run's journal is created by its own `journaledCommand` spawn,
165
+ * which has not happened yet, so gating it would fail every new run.
166
+ */
167
+ async function awaitAttachableJournal(handle, options) {
168
+ const existence = await probeJournal(handle, options);
169
+ if (existence === "yes") return;
170
+ const record = await readRecord(options);
171
+ if (record === null) throw new JournalAttachUnavailableError(options.runId, "unknown-run", `no run record exists and the journal (${options.paths.journal}) has never been written, so nothing will ever be appended to it. The runId is unknown to the RunStore — it is mistyped, from another deployment, or its record has been evicted.`);
172
+ if (record !== void 0 && isTerminalRunStatus(record.status)) throw new JournalAttachUnavailableError(options.runId, "terminal-run", `the run is already '${record.status}' and its journal (${options.paths.journal}) does not exist, so nothing will ever be appended to it. A terminal run's transcript lives in its event log, not in a journal — serve the log instead of attaching.`);
173
+ const waitMs = options.waitMs ?? 1e4;
174
+ const probeIntervalMs = options.probeIntervalMs ?? 100;
175
+ const deadline = Date.now() + waitMs;
176
+ let lastExistence = existence;
177
+ for (;;) {
178
+ const remaining = deadline - Date.now();
179
+ if (remaining <= 0) throw new JournalAttachUnavailableError(options.runId, "journal-timeout", `the run record says ${record === void 0 ? "nothing (no run store is wired)" : describeRecord(record)}, ` + (lastExistence === "unknown" ? `and its journal (${options.paths.journal}) could not be probed at all within ${waitMs}ms — every '${journalExistsCommand(options.paths)}' failed. Attaching anyway would create that journal and tail it forever, so this fails instead. Check that the sandbox is still alive and that its exec transport works.` : `but its journal (${options.paths.journal}) did not appear within ${waitMs}ms. Either the driver died before writing its first line, or the journal directory does not match the one the agent was started with.`));
180
+ if (options.signal?.aborted) return;
181
+ await sleep(Math.min(probeIntervalMs, remaining), options.signal);
182
+ lastExistence = await probeJournal(handle, options);
183
+ if (lastExistence === "yes") return;
184
+ }
185
+ }
186
+ //#endregion
187
+ export { DEFAULT_ATTACH_JOURNAL_WAIT_MS, DEFAULT_ATTACH_PROBE_INTERVAL_MS, JournalAttachUnavailableError, awaitAttachableJournal };
188
+
189
+ //# sourceMappingURL=attach-preflight.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attach-preflight.js","names":[],"sources":["../../src/attach-preflight.ts"],"sourcesContent":["/**\n * The gate that turns a HOPELESS attach into an error instead of an infinite\n * wait.\n *\n * `journalFollowCommand` creates the journal before tailing it (`: >> file`),\n * because `tail -f` on a missing path prints a diagnostic and EXITS rather than\n * waiting — a defect that made a legitimate attach racing the driver's first\n * write deliver zero lines. But creating the file has a cost: an attach for a\n * `runId` that never had a journal creates an EMPTY one and tails it forever. No\n * `{\"__exit\":N}` sentinel can ever arrive, so the caller waits indefinitely with\n * no error, no timeout, and no log line — for what is the single most likely\n * mistake on this path (a stale link, a typo, a run whose journal was cleaned up\n * after completing).\n *\n * Absence of the journal alone cannot decide the question, which is exactly why\n * `: >> file` exists: \"not written YET\" and \"will never be written\" look\n * identical on the filesystem. The RUN RECORD is what distinguishes them, and it\n * is authoritative — `runs.get(runId)` says whether the run exists at all,\n * whether it is terminal, and (via `detachedSince`) whether anyone is expected\n * to be driving it. So the policy is:\n *\n * | journal | record | decision |\n * | --------- | ----------------------- | ------------------------------------- |\n * | exists | (not consulted) | attach, under the reader's own bound |\n * | absent | unknown (`null`) | fail fast, `'unknown-run'` |\n * | absent | terminal | fail fast, `'terminal-run'` |\n * | absent | running / interrupted | BOUNDED wait, then `'journal-timeout'`|\n * | unusable | running / interrupted | BOUNDED wait, then `'journal-timeout'`|\n *\n * Four deliberate choices in that table:\n *\n * 1. **An existing journal short-circuits this gate**, before the store is read\n * at all — because gating it would make a perfectly readable journal\n * unreadable whenever a store lost its record. It does NOT mean the read is\n * unbounded: this module used to justify the short-circuit with \"a journal\n * that exists either carries a sentinel or is still being appended to, neither\n * hangs\", and that trichotomy was FALSE. `journalFollowCommand`'s first act is\n * `: >> file`, so the reader itself manufactures the third state — a file that\n * exists, receives nothing, and can never receive a sentinel — and the same\n * state is independently reachable by SIGKILL/OOM of the agent's shell before\n * its `printf`. The bound for it lives where it belongs, on the read:\n * `journal-reader.ts` fails a follow/poll that receives no bytes at all within\n * {@link DEFAULT_ATTACH_JOURNAL_WAIT_MS} with `'journal-stalled'`.\n * 2. **A terminal record with no journal fails rather than waiting.** Nothing\n * will ever be appended: the run is over and `journalCleanupCommand` deletes a\n * terminal run's files by design. Its transcript lives in the event log, which\n * the resume response serves independently of this path.\n * 3. **A live or detached record waits, but not forever.** A driver that has\n * claimed the run and not yet written its first line is the normal case, not\n * the unlucky one, so failing fast here would break the very race\n * `journalFollowCommand` was fixed to tolerate. `detachedSince` does NOT\n * change the decision — a detached run's journal is exactly what a successor\n * is supposed to read, and a driver that died before its first write leaves an\n * identical filesystem state — but it IS reported in the timeout message,\n * since \"detached with no journal after N ms\" and \"attached with no journal\n * after N ms\" point at different causes.\n * 4. **An UNUSABLE probe falls through to the bounded wait; it does not skip the\n * gate.** This module used to fail open here — `if (existence === 'unknown')\n * return` — on the reasoning that a diagnostic gate must not break an attach\n * that would otherwise have worked. That reasoning inverted the actual risk.\n * Returning handed control to a reader whose very first act CREATES the\n * journal (`journalFollowCommand`'s `: >> file`) and then tails it forever, so\n * the fail-open path did not preserve a working attach — it manufactured the\n * exact infinite wait this module exists to prevent. Worse, it was\n * self-perpetuating: the file it created made `test -f` succeed from then on,\n * so every LATER attach short-circuited at choice 1 and hung too, permanently,\n * long after the transient probe failure had cleared. An unanswerable probe is\n * precisely when a deadline matters most, so an unusable probe is re-polled\n * (it may recover) and, failing that, times out. The store checks still run\n * first and need no probe, so an unknown or terminal `runId` still fails fast.\n */\nimport { isTerminalRunStatus } from '@tanstack/ai'\nimport { journalExistsCommand } from './journal'\nimport type { JournalPaths } from './journal'\nimport type { SandboxHandle } from './contracts'\nimport type { InternalLogger } from '@tanstack/ai/adapter-internals'\nimport type { RunRecord, RunStore } from '@tanstack/ai'\n\n/**\n * How long an attach waits for a live run's journal to appear before failing.\n *\n * User-relevant, hence exported: this bounds how long an attach REQUEST can sit\n * before it answers, so an application that fronts the attach route with its own\n * timeout needs to know the number. Generous relative to the gap between a\n * driver claiming a run and its first journal write (a `spawn` plus one line),\n * and short relative to any sane HTTP timeout. Override per run with\n * `SandboxDurabilityOptions.attachWaitMs`.\n */\nexport const DEFAULT_ATTACH_JOURNAL_WAIT_MS = 10_000\n\n/**\n * How often the bounded wait re-probes for the journal. Not user-facing: it\n * trades a `test -f` per interval for attach latency, and neither number is\n * something an application tunes.\n */\nexport const DEFAULT_ATTACH_PROBE_INTERVAL_MS = 100\n\n/**\n * Which of the three hopeless-attach cases was hit. Exported so a consumer can\n * branch (a 404 for `'unknown-run'`, a 410 for `'terminal-run'`, a 504 for\n * `'journal-timeout'`) instead of matching on message text.\n */\nexport type AttachUnavailableReason =\n | 'unknown-run'\n | 'terminal-run'\n | 'journal-timeout'\n /**\n * The journal EXISTS but produced no bytes at all within the deadline, so no\n * sentinel can be coming and the follow would tail an empty (or abandoned)\n * file forever. Raised by `journal-reader.ts`, not by the preflight: the\n * preflight cannot see this state, because `test -f` succeeds for it.\n *\n * A 504 at an attach route, exactly like `'journal-timeout'`, which is why it\n * shares {@link JournalAttachUnavailableError} — but a distinct value, because\n * the cause is different: `'journal-timeout'` means nobody created the\n * journal, `'journal-stalled'` means somebody did and then stopped (a\n * SIGKILLed agent shell, a destroyed sandbox, a reader that created the file\n * itself on a fail-open path).\n */\n | 'journal-stalled'\n\n/**\n * An attach cannot succeed, and waiting longer would not change that.\n *\n * One class with a {@link AttachUnavailableReason} discriminant rather than three\n * classes: every consumer of this path handles all three cases at the same seam\n * (the attach route), so one `instanceof` plus a `switch (error.reason)` is the\n * shape that is actually written, while the message names the specific case for a\n * human reading a log.\n */\nexport class JournalAttachUnavailableError extends Error {\n constructor(\n readonly runId: string,\n readonly reason: AttachUnavailableReason,\n detail: string,\n ) {\n super(`cannot attach to run ${runId}: ${detail}`)\n this.name = 'JournalAttachUnavailableError'\n }\n}\n\n/** Existence of the journal, or `'unknown'` when the probe itself failed. */\ntype JournalExistence = 'yes' | 'no' | 'unknown'\n\nexport interface AwaitAttachableJournalOptions {\n /** The run's journal paths, as {@link journalPaths} derived them. */\n paths: JournalPaths\n /** Run id, for the store lookup and the error messages. */\n runId: string\n /**\n * The authoritative run record store. Omitted only by a caller with no store\n * wired, which loses the unknown/terminal classification but keeps the bound.\n */\n runs?: RunStore\n /** Bounded wait. Defaults to {@link DEFAULT_ATTACH_JOURNAL_WAIT_MS}. */\n waitMs?: number\n /** Re-probe interval. Defaults to {@link DEFAULT_ATTACH_PROBE_INTERVAL_MS}. */\n probeIntervalMs?: number\n /**\n * The consumer's abort. An aborted wait returns rather than throwing: the\n * caller stopped caring, which is not a diagnosis about the run.\n */\n signal?: AbortSignal\n logger?: InternalLogger\n}\n\n/**\n * Shell `test -f`, never `handle.fs.exists` — `journal.ts` rule 3: on\n * local-process the two resolve `/tmp` differently, so `fs.exists` would probe a\n * path the journal was never written to and report `false` for every run.\n */\nasync function probeJournal(\n handle: SandboxHandle,\n options: AwaitAttachableJournalOptions,\n): Promise<JournalExistence> {\n try {\n const result = await handle.process.exec(\n journalExistsCommand(options.paths),\n )\n return result.exitCode === 0 ? 'yes' : 'no'\n } catch (error) {\n options.logger?.provider(\n `attach preflight: journal existence probe failed for run ${options.runId}; re-probing under the bounded wait rather than attaching blind`,\n { runId: options.runId, error },\n )\n return 'unknown'\n }\n}\n\n/**\n * `null` means the store answered \"no such run\" — a real, actionable fact.\n * `undefined` means there is no answer to be had (no store, or `get` threw), and\n * the caller must not treat that as \"unknown run\".\n */\nasync function readRecord(\n options: AwaitAttachableJournalOptions,\n): Promise<RunRecord | null | undefined> {\n if (options.runs === undefined) return undefined\n try {\n return await options.runs.get(options.runId)\n } catch (error) {\n options.logger?.errors(\n `attach preflight: reading the run record failed for run ${options.runId}`,\n { runId: options.runId, error },\n )\n return undefined\n }\n}\n\nfunction sleep(ms: number, signal: AbortSignal | undefined): Promise<void> {\n if (ms <= 0) return Promise.resolve()\n return new Promise<void>((resolve) => {\n const timer = setTimeout(finish, ms)\n function finish(): void {\n clearTimeout(timer)\n signal?.removeEventListener('abort', finish)\n resolve()\n }\n signal?.addEventListener('abort', finish, { once: true })\n })\n}\n\nfunction describeRecord(record: RunRecord): string {\n return record.detachedSince === undefined\n ? `status '${record.status}' with a viewer attached`\n : `status '${record.status}', detached since ${new Date(record.detachedSince).toISOString()}`\n}\n\n/**\n * Resolve once the run's journal can be tailed, or reject with a\n * {@link JournalAttachUnavailableError} explaining why it never will be.\n *\n * Call this BEFORE the first follow/poll read of an attach, never on a fresh\n * run: a fresh run's journal is created by its own `journaledCommand` spawn,\n * which has not happened yet, so gating it would fail every new run.\n */\nexport async function awaitAttachableJournal(\n handle: SandboxHandle,\n options: AwaitAttachableJournalOptions,\n): Promise<void> {\n const existence = await probeJournal(handle, options)\n if (existence === 'yes') return\n\n const record = await readRecord(options)\n if (record === null) {\n throw new JournalAttachUnavailableError(\n options.runId,\n 'unknown-run',\n `no run record exists and the journal (${options.paths.journal}) has never been written, so nothing will ever be appended to it. ` +\n `The runId is unknown to the RunStore — it is mistyped, from another deployment, or its record has been evicted.`,\n )\n }\n if (record !== undefined && isTerminalRunStatus(record.status)) {\n throw new JournalAttachUnavailableError(\n options.runId,\n 'terminal-run',\n `the run is already '${record.status}' and its journal (${options.paths.journal}) does not exist, so nothing will ever be appended to it. ` +\n `A terminal run's transcript lives in its event log, not in a journal — serve the log instead of attaching.`,\n )\n }\n\n // NOTE: no fail-open branch here. An `existence === 'unknown'` probe falls\n // through into the bounded wait below — see choice 4 in the module doc for why\n // returning was worse than timing out, not safer.\n const waitMs = options.waitMs ?? DEFAULT_ATTACH_JOURNAL_WAIT_MS\n const probeIntervalMs =\n options.probeIntervalMs ?? DEFAULT_ATTACH_PROBE_INTERVAL_MS\n const deadline = Date.now() + waitMs\n let lastExistence: JournalExistence = existence\n for (;;) {\n const remaining = deadline - Date.now()\n if (remaining <= 0) {\n throw new JournalAttachUnavailableError(\n options.runId,\n 'journal-timeout',\n `the run record says ${record === undefined ? 'nothing (no run store is wired)' : describeRecord(record)}, ` +\n (lastExistence === 'unknown'\n ? `and its journal (${options.paths.journal}) could not be probed at all within ${waitMs}ms — every '${journalExistsCommand(options.paths)}' failed. ` +\n `Attaching anyway would create that journal and tail it forever, so this fails instead. Check that the sandbox is still alive and that its exec transport works.`\n : `but its journal (${options.paths.journal}) did not appear within ${waitMs}ms. ` +\n `Either the driver died before writing its first line, or the journal directory does not match the one the agent was started with.`),\n )\n }\n // The consumer gave up (client gone, lease lost). Returning hands control\n // back to the reader, whose own AbortSignal handling ends the read — a\n // caller's abort is not a diagnosis about the run.\n if (options.signal?.aborted) return\n await sleep(Math.min(probeIntervalMs, remaining), options.signal)\n lastExistence = await probeJournal(handle, options)\n if (lastExistence === 'yes') return\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwFA,IAAa,iCAAiC;;;;;;AAO9C,IAAa,mCAAmC;;;;;;;;;;AAmChD,IAAa,gCAAb,cAAmD,MAAM;CAE5C;CACA;CAFX,YACE,OACA,QACA,QACA;EACA,MAAM,wBAAwB,MAAM,IAAI,QAAQ;EAJvC,KAAA,QAAA;EACA,KAAA,SAAA;EAIT,KAAK,OAAO;CACd;AACF;;;;;;AAgCA,eAAe,aACb,QACA,SAC2B;CAC3B,IAAI;EAIF,QAAO,MAHc,OAAO,QAAQ,KAClC,qBAAqB,QAAQ,KAAK,CACpC,EAAA,CACc,aAAa,IAAI,QAAQ;CACzC,SAAS,OAAO;EACd,QAAQ,QAAQ,SACd,4DAA4D,QAAQ,MAAM,kEAC1E;GAAE,OAAO,QAAQ;GAAO;EAAM,CAChC;EACA,OAAO;CACT;AACF;;;;;;AAOA,eAAe,WACb,SACuC;CACvC,IAAI,QAAQ,SAAS,KAAA,GAAW,OAAO,KAAA;CACvC,IAAI;EACF,OAAO,MAAM,QAAQ,KAAK,IAAI,QAAQ,KAAK;CAC7C,SAAS,OAAO;EACd,QAAQ,QAAQ,OACd,2DAA2D,QAAQ,SACnE;GAAE,OAAO,QAAQ;GAAO;EAAM,CAChC;EACA;CACF;AACF;AAEA,SAAS,MAAM,IAAY,QAAgD;CACzE,IAAI,MAAM,GAAG,OAAO,QAAQ,QAAQ;CACpC,OAAO,IAAI,SAAe,YAAY;EACpC,MAAM,QAAQ,WAAW,QAAQ,EAAE;EACnC,SAAS,SAAe;GACtB,aAAa,KAAK;GAClB,QAAQ,oBAAoB,SAAS,MAAM;GAC3C,QAAQ;EACV;EACA,QAAQ,iBAAiB,SAAS,QAAQ,EAAE,MAAM,KAAK,CAAC;CAC1D,CAAC;AACH;AAEA,SAAS,eAAe,QAA2B;CACjD,OAAO,OAAO,kBAAkB,KAAA,IAC5B,WAAW,OAAO,OAAO,4BACzB,WAAW,OAAO,OAAO,oBAAoB,IAAI,KAAK,OAAO,aAAa,CAAC,CAAC,YAAY;AAC9F;;;;;;;;;AAUA,eAAsB,uBACpB,QACA,SACe;CACf,MAAM,YAAY,MAAM,aAAa,QAAQ,OAAO;CACpD,IAAI,cAAc,OAAO;CAEzB,MAAM,SAAS,MAAM,WAAW,OAAO;CACvC,IAAI,WAAW,MACb,MAAM,IAAI,8BACR,QAAQ,OACR,eACA,yCAAyC,QAAQ,MAAM,QAAQ,kLAEjE;CAEF,IAAI,WAAW,KAAA,KAAa,oBAAoB,OAAO,MAAM,GAC3D,MAAM,IAAI,8BACR,QAAQ,OACR,gBACA,uBAAuB,OAAO,OAAO,qBAAqB,QAAQ,MAAM,QAAQ,qKAElF;CAMF,MAAM,SAAS,QAAQ,UAAA;CACvB,MAAM,kBACJ,QAAQ,mBAAA;CACV,MAAM,WAAW,KAAK,IAAI,IAAI;CAC9B,IAAI,gBAAkC;CACtC,SAAS;EACP,MAAM,YAAY,WAAW,KAAK,IAAI;EACtC,IAAI,aAAa,GACf,MAAM,IAAI,8BACR,QAAQ,OACR,mBACA,uBAAuB,WAAW,KAAA,IAAY,oCAAoC,eAAe,MAAM,EAAE,OACtG,kBAAkB,YACf,oBAAoB,QAAQ,MAAM,QAAQ,sCAAsC,OAAO,cAAc,qBAAqB,QAAQ,KAAK,EAAE,6KAEzI,oBAAoB,QAAQ,MAAM,QAAQ,0BAA0B,OAAO,uIAEnF;EAKF,IAAI,QAAQ,QAAQ,SAAS;EAC7B,MAAM,MAAM,KAAK,IAAI,iBAAiB,SAAS,GAAG,QAAQ,MAAM;EAChE,gBAAgB,MAAM,aAAa,QAAQ,OAAO;EAClD,IAAI,kBAAkB,OAAO;CAC/B;AACF"}
@@ -1,124 +1,110 @@
1
+ import { resolveAllSecrets, resolveSecret } from "./secrets.js";
1
2
  import { buildSetupPlan } from "./setup-plan.js";
2
3
  import { createBootstrapShell } from "./shell.js";
3
- import { resolveGitSkillDir, mergeAgentsContent, writeAgentsFile } from "./agents-file.js";
4
- import { resolveAllSecrets, resolveSecret } from "./secrets.js";
5
- const LOCKFILES = {
6
- pnpm: "pnpm-lock.yaml",
7
- yarn: "yarn.lock",
8
- bun: "bun.lockb",
9
- npm: "package-lock.json"
4
+ import { mergeAgentsContent, resolveGitSkillDir, writeAgentsFile } from "./agents-file.js";
5
+ //#region src/bootstrap.ts
6
+ /**
7
+ * Workspace bootstrap engine — provider-agnostic because it only uses the
8
+ * {@link SandboxHandle} contract. Runs once when a sandbox is freshly created
9
+ * (or restored without its working tree): land the source, inject secrets,
10
+ * detect the package manager, and run setup commands.
11
+ *
12
+ * Harness-specific projection (CLAUDE.md, agent skills, MCP config) is NOT done
13
+ * here — that's each adapter's `projectWorkspace()` hook, since the format
14
+ * differs per harness.
15
+ */
16
+ var LOCKFILES = {
17
+ pnpm: "pnpm-lock.yaml",
18
+ yarn: "yarn.lock",
19
+ bun: "bun.lockb",
20
+ npm: "package-lock.json"
10
21
  };
11
- const DEFAULT_WORKSPACE_ROOT = "/workspace";
22
+ var DEFAULT_WORKSPACE_ROOT = "/workspace";
23
+ /** Resolve the package manager, detecting from a lockfile when `'auto'`. */
12
24
  async function detectPackageManager(handle, workspace, root) {
13
- const pm = workspace.packageManager ?? "auto";
14
- if (pm !== "auto") return pm;
15
- for (const [manager, lockfile] of Object.entries(LOCKFILES)) {
16
- if (await handle.fs.exists(`${root}/${lockfile}`)) return manager;
17
- }
18
- return void 0;
25
+ const pm = workspace.packageManager ?? "auto";
26
+ if (pm !== "auto") return pm;
27
+ for (const [manager, lockfile] of Object.entries(LOCKFILES)) if (await handle.fs.exists(`${root}/${lockfile}`)) return manager;
19
28
  }
29
+ /**
30
+ * Bootstrap a freshly created sandbox's workspace. Idempotent enough to be safe
31
+ * on restore: a git clone into a populated dir is skipped by checking for the
32
+ * target dir first.
33
+ */
20
34
  async function bootstrapWorkspace(handle, workspace, options = {}) {
21
- const root = workspace.root ?? DEFAULT_WORKSPACE_ROOT;
22
- if (workspace.secrets !== void 0) {
23
- const resolved = resolveAllSecrets(workspace.secrets);
24
- if (Object.keys(resolved).length > 0) {
25
- await handle.env.set(resolved);
26
- }
27
- }
28
- if (workspace.source.type === "git") {
29
- const alreadyCloned = await handle.fs.exists(`${root}/.git`);
30
- if (!alreadyCloned) {
31
- await handle.git.clone({
32
- url: workspace.source.url,
33
- ref: workspace.source.ref,
34
- auth: workspace.source.auth,
35
- ...workspace.source.depth !== void 0 ? { depth: workspace.source.depth } : {}
36
- });
37
- }
38
- }
39
- const skills = workspace.skills ?? [];
40
- for (const skill of skills) {
41
- if (skill.kind === "git") {
42
- const url = skill.repo.startsWith("http") ? skill.repo : `https://github.com/${skill.repo}.git`;
43
- const dir = skill.into ?? resolveGitSkillDir(root, skill);
44
- const auth = skill.secret !== void 0 && workspace.secrets !== void 0 ? { token: resolveSecret(workspace.secrets, skill.secret) } : void 0;
45
- await handle.git.clone({
46
- url,
47
- dir,
48
- ...auth !== void 0 ? { auth } : {},
49
- depth: 1
50
- });
51
- }
52
- }
53
- let agentsContent;
54
- if (workspace.instructions !== void 0 && workspace.instructions.length > 0) {
55
- agentsContent = workspace.instructions;
56
- } else {
57
- const agentsFileSkill = skills.find(
58
- (s) => s.kind === "file" && s.path === "AGENTS.md"
59
- );
60
- if (agentsFileSkill !== void 0) {
61
- agentsContent = agentsFileSkill.content;
62
- }
63
- }
64
- agentsContent = mergeAgentsContent(agentsContent, workspace.scripts);
65
- if (agentsContent !== void 0) {
66
- await writeAgentsFile(handle, root, agentsContent);
67
- }
68
- for (const skill of skills) {
69
- if (skill.kind === "file" && skill.path !== "AGENTS.md") {
70
- await handle.fs.write(`${root}/${skill.path}`, skill.content);
71
- }
72
- }
73
- const packageManager = await detectPackageManager(handle, workspace, root);
74
- const ranSetup = [];
75
- const plan = buildSetupPlan(workspace.setup);
76
- if (plan.length > 0) {
77
- const shell = await createBootstrapShell(handle, { cwd: root });
78
- try {
79
- for (const group of plan) {
80
- if (group.kind === "serial") {
81
- const result = await shell.run(group.command);
82
- if (result.exitCode !== 0) {
83
- const tail = result.stdout.trim().slice(-1500);
84
- throw new Error(
85
- `setup step failed: ${group.command} (exit ${result.exitCode})${tail ? `
86
- ${tail}` : ""}`
87
- );
88
- }
89
- ranSetup.push(group.command);
90
- } else {
91
- const { cwd, env } = await shell.forkState();
92
- const results = await Promise.all(
93
- group.commands.map(
94
- (command) => handle.process.exec(command, {
95
- cwd,
96
- env,
97
- ...options.signal ? { signal: options.signal } : {}
98
- }).then((res) => ({ command, res }))
99
- )
100
- );
101
- const failed = results.find((entry) => entry.res.exitCode !== 0);
102
- if (failed !== void 0) {
103
- const tail = `${failed.res.stdout}
104
- ${failed.res.stderr}`.trim().slice(-1500);
105
- throw new Error(
106
- `setup step failed: ${failed.command} (exit ${failed.res.exitCode})${tail ? `
107
- ${tail}` : ""}`
108
- );
109
- }
110
- ranSetup.push(...group.commands);
111
- }
112
- }
113
- } finally {
114
- await shell.dispose();
115
- }
116
- }
117
- return { packageManager, ranSetup };
35
+ const root = workspace.root ?? "/workspace";
36
+ if (workspace.secrets !== void 0) {
37
+ const resolved = resolveAllSecrets(workspace.secrets);
38
+ if (Object.keys(resolved).length > 0) await handle.env.set(resolved);
39
+ }
40
+ if (workspace.source.type === "git") {
41
+ if (!await handle.fs.exists(`${root}/.git`)) await handle.git.clone({
42
+ url: workspace.source.url,
43
+ ref: workspace.source.ref,
44
+ auth: workspace.source.auth,
45
+ ...workspace.source.depth !== void 0 ? { depth: workspace.source.depth } : {}
46
+ });
47
+ }
48
+ const skills = workspace.skills ?? [];
49
+ for (const skill of skills) if (skill.kind === "git") {
50
+ const url = skill.repo.startsWith("http") ? skill.repo : `https://github.com/${skill.repo}.git`;
51
+ const dir = skill.into ?? resolveGitSkillDir(root, skill);
52
+ const auth = skill.secret !== void 0 && workspace.secrets !== void 0 ? { token: resolveSecret(workspace.secrets, skill.secret) } : void 0;
53
+ await handle.git.clone({
54
+ url,
55
+ dir,
56
+ ...auth !== void 0 ? { auth } : {},
57
+ depth: 1
58
+ });
59
+ }
60
+ let agentsContent;
61
+ if (workspace.instructions !== void 0 && workspace.instructions.length > 0) agentsContent = workspace.instructions;
62
+ else {
63
+ const agentsFileSkill = skills.find((s) => s.kind === "file" && s.path === "AGENTS.md");
64
+ if (agentsFileSkill !== void 0) agentsContent = agentsFileSkill.content;
65
+ }
66
+ agentsContent = mergeAgentsContent(agentsContent, workspace.scripts);
67
+ if (agentsContent !== void 0) await writeAgentsFile(handle, root, agentsContent);
68
+ for (const skill of skills) if (skill.kind === "file" && skill.path !== "AGENTS.md") await handle.fs.write(`${root}/${skill.path}`, skill.content);
69
+ const packageManager = await detectPackageManager(handle, workspace, root);
70
+ const ranSetup = [];
71
+ const plan = buildSetupPlan(workspace.setup);
72
+ if (plan.length > 0) {
73
+ const shell = await createBootstrapShell(handle, { cwd: root });
74
+ try {
75
+ for (const group of plan) if (group.kind === "serial") {
76
+ const result = await shell.run(group.command);
77
+ if (result.exitCode !== 0) {
78
+ const tail = result.stdout.trim().slice(-1500);
79
+ throw new Error(`setup step failed: ${group.command} (exit ${result.exitCode})${tail ? `\n${tail}` : ""}`);
80
+ }
81
+ ranSetup.push(group.command);
82
+ } else {
83
+ const { cwd, env } = await shell.forkState();
84
+ const failed = (await Promise.all(group.commands.map((command) => handle.process.exec(command, {
85
+ cwd,
86
+ env,
87
+ ...options.signal ? { signal: options.signal } : {}
88
+ }).then((res) => ({
89
+ command,
90
+ res
91
+ }))))).find((entry) => entry.res.exitCode !== 0);
92
+ if (failed !== void 0) {
93
+ const tail = `${failed.res.stdout}\n${failed.res.stderr}`.trim().slice(-1500);
94
+ throw new Error(`setup step failed: ${failed.command} (exit ${failed.res.exitCode})${tail ? `\n${tail}` : ""}`);
95
+ }
96
+ ranSetup.push(...group.commands);
97
+ }
98
+ } finally {
99
+ await shell.dispose();
100
+ }
101
+ }
102
+ return {
103
+ packageManager,
104
+ ranSetup
105
+ };
118
106
  }
119
- export {
120
- DEFAULT_WORKSPACE_ROOT,
121
- bootstrapWorkspace,
122
- detectPackageManager
123
- };
124
- //# sourceMappingURL=bootstrap.js.map
107
+ //#endregion
108
+ export { DEFAULT_WORKSPACE_ROOT, bootstrapWorkspace, detectPackageManager };
109
+
110
+ //# sourceMappingURL=bootstrap.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"bootstrap.js","sources":["../../src/bootstrap.ts"],"sourcesContent":["/**\n * Workspace bootstrap engine — provider-agnostic because it only uses the\n * {@link SandboxHandle} contract. Runs once when a sandbox is freshly created\n * (or restored without its working tree): land the source, inject secrets,\n * detect the package manager, and run setup commands.\n *\n * Harness-specific projection (CLAUDE.md, agent skills, MCP config) is NOT done\n * here — that's each adapter's `projectWorkspace()` hook, since the format\n * differs per harness.\n */\nimport { buildSetupPlan } from './setup-plan'\nimport { createBootstrapShell } from './shell'\nimport {\n mergeAgentsContent,\n resolveGitSkillDir,\n writeAgentsFile,\n} from './agents-file'\nimport { resolveAllSecrets, resolveSecret } from './secrets'\nimport type { SandboxHandle } from './contracts'\nimport type { PackageManager, WorkspaceDefinition } from './workspace'\n\nconst LOCKFILES: Record<Exclude<PackageManager, 'auto'>, string> = {\n pnpm: 'pnpm-lock.yaml',\n yarn: 'yarn.lock',\n bun: 'bun.lockb',\n npm: 'package-lock.json',\n}\n\nexport const DEFAULT_WORKSPACE_ROOT = '/workspace'\n\n/** Resolve the package manager, detecting from a lockfile when `'auto'`. */\nexport async function detectPackageManager(\n handle: SandboxHandle,\n workspace: WorkspaceDefinition,\n root: string,\n): Promise<Exclude<PackageManager, 'auto'> | undefined> {\n const pm = workspace.packageManager ?? 'auto'\n if (pm !== 'auto') return pm\n for (const [manager, lockfile] of Object.entries(LOCKFILES) as Array<\n [Exclude<PackageManager, 'auto'>, string]\n >) {\n if (await handle.fs.exists(`${root}/${lockfile}`)) return manager\n }\n return undefined\n}\n\nexport interface BootstrapResult {\n packageManager?: Exclude<PackageManager, 'auto'>\n ranSetup: Array<string>\n}\n\n/**\n * Bootstrap a freshly created sandbox's workspace. Idempotent enough to be safe\n * on restore: a git clone into a populated dir is skipped by checking for the\n * target dir first.\n */\nexport async function bootstrapWorkspace(\n handle: SandboxHandle,\n workspace: WorkspaceDefinition,\n options: { signal?: AbortSignal } = {},\n): Promise<BootstrapResult> {\n const root = workspace.root ?? DEFAULT_WORKSPACE_ROOT\n\n // Secrets live only in the running sandbox env (never persisted).\n if (workspace.secrets !== undefined) {\n const resolved = resolveAllSecrets(workspace.secrets)\n if (Object.keys(resolved).length > 0) {\n await handle.env.set(resolved)\n }\n }\n\n // Land the source. Clone into the handle's own default root (each provider\n // maps the conventional `/workspace` virtual root to its real backing dir),\n // rather than passing a virtual `dir` that can't be remapped inside a shell\n // command string.\n if (workspace.source.type === 'git') {\n const alreadyCloned = await handle.fs.exists(`${root}/.git`)\n if (!alreadyCloned) {\n await handle.git.clone({\n url: workspace.source.url,\n ref: workspace.source.ref,\n auth: workspace.source.auth,\n ...(workspace.source.depth !== undefined\n ? { depth: workspace.source.depth }\n : {}),\n })\n }\n }\n // 'local' is provider-pre-populated at create; 'none' starts empty.\n\n // Clone git-skill repos so setup steps (and the harness projector) can use\n // them. gitSkill clones are always shallow (depth 1) unless the skill's own\n // repo entry carries a depth override — the WorkspaceSkill `git` variant\n // does not expose one, so depth always defaults to 1 inside git.clone.\n const skills = workspace.skills ?? []\n for (const skill of skills) {\n if (skill.kind === 'git') {\n const url = skill.repo.startsWith('http')\n ? skill.repo\n : `https://github.com/${skill.repo}.git`\n const dir = skill.into ?? resolveGitSkillDir(root, skill)\n const auth =\n skill.secret !== undefined && workspace.secrets !== undefined\n ? { token: resolveSecret(workspace.secrets, skill.secret) }\n : undefined\n await handle.git.clone({\n url,\n dir,\n ...(auth !== undefined ? { auth } : {}),\n depth: 1,\n })\n }\n }\n\n // Write AGENTS.md (and its per-CLI symlinks) when instructions are provided\n // directly on the workspace, via a fileSkill whose path is `AGENTS.md`, or\n // when named workspace scripts should be surfaced for the agent.\n let agentsContent: string | undefined\n if (\n workspace.instructions !== undefined &&\n workspace.instructions.length > 0\n ) {\n agentsContent = workspace.instructions\n } else {\n const agentsFileSkill = skills.find(\n (s): s is Extract<typeof s, { kind: 'file' }> =>\n s.kind === 'file' && s.path === 'AGENTS.md',\n )\n if (agentsFileSkill !== undefined) {\n agentsContent = agentsFileSkill.content\n }\n }\n agentsContent = mergeAgentsContent(agentsContent, workspace.scripts)\n if (agentsContent !== undefined) {\n await writeAgentsFile(handle, root, agentsContent)\n }\n\n // Write all other fileSkills directly into the workspace root.\n for (const skill of skills) {\n if (skill.kind === 'file' && skill.path !== 'AGENTS.md') {\n await handle.fs.write(`${root}/${skill.path}`, skill.content)\n }\n }\n\n const packageManager = await detectPackageManager(handle, workspace, root)\n\n // Run setup over a single persistent shell so `cd`/exports persist across\n // serial steps. Parallel groups fork the shell's current cwd+env into\n // concurrent one-shot exec calls.\n const ranSetup: Array<string> = []\n const plan = buildSetupPlan(workspace.setup)\n if (plan.length > 0) {\n const shell = await createBootstrapShell(handle, { cwd: root })\n try {\n for (const group of plan) {\n if (group.kind === 'serial') {\n const result = await shell.run(group.command)\n if (result.exitCode !== 0) {\n const tail = result.stdout.trim().slice(-1500)\n throw new Error(\n `setup step failed: ${group.command} (exit ${result.exitCode})${tail ? `\\n${tail}` : ''}`,\n )\n }\n ranSetup.push(group.command)\n } else {\n const { cwd, env } = await shell.forkState()\n const results = await Promise.all(\n group.commands.map((command) =>\n handle.process\n .exec(command, {\n cwd,\n env,\n ...(options.signal ? { signal: options.signal } : {}),\n })\n .then((res) => ({ command, res })),\n ),\n )\n const failed = results.find((entry) => entry.res.exitCode !== 0)\n if (failed !== undefined) {\n const tail = `${failed.res.stdout}\\n${failed.res.stderr}`\n .trim()\n .slice(-1500)\n throw new Error(\n `setup step failed: ${failed.command} (exit ${failed.res.exitCode})${tail ? `\\n${tail}` : ''}`,\n )\n }\n ranSetup.push(...group.commands)\n }\n }\n } finally {\n await shell.dispose()\n }\n }\n\n return { packageManager, ranSetup }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"bootstrap.js","names":[],"sources":["../../src/bootstrap.ts"],"sourcesContent":["/**\n * Workspace bootstrap engine — provider-agnostic because it only uses the\n * {@link SandboxHandle} contract. Runs once when a sandbox is freshly created\n * (or restored without its working tree): land the source, inject secrets,\n * detect the package manager, and run setup commands.\n *\n * Harness-specific projection (CLAUDE.md, agent skills, MCP config) is NOT done\n * here — that's each adapter's `projectWorkspace()` hook, since the format\n * differs per harness.\n */\nimport { buildSetupPlan } from './setup-plan'\nimport { createBootstrapShell } from './shell'\nimport {\n mergeAgentsContent,\n resolveGitSkillDir,\n writeAgentsFile,\n} from './agents-file'\nimport { resolveAllSecrets, resolveSecret } from './secrets'\nimport type { SandboxHandle } from './contracts'\nimport type { PackageManager, WorkspaceDefinition } from './workspace'\n\nconst LOCKFILES: Record<Exclude<PackageManager, 'auto'>, string> = {\n pnpm: 'pnpm-lock.yaml',\n yarn: 'yarn.lock',\n bun: 'bun.lockb',\n npm: 'package-lock.json',\n}\n\nexport const DEFAULT_WORKSPACE_ROOT = '/workspace'\n\n/** Resolve the package manager, detecting from a lockfile when `'auto'`. */\nexport async function detectPackageManager(\n handle: SandboxHandle,\n workspace: WorkspaceDefinition,\n root: string,\n): Promise<Exclude<PackageManager, 'auto'> | undefined> {\n const pm = workspace.packageManager ?? 'auto'\n if (pm !== 'auto') return pm\n for (const [manager, lockfile] of Object.entries(LOCKFILES) as Array<\n [Exclude<PackageManager, 'auto'>, string]\n >) {\n if (await handle.fs.exists(`${root}/${lockfile}`)) return manager\n }\n return undefined\n}\n\nexport interface BootstrapResult {\n packageManager?: Exclude<PackageManager, 'auto'>\n ranSetup: Array<string>\n}\n\n/**\n * Bootstrap a freshly created sandbox's workspace. Idempotent enough to be safe\n * on restore: a git clone into a populated dir is skipped by checking for the\n * target dir first.\n */\nexport async function bootstrapWorkspace(\n handle: SandboxHandle,\n workspace: WorkspaceDefinition,\n options: { signal?: AbortSignal } = {},\n): Promise<BootstrapResult> {\n const root = workspace.root ?? DEFAULT_WORKSPACE_ROOT\n\n // Secrets live only in the running sandbox env (never persisted).\n if (workspace.secrets !== undefined) {\n const resolved = resolveAllSecrets(workspace.secrets)\n if (Object.keys(resolved).length > 0) {\n await handle.env.set(resolved)\n }\n }\n\n // Land the source. Clone into the handle's own default root (each provider\n // maps the conventional `/workspace` virtual root to its real backing dir),\n // rather than passing a virtual `dir` that can't be remapped inside a shell\n // command string.\n if (workspace.source.type === 'git') {\n const alreadyCloned = await handle.fs.exists(`${root}/.git`)\n if (!alreadyCloned) {\n await handle.git.clone({\n url: workspace.source.url,\n ref: workspace.source.ref,\n auth: workspace.source.auth,\n ...(workspace.source.depth !== undefined\n ? { depth: workspace.source.depth }\n : {}),\n })\n }\n }\n // 'local' is provider-pre-populated at create; 'none' starts empty.\n\n // Clone git-skill repos so setup steps (and the harness projector) can use\n // them. gitSkill clones are always shallow (depth 1) unless the skill's own\n // repo entry carries a depth override — the WorkspaceSkill `git` variant\n // does not expose one, so depth always defaults to 1 inside git.clone.\n const skills = workspace.skills ?? []\n for (const skill of skills) {\n if (skill.kind === 'git') {\n const url = skill.repo.startsWith('http')\n ? skill.repo\n : `https://github.com/${skill.repo}.git`\n const dir = skill.into ?? resolveGitSkillDir(root, skill)\n const auth =\n skill.secret !== undefined && workspace.secrets !== undefined\n ? { token: resolveSecret(workspace.secrets, skill.secret) }\n : undefined\n await handle.git.clone({\n url,\n dir,\n ...(auth !== undefined ? { auth } : {}),\n depth: 1,\n })\n }\n }\n\n // Write AGENTS.md (and its per-CLI symlinks) when instructions are provided\n // directly on the workspace, via a fileSkill whose path is `AGENTS.md`, or\n // when named workspace scripts should be surfaced for the agent.\n let agentsContent: string | undefined\n if (\n workspace.instructions !== undefined &&\n workspace.instructions.length > 0\n ) {\n agentsContent = workspace.instructions\n } else {\n const agentsFileSkill = skills.find(\n (s): s is Extract<typeof s, { kind: 'file' }> =>\n s.kind === 'file' && s.path === 'AGENTS.md',\n )\n if (agentsFileSkill !== undefined) {\n agentsContent = agentsFileSkill.content\n }\n }\n agentsContent = mergeAgentsContent(agentsContent, workspace.scripts)\n if (agentsContent !== undefined) {\n await writeAgentsFile(handle, root, agentsContent)\n }\n\n // Write all other fileSkills directly into the workspace root.\n for (const skill of skills) {\n if (skill.kind === 'file' && skill.path !== 'AGENTS.md') {\n await handle.fs.write(`${root}/${skill.path}`, skill.content)\n }\n }\n\n const packageManager = await detectPackageManager(handle, workspace, root)\n\n // Run setup over a single persistent shell so `cd`/exports persist across\n // serial steps. Parallel groups fork the shell's current cwd+env into\n // concurrent one-shot exec calls.\n const ranSetup: Array<string> = []\n const plan = buildSetupPlan(workspace.setup)\n if (plan.length > 0) {\n const shell = await createBootstrapShell(handle, { cwd: root })\n try {\n for (const group of plan) {\n if (group.kind === 'serial') {\n const result = await shell.run(group.command)\n if (result.exitCode !== 0) {\n const tail = result.stdout.trim().slice(-1500)\n throw new Error(\n `setup step failed: ${group.command} (exit ${result.exitCode})${tail ? `\\n${tail}` : ''}`,\n )\n }\n ranSetup.push(group.command)\n } else {\n const { cwd, env } = await shell.forkState()\n const results = await Promise.all(\n group.commands.map((command) =>\n handle.process\n .exec(command, {\n cwd,\n env,\n ...(options.signal ? { signal: options.signal } : {}),\n })\n .then((res) => ({ command, res })),\n ),\n )\n const failed = results.find((entry) => entry.res.exitCode !== 0)\n if (failed !== undefined) {\n const tail = `${failed.res.stdout}\\n${failed.res.stderr}`\n .trim()\n .slice(-1500)\n throw new Error(\n `setup step failed: ${failed.command} (exit ${failed.res.exitCode})${tail ? `\\n${tail}` : ''}`,\n )\n }\n ranSetup.push(...group.commands)\n }\n }\n } finally {\n await shell.dispose()\n }\n }\n\n return { packageManager, ranSetup }\n}\n"],"mappings":"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"}