@dynamicagents/plugins 0.11.0 → 0.12.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 (95) hide show
  1. package/README.md +12 -13
  2. package/dist/arc-agi/analysis.d.ts +16 -30
  3. package/dist/arc-agi/analysis.js +20 -47
  4. package/dist/arc-agi/client.d.ts +3 -2
  5. package/dist/arc-agi/game-tools.d.ts +5 -6
  6. package/dist/arc-agi/index.d.ts +6 -8
  7. package/dist/arc-agi/index.js +3 -3
  8. package/dist/arc-agi/main-agent.d.ts +5 -8
  9. package/dist/arc-agi/main-agent.js +5 -8
  10. package/dist/arc-agi/recipe.d.ts +17 -20
  11. package/dist/arc-agi/recipe.js +17 -20
  12. package/dist/arc-agi/soul.d.ts +16 -27
  13. package/dist/arc-agi/soul.js +16 -27
  14. package/dist/arc-agi/store.d.ts +4 -5
  15. package/dist/arc-agi/store.js +4 -5
  16. package/dist/arc-agi/tools.d.ts +1 -1
  17. package/dist/arc-agi/tools.js +26 -19
  18. package/dist/arc-agi/types.d.ts +6 -7
  19. package/dist/claude-code/config.d.ts +88 -11
  20. package/dist/claude-code/config.js +11 -2
  21. package/dist/claude-code/credentials.d.ts +21 -9
  22. package/dist/claude-code/credentials.js +43 -9
  23. package/dist/claude-code/egress.d.ts +7 -13
  24. package/dist/claude-code/egress.js +9 -10
  25. package/dist/claude-code/events.d.ts +47 -0
  26. package/dist/claude-code/events.js +96 -10
  27. package/dist/claude-code/index.d.ts +31 -13
  28. package/dist/claude-code/index.js +62 -15
  29. package/dist/claude-code/run.d.ts +135 -41
  30. package/dist/claude-code/run.js +366 -61
  31. package/dist/computer/advisory.d.ts +31 -18
  32. package/dist/computer/advisory.js +30 -5
  33. package/dist/computer/context.d.ts +23 -0
  34. package/dist/computer/context.js +225 -0
  35. package/dist/computer/file-lock.d.ts +5 -0
  36. package/dist/computer/file-lock.js +39 -0
  37. package/dist/computer/gate.d.ts +8 -8
  38. package/dist/computer/gate.js +7 -7
  39. package/dist/computer/host/ca-trust.d.ts +88 -0
  40. package/dist/computer/host/ca-trust.js +303 -0
  41. package/dist/computer/host/container-deps.d.ts +54 -0
  42. package/dist/computer/host/container-deps.js +224 -0
  43. package/dist/computer/host/container-fault.d.ts +38 -0
  44. package/dist/computer/host/container-fault.js +58 -0
  45. package/dist/computer/host/git-host.d.ts +62 -0
  46. package/dist/computer/host/git-host.js +156 -0
  47. package/dist/computer/host/git-identity.d.ts +57 -0
  48. package/dist/computer/host/git-identity.js +147 -0
  49. package/dist/computer/host/install-job.d.ts +121 -0
  50. package/dist/computer/host/install-job.js +766 -0
  51. package/dist/computer/host/sync.d.ts +111 -0
  52. package/dist/computer/host/sync.js +124 -0
  53. package/dist/computer/host/wake.d.ts +19 -0
  54. package/dist/computer/host/wake.js +10 -0
  55. package/dist/computer/host/workspace.d.ts +424 -0
  56. package/dist/computer/host/workspace.js +1258 -0
  57. package/dist/computer/index.d.ts +70 -50
  58. package/dist/computer/index.js +79 -767
  59. package/dist/computer/install.d.ts +8 -12
  60. package/dist/computer/install.js +6 -9
  61. package/dist/computer/paths.d.ts +21 -16
  62. package/dist/computer/paths.js +40 -38
  63. package/dist/computer/read.d.ts +18 -22
  64. package/dist/computer/read.js +24 -27
  65. package/dist/computer/render.d.ts +28 -0
  66. package/dist/computer/render.js +34 -1
  67. package/dist/computer/shell.d.ts +45 -0
  68. package/dist/computer/shell.js +80 -0
  69. package/dist/computer/tools-exec.d.ts +3 -0
  70. package/dist/computer/tools-exec.js +269 -0
  71. package/dist/computer/tools-file.d.ts +4 -0
  72. package/dist/computer/tools-file.js +124 -0
  73. package/dist/computer/tools-find.d.ts +3 -0
  74. package/dist/computer/tools-find.js +216 -0
  75. package/dist/recall/index.d.ts +9 -3
  76. package/dist/recall/index.js +22 -12
  77. package/dist/repo/checkout.d.ts +34 -4
  78. package/dist/repo/checkout.js +36 -1
  79. package/dist/repo/context.d.ts +109 -0
  80. package/dist/repo/context.js +530 -0
  81. package/dist/repo/index.d.ts +6 -29
  82. package/dist/repo/index.js +17 -1027
  83. package/dist/repo/tools-clone.d.ts +4 -0
  84. package/dist/repo/tools-clone.js +192 -0
  85. package/dist/repo/tools-forge.d.ts +4 -0
  86. package/dist/repo/tools-forge.js +179 -0
  87. package/dist/repo/tools-review.d.ts +3 -0
  88. package/dist/repo/tools-review.js +373 -0
  89. package/dist/repo/tools-worktree.d.ts +4 -0
  90. package/dist/repo/tools-worktree.js +221 -0
  91. package/dist/triage/index.d.ts +6 -1
  92. package/dist/triage/index.js +32 -23
  93. package/dist/workspace/index.d.ts +3 -3
  94. package/dist/workspace/index.js +3 -3
  95. package/package.json +21 -15
@@ -9,21 +9,20 @@ import type { InstallState } from "./install.js";
9
9
  * and the generation marker. It is right for that and unchanged by this file.
10
10
  *
11
11
  * Every *reader*, though, is asking a different question: **can I rely on this
12
- * workspace right now?** The two diverge, and where they did the old code either
13
- * overloaded a variant or invented a record:
12
+ * workspace right now?** The two diverge, and expressing the second through the
13
+ * first goes wrong in ways that are each easy to reintroduce:
14
14
  *
15
- * - A workspace at its storage ceiling was written as `skipped` — the same
16
- * variant that means "this checkout has nothing to install". One is a hard
17
- * wall about the Durable Object, the other is routine and about the
18
- * repository, and they were told apart only by reading prose. Both readers
19
- * missed the first.
20
- * - Capacity therefore travelled through a channel gated by
21
- * {@link file://./gate.ts needsDependencies}, so a full workspace said nothing
15
+ * - A workspace at its storage ceiling is not `skipped`. That variant means
16
+ * "this checkout has nothing to install" — routine, and about the repository.
17
+ * A ceiling is a hard wall about the Durable Object, and the two are then told
18
+ * apart only by reading prose.
19
+ * - Capacity must not travel through a channel gated by
20
+ * {@link file://./gate.ts needsDependencies}, or a full workspace says nothing
22
21
  * at all to `echo hi > file.txt` — the command whose write is being lost.
23
- * - "The tree is fine now", after a subagent installed by hand, had no way to be
24
- * said except by fabricating a `done` with an invented exit code — and the
25
- * probe it rested on, a bare `test -d node_modules`, is satisfied by the
26
- * wreckage of the very install it was overriding.
22
+ * - "The tree is fine now", after a subagent installed by hand, has no honest
23
+ * spelling as a job record: fabricating a `done` rests on the presence of a
24
+ * `node_modules` directory, which the wreckage of the very install being
25
+ * overridden already satisfies.
27
26
  *
28
27
  * So an advisory is **derived at read time** and describes the workspace, not a
29
28
  * job. Its absence is the good case, which is what removes the need to invent
@@ -139,11 +138,11 @@ export interface AdvisoryInput {
139
138
  /**
140
139
  * Whether a `node_modules` directory exists — **existence only, not health**.
141
140
  *
142
- * Named for what it can actually observe. A host probes this with the
143
- * equivalent of `test -d`, and that answers a narrower question than
144
- * "are the dependencies fine": a `npm ci` that died partway leaves the
145
- * directory behind holding an incomplete tree, so a `true` here is entirely
146
- * consistent with the install having failed for real.
141
+ * Named for what it can actually observe. A host answers it by looking for the
142
+ * directory in the workspace, and that is a narrower question than "are the
143
+ * dependencies fine": an `npm ci` that died partway leaves the directory
144
+ * behind holding an incomplete tree, so a `true` here is entirely consistent
145
+ * with the install having failed for real.
147
146
  *
148
147
  * So it never suppresses a proven failure. It travels onto the advisory as
149
148
  * {@link WorkspaceAdvisory} `treePresent` and the reader is told which way the
@@ -152,6 +151,20 @@ export interface AdvisoryInput {
152
151
  * mode it was recording.
153
152
  */
154
153
  dependencyTreePresent: boolean;
154
+ /**
155
+ * A reinstall the host has already queued, as the moment it was queued.
156
+ *
157
+ * The difference between "your dependencies are broken" and "your
158
+ * dependencies are being rebuilt", and the two read identically from the
159
+ * durable record alone — an install whose container was replaced writes
160
+ * `failed`, and the arming that will repair it is a separate fact the record
161
+ * does not carry.
162
+ *
163
+ * A production session was briefed `deps-broken` six seconds before the
164
+ * armed reinstall it did not know about finished, and spent its own turns
165
+ * re-running `npm ci` on top of a tree that was already landing.
166
+ */
167
+ reinstallArmedAt?: number;
155
168
  }
156
169
  /**
157
170
  * Everything currently true about a workspace, from what the host can see.
@@ -129,9 +129,34 @@ export function deriveAdvisories(input) {
129
129
  });
130
130
  break;
131
131
  case "failed":
132
- // Always reported. The record is durable evidence that an install failed,
133
- // and nothing available here is evidence that a later one succeeded — so
134
- // the presence of a tree qualifies the advisory instead of deleting it.
132
+ /**
133
+ * A queued reinstall changes the *kind*, not the severity of the facts.
134
+ *
135
+ * `deps-broken` is permanent by definition — nothing clears that record
136
+ * except another checkout — and that permanence is what the reader acts
137
+ * on: a session told this installs for itself. When a reinstall is
138
+ * already armed that is the wrong instruction and the wrong shape, because
139
+ * the condition does clear with nobody acting. `deps-building` is the one
140
+ * that says so, and its wording for a session already asks the reader to
141
+ * wait rather than start a second install on top of the first.
142
+ *
143
+ * Counted from when the reinstall was armed rather than when it starts:
144
+ * arming is the moment the repair became certain, and the alarm that runs
145
+ * it owns no clock this can read.
146
+ */
147
+ if (input.reinstallArmedAt !== undefined) {
148
+ advisories.push({
149
+ kind: "deps-building",
150
+ command: install.command,
151
+ startedAt: input.reinstallArmedAt,
152
+ ...(install.tail ? { tail: install.tail } : {})
153
+ });
154
+ break;
155
+ }
156
+ // Otherwise always reported. The record is durable evidence that an
157
+ // install failed, and nothing available here is evidence that a later one
158
+ // succeeded — so the presence of a tree qualifies the advisory instead of
159
+ // deleting it.
135
160
  advisories.push({
136
161
  kind: "deps-broken",
137
162
  command: install.command,
@@ -147,8 +172,8 @@ export function deriveAdvisories(input) {
147
172
  advisories.push({ kind: "deps-absent", reason: install.reason });
148
173
  break;
149
174
  // Nothing to say. `idle` means no install has been armed yet, which the
150
- // host's cold-container check resolves on its own, and `done` is the
151
- // ordinary case — a caller told its dependencies are fine has been told
175
+ // host resolves on its own when it finds no tree in the workspace, and
176
+ // `done` is the ordinary case — a caller told its dependencies are fine has been told
152
177
  // nothing, at the cost of prefix tokens on every run.
153
178
  case "idle":
154
179
  case "done":
@@ -0,0 +1,23 @@
1
+ import type { WorkspaceClient } from "@cloudflare/computer";
2
+ import { type ExecGate } from "./gate.js";
3
+ import type { WorkspaceAdvisory } from "./advisory.js";
4
+ import type { ComputerConfig } from "./index.js";
5
+ /** A command that has not finished in this long is a hung command. */
6
+ export declare const DEFAULT_TIMEOUT_MS: number;
7
+ /** Where checkouts live, in the container and in the workspace alike. */
8
+ export declare const DEFAULT_CWD = "/workspace";
9
+ /** What {@link computerContext} hands each group of tools. */
10
+ export interface ComputerContext {
11
+ config: ComputerConfig;
12
+ cwd: string;
13
+ timeoutMs: number;
14
+ maxChars: number;
15
+ workspace: () => Promise<WorkspaceClient>;
16
+ awaitAdvisories: (command: string, signal?: AbortSignal) => Promise<ExecGate>;
17
+ definedEnv: () => Record<string, string> | undefined;
18
+ inWorkspace: (gerund: string, subject: string, body: (fs: WorkspaceClient["fs"]) => Promise<string>) => Promise<string>;
19
+ refuseWrite: () => Promise<string | undefined>;
20
+ scope: () => string;
21
+ }
22
+ /** Takes what {@link file://./index.ts buildComputerTools} takes. */
23
+ export declare function computerContext(workspace: () => Promise<WorkspaceClient>, config: ComputerConfig, advisories?: () => Promise<readonly WorkspaceAdvisory[]>, fsWorkspace?: () => Promise<WorkspaceClient>, lockScope?: () => string): ComputerContext;
@@ -0,0 +1,225 @@
1
+ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
2
+ if (value !== null && value !== void 0) {
3
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
4
+ var dispose, inner;
5
+ if (async) {
6
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
7
+ dispose = value[Symbol.asyncDispose];
8
+ }
9
+ if (dispose === void 0) {
10
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
11
+ dispose = value[Symbol.dispose];
12
+ if (async) inner = dispose;
13
+ }
14
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
15
+ if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
16
+ env.stack.push({ value: value, dispose: dispose, async: async });
17
+ }
18
+ else if (async) {
19
+ env.stack.push({ async: true });
20
+ }
21
+ return value;
22
+ };
23
+ var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
24
+ return function (env) {
25
+ function fail(e) {
26
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
27
+ env.hasError = true;
28
+ }
29
+ var r, s = 0;
30
+ function next() {
31
+ while (r = env.stack.pop()) {
32
+ try {
33
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
34
+ if (r.dispose) {
35
+ var result = r.dispose.call(r.value);
36
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
37
+ }
38
+ else s |= 1;
39
+ }
40
+ catch (e) {
41
+ fail(e);
42
+ }
43
+ }
44
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
45
+ if (env.hasError) throw env.error;
46
+ }
47
+ return next();
48
+ };
49
+ })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
50
+ var e = new Error(message);
51
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
+ });
53
+ import { withAbort } from "@dynamicagents/core";
54
+ import { execGate, writeGate } from "./gate.js";
55
+ /**
56
+ * The machinery every container tool closes over: the resolved config, the
57
+ * advisory gates, and the workspace opener the file tools share.
58
+ */
59
+ /**
60
+ * How much command output the model is allowed to see, in characters.
61
+ *
62
+ * One `npm install` prints more than a small context window holds, and the
63
+ * interesting part of a failing build is the first error and the last summary —
64
+ * never the middle. So output is truncated from the middle rather than the end,
65
+ * which is what a naive `slice` would do and would drop the exit summary that
66
+ * says what actually failed.
67
+ */
68
+ const DEFAULT_MAX_OUTPUT_CHARS = 16_000;
69
+ /** A command that has not finished in this long is a hung command. */
70
+ export const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000;
71
+ /** Where checkouts live, in the container and in the workspace alike. */
72
+ export const DEFAULT_CWD = "/workspace";
73
+ /**
74
+ * How often the install gate re-reads the install's status.
75
+ *
76
+ * An interval rather than a subscription because the status lives in another
77
+ * Durable Object with no event to wait on. Never slept past the gate itself —
78
+ * see `awaitAdvisories` in {@link computerContext}.
79
+ */
80
+ const INSTALL_POLL_MS = 3_000;
81
+ /** Takes what {@link file://./index.ts buildComputerTools} takes. */
82
+ export function computerContext(workspace, config, advisories, fsWorkspace = workspace, lockScope) {
83
+ const cwd = config.cwd ?? DEFAULT_CWD;
84
+ const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
85
+ const maxChars = config.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
86
+ const gateMs = config.installGateMs ?? 90_000;
87
+ const env = config.env;
88
+ const ownScope = crypto.randomUUID();
89
+ const scope = () => lockScope?.() ?? ownScope;
90
+ /**
91
+ * Wait out anything transient, up to the gate, and report whatever is left.
92
+ *
93
+ * Polled rather than subscribed: the state lives in another Durable Object,
94
+ * there is no event to wait on, and the whole window is under two minutes.
95
+ *
96
+ * **Read for every command, including the ones that need no dependencies.**
97
+ * The filter that spares `cat README.md` from queueing behind an `npm ci`
98
+ * lives in `execGate`, where it applies per advisory — because a dependency
99
+ * install and a full workspace are not relevant to the same commands, and one
100
+ * filter over both leaves a workspace at its ceiling silent for exactly the
101
+ * commands whose writes it is dropping. The cost is one same-colo RPC per
102
+ * command, against a class of silent data loss.
103
+ */
104
+ const awaitAdvisories = async (command, signal) => {
105
+ if (!advisories)
106
+ return {};
107
+ // Fails **open**, and that belongs here rather than at the call site: this
108
+ // is a read of another Durable Object's state. An error must not take out a
109
+ // working shell — running the command is exactly what would have happened
110
+ // before the gate existed, and a spurious blockage would strand the
111
+ // subagent.
112
+ const read = async () => {
113
+ try {
114
+ signal?.throwIfAborted();
115
+ return await withAbort(signal, advisories());
116
+ }
117
+ catch (err) {
118
+ // Open on a failed read, never on a cancel: failing open here would go on
119
+ // to run the command the caller just gave up on.
120
+ if (signal?.aborted)
121
+ throw err;
122
+ return [];
123
+ }
124
+ };
125
+ // Clamped to whatever is left of the gate, not the bare interval. A fixed
126
+ // sleep makes `installGateMs` a floor rather than the ceiling it is
127
+ // documented to be: at a 100 ms gate the loop still slept the full three
128
+ // seconds before looking again, so a public timeout overshot by 30×.
129
+ const deadline = Date.now() + gateMs;
130
+ let current = await read();
131
+ // Keyed on the gate's own verdict rather than on a state name, so "what is
132
+ // worth waiting for" has one definition and it is the one that will decide
133
+ // the outcome.
134
+ while (execGate(current, command).block && Date.now() < deadline) {
135
+ const wait = Math.min(INSTALL_POLL_MS, deadline - Date.now());
136
+ // Throws on a cancel rather than returning the verdict so far: a gate that
137
+ // outlived its caller would otherwise go on to run the command it was holding.
138
+ await withAbort(signal, new Promise((resolve) => setTimeout(resolve, wait)));
139
+ current = await read();
140
+ }
141
+ return execGate(current, command);
142
+ };
143
+ /**
144
+ * Whether a write can succeed at all, asked once and not polled.
145
+ *
146
+ * Separate from {@link awaitAdvisories} because the question is different:
147
+ * there is no command to classify and nothing worth waiting for. An install in
148
+ * flight does not stop a file being written, and the one thing that does —
149
+ * a workspace that no longer accepts writes — never resolves on its own.
150
+ *
151
+ * Fails **open** for the same reason the exec gate does: a read of another
152
+ * Durable Object's state must not take out a working tool.
153
+ */
154
+ const refuseWrite = async () => {
155
+ if (!advisories)
156
+ return undefined;
157
+ try {
158
+ return writeGate(await advisories());
159
+ }
160
+ catch {
161
+ return undefined;
162
+ }
163
+ };
164
+ /**
165
+ * Host-supplied environment, with the undefined entries dropped.
166
+ *
167
+ * `RuntimeExecOptions.env` is `Record<string, string>`, and a config thunk
168
+ * that reads straight off `env` will hand back `undefined` for anything
169
+ * unset — which would otherwise arrive in the container as the string
170
+ * "undefined".
171
+ */
172
+ const definedEnv = () => {
173
+ if (!env)
174
+ return undefined;
175
+ const entries = Object.entries(env()).filter((entry) => entry[1] !== undefined);
176
+ return entries.length > 0 ? Object.fromEntries(entries) : undefined;
177
+ };
178
+ /**
179
+ * Open the workspace, run one file operation in it, and own the failure.
180
+ *
181
+ * Five lines every file tool would otherwise repeat with only the verb changed
182
+ * — five chances for the seventh to be written without them. The catch is the
183
+ * point: a failed file operation is the model's to recover from, and it can
184
+ * only recover from what it is told, so this returns the sentence rather than
185
+ * throwing it.
186
+ *
187
+ * `sb_exec` deliberately does not use it. That one recognises `EEXEC_LOST`,
188
+ * logs, and carries an install warning through the failure path — a bespoke
189
+ * catch saying something this cannot.
190
+ *
191
+ * It opens through `fsWorkspace`, which is what makes the capability sentence
192
+ * about these tools true: they answer while the container is down.
193
+ */
194
+ const inWorkspace = async (gerund, subject, body) => {
195
+ try {
196
+ const env_1 = { stack: [], error: void 0, hasError: false };
197
+ try {
198
+ const ws = __addDisposableResource(env_1, await fsWorkspace(), false);
199
+ return await body(ws.fs);
200
+ }
201
+ catch (e_1) {
202
+ env_1.error = e_1;
203
+ env_1.hasError = true;
204
+ }
205
+ finally {
206
+ __disposeResources(env_1);
207
+ }
208
+ }
209
+ catch (err) {
210
+ return `error ${gerund} ${subject}: ${String(err)}`;
211
+ }
212
+ };
213
+ return {
214
+ config,
215
+ cwd,
216
+ timeoutMs,
217
+ maxChars,
218
+ workspace,
219
+ awaitAdvisories,
220
+ definedEnv,
221
+ inWorkspace,
222
+ refuseWrite,
223
+ scope
224
+ };
225
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * `path` is canonicalised the way the filesystem resolves it, so spellings with
3
+ * `.`, `..` or doubled slashes share one lock.
4
+ */
5
+ export declare function withFileLock<T>(scope: string, path: string, body: () => Promise<T>): Promise<T>;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * One mutation per file at a time, across every tool set in this isolate.
3
+ *
4
+ * `sb_edit` reads, replaces and writes back over RPC, and the AI SDK runs one
5
+ * step's tool calls concurrently — so two edits to one file would both report
6
+ * success and the first would be lost. Module-level because a parent and a
7
+ * subagent can hold separate tool sets over one workspace; another isolate is
8
+ * out of reach.
9
+ */
10
+ const tails = new Map();
11
+ /**
12
+ * `path` is canonicalised the way the filesystem resolves it, so spellings with
13
+ * `.`, `..` or doubled slashes share one lock.
14
+ */
15
+ export async function withFileLock(scope, path, body) {
16
+ const key = `${scope}\0${canonical(path)}`;
17
+ const run = (tails.get(key) ?? Promise.resolve()).then(body);
18
+ const tail = run.catch(() => { });
19
+ tails.set(key, tail);
20
+ try {
21
+ return await run;
22
+ }
23
+ finally {
24
+ if (tails.get(key) === tail)
25
+ tails.delete(key);
26
+ }
27
+ }
28
+ function canonical(path) {
29
+ const out = [];
30
+ for (const segment of path.split("/")) {
31
+ if (segment === "" || segment === ".")
32
+ continue;
33
+ if (segment === "..")
34
+ out.pop();
35
+ else
36
+ out.push(segment);
37
+ }
38
+ return `/${out.join("/")}`;
39
+ }
@@ -18,10 +18,10 @@ import { type WorkspaceAdvisory } from "./advisory.js";
18
18
  * Unexplained, the model receives `Execution "…" was lost when its container
19
19
  * runtime was replaced`, reads it as a crash, and goes looking at its command.
20
20
  *
21
- * What it needs is three facts: nothing ran to completion, the checkout survived
22
- * because the filesystem is the Durable Object's rather than the container's, and
23
- * `node_modules` did not because it never was. So re-run, and expect the install
24
- * to be rebuilding underneath.
21
+ * What it needs is two facts: nothing ran to completion, and the workspace
22
+ * survived because the filesystem is the Durable Object's rather than the
23
+ * container's. So re-run it. `node_modules` is the container's, so the note
24
+ * says the replacement reinstalls it.
25
25
  *
26
26
  * Matched on `code` rather than the message — the property the package sets
27
27
  * deliberately, and the one that survives a reworded string.
@@ -52,10 +52,10 @@ export declare function execLostNote(err: unknown): string | undefined;
52
52
  * ## What a command is told depends on the command
53
53
  *
54
54
  * A dependency advisory reaches only commands that read `node_modules`; a
55
- * universal one reaches everything. That asymmetry is the reason a full
56
- * workspace used to say nothing at all to `echo hi > file.txt` — the write being
57
- * lost is usually not a dependency's, and the filter that was right for installs
58
- * was silently applied to capacity too.
55
+ * universal one reaches everything. Applying the dependency filter to capacity
56
+ * too is the mistake that asymmetry invites: a full workspace then says nothing
57
+ * at all to `echo hi > file.txt`, and the write being lost is usually not a
58
+ * dependency's.
59
59
  */
60
60
  export interface ExecGate {
61
61
  /** Set only for something transient: the command did not run. */
@@ -18,10 +18,10 @@ import { renderAdvisory, shapeOf } from "./advisory.js";
18
18
  * Unexplained, the model receives `Execution "…" was lost when its container
19
19
  * runtime was replaced`, reads it as a crash, and goes looking at its command.
20
20
  *
21
- * What it needs is three facts: nothing ran to completion, the checkout survived
22
- * because the filesystem is the Durable Object's rather than the container's, and
23
- * `node_modules` did not because it never was. So re-run, and expect the install
24
- * to be rebuilding underneath.
21
+ * What it needs is two facts: nothing ran to completion, and the workspace
22
+ * survived because the filesystem is the Durable Object's rather than the
23
+ * container's. So re-run it. `node_modules` is the container's, so the note
24
+ * says the replacement reinstalls it.
25
25
  *
26
26
  * Matched on `code` rather than the message — the property the package sets
27
27
  * deliberately, and the one that survives a reworded string.
@@ -31,9 +31,9 @@ export function execLostNote(err) {
31
31
  return undefined;
32
32
  return ("the container was replaced while this command was running, so it was lost — " +
33
33
  "nothing ran to completion and no output survived. This is infrastructure, " +
34
- "not your command: re-run it. The checkout is durable and is exactly as you " +
35
- "left it, but `node_modules` lived in the old container and is being rebuilt, " +
36
- "so anything that needs dependencies may have to wait for that install.");
34
+ "not your command: re-run it. The workspace is durable and is exactly as you " +
35
+ "left it. `node_modules` is not: the replacement reinstalls it, and a " +
36
+ "command that needs it waits for that.");
37
37
  }
38
38
  /** Which advisories this particular command needs to hear about. */
39
39
  function relevantTo(advisories, command) {
@@ -0,0 +1,88 @@
1
+ import type { Workspace } from "@cloudflare/computer";
2
+ /**
3
+ * Making a container able to speak TLS, once there is a CA to trust.
4
+ *
5
+ * Only `egress: { mode: "http-gateway" }` needs this, but it lives on every
6
+ * workspace because the cost of getting it wrong is asymmetric: the trust
7
+ * install against a container with no CA prints one line and exits 0, while a
8
+ * container that missed it fails every later HTTPS connection with an error
9
+ * that names no cause.
10
+ */
11
+ /**
12
+ * Install the egress interception CA, and say what was there to install.
13
+ *
14
+ * One shell string rather than a script in the image, because **the image cannot
15
+ * run this at all**, and the reason is ordering rather than preference.
16
+ * Cloudflare's own recipe assumes interception is declared as container
17
+ * configuration, so the runtime mounts the CA before the container starts.
18
+ * `@cloudflare/computer` uses the raw container API, where the only interception
19
+ * hooks are methods called *after* `start()` — so an entrypoint runs strictly
20
+ * before the CA can exist, finds nothing, and reports the container
21
+ * un-intercepted while every later TLS connection fails.
22
+ *
23
+ * The listing comes first and runs unconditionally: when the CA is missing,
24
+ * *what else is in that directory* is the only evidence distinguishing "mounted
25
+ * somewhere else" from "never provisioned", and it costs one line.
26
+ */
27
+ export declare const TRUST_CA_COMMAND: string;
28
+ export interface ContainerTrustDeps {
29
+ workspace: () => Workspace;
30
+ /** The runtime's own container handle, for the exit this has to hear about. */
31
+ container: () => Container | undefined;
32
+ /** Log prefix and object id, for the one line an operator can see. */
33
+ tag: () => string;
34
+ id: () => string;
35
+ /** Told when the watched container exits, for what else it described. */
36
+ onExit?: () => void;
37
+ }
38
+ /**
39
+ * Whether the container this object is talking to has had the CA installed.
40
+ *
41
+ * ## Why the answer is in memory
42
+ *
43
+ * What it describes is a *container*, which does not outlive the isolate in any
44
+ * way worth persisting. A stored `true` would be the exact bug this exists to
45
+ * prevent — a flag surviving the container it describes, and vouching for its
46
+ * replacement.
47
+ *
48
+ * It is wrong only in the safe direction. An isolate that lost it re-runs an
49
+ * idempotent `install -m 644`.
50
+ *
51
+ * ## What clears it
52
+ *
53
+ * **The container's own exit**, watched through `monitor()`. This is the signal
54
+ * that does not depend on anyone noticing: a container replaced underneath a
55
+ * running command — by a reconnect, a relaunch, a crash — exits, and the watch
56
+ * fires whoever caused it. Nothing else here is in a position to know, because
57
+ * most commands run through the stub this object hands out and report their
58
+ * failures to their own caller, not to it.
59
+ *
60
+ * The watch lives in memory beside the flag it clears, which is the right
61
+ * lifetime: an isolate that loses one loses the other, and the replacement
62
+ * starts out untrusted. It is deliberately not held open with `waitUntil` —
63
+ * that would keep an invocation alive for the life of the container.
64
+ *
65
+ * A container seen not running, and a command reporting `EEXEC_LOST`, both call
66
+ * {@link forget} as well. They are cheap, they are strictly earlier in some
67
+ * orderings, and neither is load-bearing on its own.
68
+ */
69
+ export declare class ContainerTrust {
70
+ #private;
71
+ private readonly deps;
72
+ constructor(deps: ContainerTrustDeps);
73
+ /** Forget that any container was trusted. */
74
+ forget(): void;
75
+ /**
76
+ * Trust the CA if this container has not been trusted yet.
77
+ *
78
+ * Called from the workspace's `ready()` path, which is the earliest point that
79
+ * is not too early: the backend's `connect()` has completed, so start and
80
+ * interception have both happened.
81
+ *
82
+ * **Never throws.** A workspace that cannot trust the CA still has a checkout,
83
+ * a shell and a git history, and whatever runs next will say plainly what went
84
+ * wrong — refusing to proceed here would replace a legible TLS error with an
85
+ * opaque one.
86
+ */
87
+ ensure(): Promise<void>;
88
+ }