@dynamicagents/plugins 0.11.1 → 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.
- package/README.md +12 -13
- package/dist/arc-agi/analysis.d.ts +16 -30
- package/dist/arc-agi/analysis.js +20 -47
- package/dist/arc-agi/game-tools.d.ts +5 -6
- package/dist/arc-agi/index.d.ts +6 -8
- package/dist/arc-agi/index.js +3 -3
- package/dist/arc-agi/main-agent.d.ts +5 -8
- package/dist/arc-agi/main-agent.js +5 -8
- package/dist/arc-agi/recipe.d.ts +17 -20
- package/dist/arc-agi/recipe.js +17 -20
- package/dist/arc-agi/soul.d.ts +16 -27
- package/dist/arc-agi/soul.js +16 -27
- package/dist/arc-agi/store.d.ts +4 -5
- package/dist/arc-agi/store.js +4 -5
- package/dist/arc-agi/tools.d.ts +1 -1
- package/dist/arc-agi/tools.js +10 -12
- package/dist/arc-agi/types.d.ts +6 -7
- package/dist/claude-code/config.d.ts +88 -11
- package/dist/claude-code/config.js +11 -2
- package/dist/claude-code/credentials.d.ts +21 -9
- package/dist/claude-code/credentials.js +43 -9
- package/dist/claude-code/egress.d.ts +7 -13
- package/dist/claude-code/egress.js +9 -10
- package/dist/claude-code/events.d.ts +47 -0
- package/dist/claude-code/events.js +96 -10
- package/dist/claude-code/index.d.ts +31 -13
- package/dist/claude-code/index.js +62 -15
- package/dist/claude-code/run.d.ts +135 -41
- package/dist/claude-code/run.js +366 -61
- package/dist/computer/advisory.d.ts +31 -18
- package/dist/computer/advisory.js +30 -5
- package/dist/computer/context.d.ts +23 -0
- package/dist/computer/context.js +225 -0
- package/dist/computer/file-lock.d.ts +5 -0
- package/dist/computer/file-lock.js +39 -0
- package/dist/computer/gate.d.ts +8 -8
- package/dist/computer/gate.js +7 -7
- package/dist/computer/host/ca-trust.d.ts +88 -0
- package/dist/computer/host/ca-trust.js +303 -0
- package/dist/computer/host/container-deps.d.ts +54 -0
- package/dist/computer/host/container-deps.js +224 -0
- package/dist/computer/host/container-fault.d.ts +38 -0
- package/dist/computer/host/container-fault.js +58 -0
- package/dist/computer/host/git-host.d.ts +62 -0
- package/dist/computer/host/git-host.js +156 -0
- package/dist/computer/host/git-identity.d.ts +57 -0
- package/dist/computer/host/git-identity.js +147 -0
- package/dist/computer/host/install-job.d.ts +121 -0
- package/dist/computer/host/install-job.js +766 -0
- package/dist/computer/host/sync.d.ts +111 -0
- package/dist/computer/host/sync.js +124 -0
- package/dist/computer/host/wake.d.ts +19 -0
- package/dist/computer/host/wake.js +10 -0
- package/dist/computer/host/workspace.d.ts +424 -0
- package/dist/computer/host/workspace.js +1258 -0
- package/dist/computer/index.d.ts +70 -50
- package/dist/computer/index.js +79 -767
- package/dist/computer/install.d.ts +8 -12
- package/dist/computer/install.js +6 -9
- package/dist/computer/paths.d.ts +21 -16
- package/dist/computer/paths.js +40 -38
- package/dist/computer/read.d.ts +18 -22
- package/dist/computer/read.js +24 -27
- package/dist/computer/render.d.ts +28 -0
- package/dist/computer/render.js +34 -1
- package/dist/computer/shell.d.ts +45 -0
- package/dist/computer/shell.js +80 -0
- package/dist/computer/tools-exec.d.ts +3 -0
- package/dist/computer/tools-exec.js +269 -0
- package/dist/computer/tools-file.d.ts +4 -0
- package/dist/computer/tools-file.js +124 -0
- package/dist/computer/tools-find.d.ts +3 -0
- package/dist/computer/tools-find.js +216 -0
- package/dist/recall/index.d.ts +9 -3
- package/dist/recall/index.js +22 -12
- package/dist/repo/checkout.d.ts +34 -4
- package/dist/repo/checkout.js +36 -1
- package/dist/repo/context.d.ts +109 -0
- package/dist/repo/context.js +530 -0
- package/dist/repo/index.d.ts +6 -29
- package/dist/repo/index.js +17 -1027
- package/dist/repo/tools-clone.d.ts +4 -0
- package/dist/repo/tools-clone.js +192 -0
- package/dist/repo/tools-forge.d.ts +4 -0
- package/dist/repo/tools-forge.js +179 -0
- package/dist/repo/tools-review.d.ts +3 -0
- package/dist/repo/tools-review.js +373 -0
- package/dist/repo/tools-worktree.d.ts +4 -0
- package/dist/repo/tools-worktree.js +221 -0
- package/dist/triage/index.d.ts +6 -1
- package/dist/triage/index.js +32 -23
- package/dist/workspace/index.d.ts +3 -3
- package/dist/workspace/index.js +3 -3
- 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
|
|
13
|
-
*
|
|
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
|
|
16
|
-
*
|
|
17
|
-
* wall about the Durable Object, the
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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,
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
|
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,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
|
+
}
|
package/dist/computer/gate.d.ts
CHANGED
|
@@ -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
|
|
22
|
-
* because the filesystem is the Durable Object's rather than the
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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.
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
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. */
|
package/dist/computer/gate.js
CHANGED
|
@@ -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
|
|
22
|
-
* because the filesystem is the Durable Object's rather than the
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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
|
|
35
|
-
"left it
|
|
36
|
-
"
|
|
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
|
+
}
|