@dynamicagents/plugins 0.11.1 → 0.13.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 +59 -79
- package/dist/browser/index.d.ts +0 -2
- package/dist/browser/index.js +21 -27
- package/dist/claude-code/cards.d.ts +53 -0
- package/dist/claude-code/cards.js +272 -0
- package/dist/claude-code/config.d.ts +73 -31
- package/dist/claude-code/config.js +0 -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 +15 -14
- package/dist/claude-code/egress.js +51 -14
- package/dist/claude-code/events.d.ts +153 -9
- package/dist/claude-code/events.js +289 -58
- package/dist/claude-code/github.d.ts +39 -0
- package/dist/claude-code/github.js +82 -0
- package/dist/claude-code/index.d.ts +24 -80
- package/dist/claude-code/index.js +19 -234
- package/dist/claude-code/model.d.ts +152 -0
- package/dist/claude-code/model.js +342 -0
- package/dist/claude-code/run.d.ts +283 -92
- package/dist/claude-code/run.js +620 -114
- package/dist/claude-code/session.d.ts +131 -0
- package/dist/claude-code/session.js +285 -0
- package/dist/claude-code/spec.d.ts +46 -0
- package/dist/claude-code/spec.js +83 -0
- package/dist/claude-code/verified.d.ts +15 -0
- package/dist/claude-code/verified.js +15 -0
- package/dist/computer/context.d.ts +19 -0
- package/dist/computer/context.js +222 -0
- package/dist/computer/file-lock.d.ts +5 -0
- package/dist/computer/file-lock.js +40 -0
- package/dist/computer/gate.d.ts +12 -12
- package/dist/computer/gate.js +12 -12
- package/dist/computer/index.d.ts +57 -255
- package/dist/computer/index.js +98 -1039
- package/dist/computer/paths.d.ts +47 -22
- package/dist/computer/paths.js +64 -49
- package/dist/computer/proxy.d.ts +45 -0
- package/dist/computer/proxy.js +234 -0
- package/dist/computer/render.d.ts +27 -38
- package/dist/computer/render.js +32 -65
- package/dist/computer/tools-exec.d.ts +3 -0
- package/dist/computer/tools-exec.js +268 -0
- package/dist/computer/tools-file.d.ts +12 -0
- package/dist/computer/tools-file.js +64 -0
- package/dist/computer/tools-grep.d.ts +3 -0
- package/dist/computer/tools-grep.js +129 -0
- package/dist/repo/checkout.d.ts +35 -6
- package/dist/repo/checkout.js +64 -6
- package/dist/repo/context.d.ts +135 -0
- package/dist/repo/context.js +582 -0
- package/dist/repo/index.d.ts +79 -57
- package/dist/repo/index.js +118 -1112
- package/dist/repo/tools-checks.d.ts +3 -0
- package/dist/repo/tools-checks.js +112 -0
- package/dist/repo/tools-clone.d.ts +4 -0
- package/dist/repo/tools-clone.js +192 -0
- package/dist/repo/tools-pr.d.ts +14 -0
- package/dist/repo/tools-pr.js +196 -0
- package/dist/repo/tools-review.d.ts +12 -0
- package/dist/repo/tools-review.js +393 -0
- package/dist/repo/tools-worktree.d.ts +4 -0
- package/dist/repo/tools-worktree.js +305 -0
- package/dist/repo/tools-worktrees.d.ts +12 -0
- package/dist/repo/tools-worktrees.js +48 -0
- package/dist/repo/url.d.ts +2 -2
- package/dist/repo/url.js +7 -7
- package/dist/scratch/index.d.ts +4 -4
- package/dist/scratch/index.js +12 -11
- package/dist/{computer → workspace}/advisory.d.ts +33 -20
- package/dist/{computer → workspace}/advisory.js +32 -7
- package/dist/workspace/ca-trust.d.ts +95 -0
- package/dist/workspace/ca-trust.js +312 -0
- package/dist/workspace/container-deps.d.ts +54 -0
- package/dist/workspace/container-deps.js +224 -0
- package/dist/workspace/container-fault.d.ts +38 -0
- package/dist/workspace/container-fault.js +73 -0
- package/dist/workspace/exec.d.ts +94 -0
- package/dist/workspace/exec.js +153 -0
- package/dist/workspace/format.d.ts +36 -0
- package/dist/workspace/format.js +57 -0
- package/dist/workspace/git-host.d.ts +79 -0
- package/dist/workspace/git-host.js +226 -0
- package/dist/workspace/git-identity.d.ts +57 -0
- package/dist/workspace/git-identity.js +147 -0
- package/dist/workspace/index.d.ts +43 -47
- package/dist/workspace/index.js +52 -112
- package/dist/workspace/install-job.d.ts +121 -0
- package/dist/workspace/install-job.js +866 -0
- package/dist/{computer → workspace}/install.d.ts +15 -19
- package/dist/{computer → workspace}/install.js +8 -11
- package/dist/workspace/object.d.ts +502 -0
- package/dist/workspace/object.js +1427 -0
- package/dist/workspace/open.d.ts +95 -0
- package/dist/workspace/open.js +63 -0
- package/dist/workspace/read.d.ts +16 -0
- package/dist/workspace/read.js +22 -0
- package/dist/workspace/shell.d.ts +45 -0
- package/dist/workspace/shell.js +80 -0
- package/dist/workspace/sync.d.ts +111 -0
- package/dist/workspace/sync.js +124 -0
- package/dist/workspace/wake.d.ts +19 -0
- package/dist/workspace/wake.js +10 -0
- package/package.json +28 -42
- package/dist/arc-agi/analysis.d.ts +0 -336
- package/dist/arc-agi/analysis.js +0 -829
- package/dist/arc-agi/client.d.ts +0 -82
- package/dist/arc-agi/client.js +0 -113
- package/dist/arc-agi/game-tools.d.ts +0 -25
- package/dist/arc-agi/game-tools.js +0 -26
- package/dist/arc-agi/index.d.ts +0 -42
- package/dist/arc-agi/index.js +0 -175
- package/dist/arc-agi/main-agent.d.ts +0 -27
- package/dist/arc-agi/main-agent.js +0 -65
- package/dist/arc-agi/recipe.d.ts +0 -51
- package/dist/arc-agi/recipe.js +0 -100
- package/dist/arc-agi/scorecard.d.ts +0 -118
- package/dist/arc-agi/scorecard.js +0 -163
- package/dist/arc-agi/soul.d.ts +0 -40
- package/dist/arc-agi/soul.js +0 -90
- package/dist/arc-agi/store.d.ts +0 -154
- package/dist/arc-agi/store.js +0 -216
- package/dist/arc-agi/tools.d.ts +0 -36
- package/dist/arc-agi/tools.js +0 -576
- package/dist/arc-agi/types.d.ts +0 -212
- package/dist/arc-agi/types.js +0 -19
- package/dist/claude-code/recipe.d.ts +0 -53
- package/dist/claude-code/recipe.js +0 -100
- package/dist/computer/read.d.ts +0 -137
- package/dist/computer/read.js +0 -203
- package/dist/recall/index.d.ts +0 -99
- package/dist/recall/index.js +0 -171
- package/dist/triage/index.d.ts +0 -355
- package/dist/triage/index.js +0 -302
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { humanMs } from "./
|
|
1
|
+
import { humanMs } from "./format.js";
|
|
2
2
|
/** What each advisory *means*. The one exhaustive home for policy. */
|
|
3
3
|
export function shapeOf(advisory) {
|
|
4
4
|
switch (advisory.kind) {
|
|
@@ -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":
|
|
@@ -163,7 +188,7 @@ export function deriveAdvisories(input) {
|
|
|
163
188
|
/**
|
|
164
189
|
* Everything a session should know before it starts, or `undefined` for silence.
|
|
165
190
|
*
|
|
166
|
-
* The counterpart to {@link file
|
|
191
|
+
* The counterpart to {@link file://../computer/gate.ts execGate}, for the reader that is a
|
|
167
192
|
* whole run rather than one command. It cannot be blocked and does not need to
|
|
168
193
|
* be: it can install dependencies itself, and the only thing it must not do is
|
|
169
194
|
* work silently against a workspace that is not what it assumes.
|
|
@@ -0,0 +1,95 @@
|
|
|
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
|
+
/**
|
|
74
|
+
* Forget that any container was trusted.
|
|
75
|
+
*
|
|
76
|
+
* Drops the standing watch too, because this is also reached for a container
|
|
77
|
+
* that keeps running — a `running` read, a lost exec. The watch it leaves
|
|
78
|
+
* behind ignores that container's real exit, so without a fresh one nothing
|
|
79
|
+
* would clear the flag again and the replacement would be vouched for.
|
|
80
|
+
*/
|
|
81
|
+
forget(): void;
|
|
82
|
+
/**
|
|
83
|
+
* Trust the CA if this container has not been trusted yet.
|
|
84
|
+
*
|
|
85
|
+
* Called from the workspace's `ready()` path, which is the earliest point that
|
|
86
|
+
* is not too early: the backend's `connect()` has completed, so start and
|
|
87
|
+
* interception have both happened.
|
|
88
|
+
*
|
|
89
|
+
* **Never throws.** A workspace that cannot trust the CA still has a checkout,
|
|
90
|
+
* a shell and a git history, and whatever runs next will say plainly what went
|
|
91
|
+
* wrong — refusing to proceed here would replace a legible TLS error with an
|
|
92
|
+
* opaque one.
|
|
93
|
+
*/
|
|
94
|
+
ensure(): Promise<void>;
|
|
95
|
+
}
|
|
@@ -0,0 +1,312 @@
|
|
|
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 { deploymentFault } from "./container-fault.js";
|
|
54
|
+
/**
|
|
55
|
+
* Making a container able to speak TLS, once there is a CA to trust.
|
|
56
|
+
*
|
|
57
|
+
* Only `egress: { mode: "http-gateway" }` needs this, but it lives on every
|
|
58
|
+
* workspace because the cost of getting it wrong is asymmetric: the trust
|
|
59
|
+
* install against a container with no CA prints one line and exits 0, while a
|
|
60
|
+
* container that missed it fails every later HTTPS connection with an error
|
|
61
|
+
* that names no cause.
|
|
62
|
+
*/
|
|
63
|
+
/**
|
|
64
|
+
* Install the egress interception CA, and say what was there to install.
|
|
65
|
+
*
|
|
66
|
+
* One shell string rather than a script in the image, because **the image cannot
|
|
67
|
+
* run this at all**, and the reason is ordering rather than preference.
|
|
68
|
+
* Cloudflare's own recipe assumes interception is declared as container
|
|
69
|
+
* configuration, so the runtime mounts the CA before the container starts.
|
|
70
|
+
* `@cloudflare/computer` uses the raw container API, where the only interception
|
|
71
|
+
* hooks are methods called *after* `start()` — so an entrypoint runs strictly
|
|
72
|
+
* before the CA can exist, finds nothing, and reports the container
|
|
73
|
+
* un-intercepted while every later TLS connection fails.
|
|
74
|
+
*
|
|
75
|
+
* The listing comes first and runs unconditionally: when the CA is missing,
|
|
76
|
+
* *what else is in that directory* is the only evidence distinguishing "mounted
|
|
77
|
+
* somewhere else" from "never provisioned", and it costs one line.
|
|
78
|
+
*/
|
|
79
|
+
export const TRUST_CA_COMMAND = [
|
|
80
|
+
'ls -A /etc/cloudflare/certs 2>&1 || echo "(no /etc/cloudflare/certs)"',
|
|
81
|
+
"CA=/etc/cloudflare/certs/cloudflare-containers-ca.crt",
|
|
82
|
+
'if [ -r "$CA" ]; then',
|
|
83
|
+
// Trailing operators, not leading ones: a newline ends a command in sh, so an
|
|
84
|
+
// `&&` opening the next line is a syntax error rather than a continuation.
|
|
85
|
+
' install -m 644 "$CA" /usr/local/share/ca-certificates/cloudflare-containers-ca.crt &&',
|
|
86
|
+
" update-ca-certificates > /dev/null 2>&1 &&",
|
|
87
|
+
' echo TRUSTED || echo "TRUST FAILED"',
|
|
88
|
+
"else",
|
|
89
|
+
' echo "NO CA AT $CA"',
|
|
90
|
+
"fi"
|
|
91
|
+
].join("\n");
|
|
92
|
+
/** How long the trust install may take before it is not going to happen. */
|
|
93
|
+
const TRUST_TIMEOUT_MS = 30_000;
|
|
94
|
+
/**
|
|
95
|
+
* Whether the container this object is talking to has had the CA installed.
|
|
96
|
+
*
|
|
97
|
+
* ## Why the answer is in memory
|
|
98
|
+
*
|
|
99
|
+
* What it describes is a *container*, which does not outlive the isolate in any
|
|
100
|
+
* way worth persisting. A stored `true` would be the exact bug this exists to
|
|
101
|
+
* prevent — a flag surviving the container it describes, and vouching for its
|
|
102
|
+
* replacement.
|
|
103
|
+
*
|
|
104
|
+
* It is wrong only in the safe direction. An isolate that lost it re-runs an
|
|
105
|
+
* idempotent `install -m 644`.
|
|
106
|
+
*
|
|
107
|
+
* ## What clears it
|
|
108
|
+
*
|
|
109
|
+
* **The container's own exit**, watched through `monitor()`. This is the signal
|
|
110
|
+
* that does not depend on anyone noticing: a container replaced underneath a
|
|
111
|
+
* running command — by a reconnect, a relaunch, a crash — exits, and the watch
|
|
112
|
+
* fires whoever caused it. Nothing else here is in a position to know, because
|
|
113
|
+
* most commands run through the stub this object hands out and report their
|
|
114
|
+
* failures to their own caller, not to it.
|
|
115
|
+
*
|
|
116
|
+
* The watch lives in memory beside the flag it clears, which is the right
|
|
117
|
+
* lifetime: an isolate that loses one loses the other, and the replacement
|
|
118
|
+
* starts out untrusted. It is deliberately not held open with `waitUntil` —
|
|
119
|
+
* that would keep an invocation alive for the life of the container.
|
|
120
|
+
*
|
|
121
|
+
* A container seen not running, and a command reporting `EEXEC_LOST`, both call
|
|
122
|
+
* {@link forget} as well. They are cheap, they are strictly earlier in some
|
|
123
|
+
* orderings, and neither is load-bearing on its own.
|
|
124
|
+
*/
|
|
125
|
+
export class ContainerTrust {
|
|
126
|
+
deps;
|
|
127
|
+
#trusted = false;
|
|
128
|
+
/** Whether an exit watch is standing for the container now running. */
|
|
129
|
+
#watching = false;
|
|
130
|
+
/** Bumped by {@link forget}, so a watch armed before it is ignored. */
|
|
131
|
+
#generation = 0;
|
|
132
|
+
/**
|
|
133
|
+
* Whether this container's deployment fault has already been reported.
|
|
134
|
+
*
|
|
135
|
+
* Same lifetime as {@link ContainerTrust.forget}'s other flag, and for the
|
|
136
|
+
* same reason: what it describes is one container. A fault that outlived it
|
|
137
|
+
* would silence the report for the replacement, which is the container an
|
|
138
|
+
* operator is waiting to hear about.
|
|
139
|
+
*/
|
|
140
|
+
#faultReported = false;
|
|
141
|
+
constructor(deps) {
|
|
142
|
+
this.deps = deps;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Forget that any container was trusted.
|
|
146
|
+
*
|
|
147
|
+
* Drops the standing watch too, because this is also reached for a container
|
|
148
|
+
* that keeps running — a `running` read, a lost exec. The watch it leaves
|
|
149
|
+
* behind ignores that container's real exit, so without a fresh one nothing
|
|
150
|
+
* would clear the flag again and the replacement would be vouched for.
|
|
151
|
+
*/
|
|
152
|
+
forget() {
|
|
153
|
+
this.#generation += 1;
|
|
154
|
+
this.#trusted = false;
|
|
155
|
+
this.#watching = false;
|
|
156
|
+
this.#faultReported = false;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Watch this container's exit, so its replacement is not trusted on its
|
|
160
|
+
* behalf.
|
|
161
|
+
*
|
|
162
|
+
* Re-armed rather than kept: the promise settles once, for the container it
|
|
163
|
+
* was taken from, so the next {@link ensure} takes a fresh one for whatever is
|
|
164
|
+
* running then.
|
|
165
|
+
*
|
|
166
|
+
* A rejection is an exit too — the runtime reporting the container went away
|
|
167
|
+
* badly — so both settlements clear the flag, and neither is allowed to
|
|
168
|
+
* surface as an unhandled rejection.
|
|
169
|
+
*
|
|
170
|
+
* An exit already accounted for — the container was forgotten since — is not
|
|
171
|
+
* reported: a replacement may be running by then, and it is not the one that
|
|
172
|
+
* exited.
|
|
173
|
+
*/
|
|
174
|
+
#watch() {
|
|
175
|
+
if (this.#watching)
|
|
176
|
+
return;
|
|
177
|
+
const container = this.deps.container();
|
|
178
|
+
if (!container)
|
|
179
|
+
return;
|
|
180
|
+
const generation = this.#generation;
|
|
181
|
+
try {
|
|
182
|
+
// A stale watch returns before touching anything: `#watching` belongs to
|
|
183
|
+
// whichever watch was armed since.
|
|
184
|
+
const done = () => {
|
|
185
|
+
if (generation !== this.#generation)
|
|
186
|
+
return;
|
|
187
|
+
this.forget();
|
|
188
|
+
this.deps.onExit?.();
|
|
189
|
+
};
|
|
190
|
+
container.monitor().then(done, done);
|
|
191
|
+
this.#watching = true;
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
// No watch, so the flag keeps its other two ways of being cleared. Worth a
|
|
195
|
+
// line because it silently widens the window this class exists to close.
|
|
196
|
+
console.warn(`[${this.deps.tag()}] could not watch the container`, {
|
|
197
|
+
id: this.deps.id(),
|
|
198
|
+
err: String(err)
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Trust the CA if this container has not been trusted yet.
|
|
204
|
+
*
|
|
205
|
+
* Called from the workspace's `ready()` path, which is the earliest point that
|
|
206
|
+
* is not too early: the backend's `connect()` has completed, so start and
|
|
207
|
+
* interception have both happened.
|
|
208
|
+
*
|
|
209
|
+
* **Never throws.** A workspace that cannot trust the CA still has a checkout,
|
|
210
|
+
* a shell and a git history, and whatever runs next will say plainly what went
|
|
211
|
+
* wrong — refusing to proceed here would replace a legible TLS error with an
|
|
212
|
+
* opaque one.
|
|
213
|
+
*/
|
|
214
|
+
async ensure() {
|
|
215
|
+
this.#watch();
|
|
216
|
+
if (this.#trusted)
|
|
217
|
+
return;
|
|
218
|
+
// An exec pushes the workspace before it spawns, and a new container is
|
|
219
|
+
// empty, so this one — the first in every container — carries the whole
|
|
220
|
+
// tree. {@link TRUST_TIMEOUT_MS} bounds the process, not any of that.
|
|
221
|
+
const startedAt = Date.now();
|
|
222
|
+
try {
|
|
223
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
224
|
+
try {
|
|
225
|
+
const handle = __addDisposableResource(env_1, await this.deps
|
|
226
|
+
.workspace()
|
|
227
|
+
.runtime.exec(TRUST_CA_COMMAND, {
|
|
228
|
+
cwd: "/",
|
|
229
|
+
encoding: "utf8",
|
|
230
|
+
timeoutMs: TRUST_TIMEOUT_MS
|
|
231
|
+
}), false);
|
|
232
|
+
const result = await handle.result();
|
|
233
|
+
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
|
|
234
|
+
// Marked only when the command reported an outcome that will not change
|
|
235
|
+
// inside this container. `TRUSTED` is the success; `NO CA AT …` is the
|
|
236
|
+
// intentional terminal case, since interception is configured by the
|
|
237
|
+
// `connect()` already awaited and a CA absent now stays absent until the
|
|
238
|
+
// container is replaced — retrying that on the object's hottest path would
|
|
239
|
+
// buy nothing.
|
|
240
|
+
//
|
|
241
|
+
// `TRUST FAILED` is neither. The command exits 0 whatever happens, so
|
|
242
|
+
// treating "it ran" as "it worked" would mark a container trusted whose
|
|
243
|
+
// `install` or `update-ca-certificates` failed — and every later HTTPS
|
|
244
|
+
// connection in it fails with nothing naming the cause, permanently,
|
|
245
|
+
// because nothing tries again.
|
|
246
|
+
//
|
|
247
|
+
// A throw leaves this false too, so an unreachable container is retried.
|
|
248
|
+
this.#trusted = !output.includes("TRUST FAILED");
|
|
249
|
+
// Logged at info once per container, deliberately. The container's own
|
|
250
|
+
// stdout does not reach Workers Observability, so this line is the only
|
|
251
|
+
// place an operator can see whether the container can speak TLS at all —
|
|
252
|
+
// and its absence is itself the answer when a workspace never got this far.
|
|
253
|
+
//
|
|
254
|
+
// Once per container rather than per call is also what makes it readable:
|
|
255
|
+
// on the busiest entry point in the object, a line per call would bury the
|
|
256
|
+
// one that matters under thousands that say the same thing.
|
|
257
|
+
console.info(`[${this.deps.tag()}] container TLS trust`, {
|
|
258
|
+
id: this.deps.id(),
|
|
259
|
+
exitCode: result.exitCode,
|
|
260
|
+
trusted: this.#trusted,
|
|
261
|
+
// Where a cold workspace's time went, on the line already logged once
|
|
262
|
+
// per container.
|
|
263
|
+
pushed: result.pushed,
|
|
264
|
+
ms: Date.now() - startedAt,
|
|
265
|
+
output
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
catch (e_1) {
|
|
269
|
+
env_1.error = e_1;
|
|
270
|
+
env_1.hasError = true;
|
|
271
|
+
}
|
|
272
|
+
finally {
|
|
273
|
+
__disposeResources(env_1);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
catch (err) {
|
|
277
|
+
// A deployment fault is reported as itself, at error, once per container.
|
|
278
|
+
// The generic line below is the right one for a container that is merely
|
|
279
|
+
// unreachable — a retry is what clears that. It is the wrong one for a
|
|
280
|
+
// Worker whose image or container application cannot work at all:
|
|
281
|
+
// retrying logs the same symptom until something else gives up, and the
|
|
282
|
+
// sentence naming the cause is the one an operator needs.
|
|
283
|
+
//
|
|
284
|
+
// Only the *log* stops. The trust command is still sent on the next call,
|
|
285
|
+
// because the classification is a string match and this is the direction
|
|
286
|
+
// that is safe to be wrong in: a misread costs a line nobody needed, where
|
|
287
|
+
// refusing to retry would leave a container permanently untrusted on the
|
|
288
|
+
// strength of a message the library is free to reword. What actually ends
|
|
289
|
+
// the reporting is the container going away, which clears the flag through
|
|
290
|
+
// the same watch that clears the trust.
|
|
291
|
+
const fault = deploymentFault(err);
|
|
292
|
+
if (fault) {
|
|
293
|
+
if (!this.#faultReported) {
|
|
294
|
+
this.#faultReported = true;
|
|
295
|
+
console.error(`[${this.deps.tag()}] ${fault.summary}`, {
|
|
296
|
+
id: this.deps.id(),
|
|
297
|
+
remedy: fault.remedy,
|
|
298
|
+
err: String(err)
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
console.warn(`[${this.deps.tag()}] could not trust the interception CA`, {
|
|
304
|
+
id: this.deps.id(),
|
|
305
|
+
// Separates a container that refused at once from one that was pushed
|
|
306
|
+
// to and then dropped.
|
|
307
|
+
ms: Date.now() - startedAt,
|
|
308
|
+
err: String(err)
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type Workspace } from "@cloudflare/computer";
|
|
2
|
+
/** Written into the tree by a successful host install; see `./install-job.ts`. */
|
|
3
|
+
export declare const INSTALLED_MARKER = ".da-installed";
|
|
4
|
+
/**
|
|
5
|
+
* The per-container setup, and a read of the marker under `dir`.
|
|
6
|
+
*
|
|
7
|
+
* The helper mounts the package root the command targets — its directory, or
|
|
8
|
+
* `--prefix`, `--cwd`, `--dir`, `-C` — only under the workspace, never inside
|
|
9
|
+
* another tree. Workspace members are not mounted: a mount point cannot be
|
|
10
|
+
* removed, so one guessed into `dist/` breaks `rm -rf dist`, and an install at
|
|
11
|
+
* the root puts the bulk of the tree there.
|
|
12
|
+
*
|
|
13
|
+
* Wrappers are written with `install`, never through the path: `npm` is
|
|
14
|
+
* commonly a symlink to `npm-cli.js`, and writing through it replaces npm with a
|
|
15
|
+
* wrapper that execs itself. For the same reason a wrapper finds its tool on
|
|
16
|
+
* `PATH` with `WRAPPER_DIR` skipped, when it runs. `corepack enable` puts its
|
|
17
|
+
* shims beside the `corepack` on `PATH` — the wrapper — so it is pointed at the
|
|
18
|
+
* real one's directory instead.
|
|
19
|
+
*/
|
|
20
|
+
export declare function depsSetupCommand(workspaceDir: string, dir?: string): string;
|
|
21
|
+
/** What a setup run found in the container. */
|
|
22
|
+
export interface DepsFound {
|
|
23
|
+
/** The marker's contents, or `null` when no host install completed here. */
|
|
24
|
+
marker: string | null;
|
|
25
|
+
}
|
|
26
|
+
export interface ContainerDepsDeps {
|
|
27
|
+
workspace: () => Workspace;
|
|
28
|
+
workspaceDir: string;
|
|
29
|
+
tag: () => string;
|
|
30
|
+
id: () => string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The setup above, once per container.
|
|
34
|
+
*
|
|
35
|
+
* In memory, with {@link file://./ca-trust.ts}'s lifetime and for its reason: it
|
|
36
|
+
* describes one container, and an isolate that lost it re-runs an idempotent
|
|
37
|
+
* command.
|
|
38
|
+
*/
|
|
39
|
+
export declare class ContainerDeps {
|
|
40
|
+
#private;
|
|
41
|
+
private readonly deps;
|
|
42
|
+
constructor(deps: ContainerDepsDeps);
|
|
43
|
+
forget(): void;
|
|
44
|
+
/**
|
|
45
|
+
* Run the setup if this container has not had it, and report the marker under
|
|
46
|
+
* `dir`. `undefined` when it had, or with no `dir`.
|
|
47
|
+
*
|
|
48
|
+
* Throws when the container answered and the setup did not finish: nothing
|
|
49
|
+
* would say so later, since an unwrapped install succeeds, into the workspace.
|
|
50
|
+
* An unreachable container is only logged, like the CA install: the command
|
|
51
|
+
* after it fails the same way.
|
|
52
|
+
*/
|
|
53
|
+
ensure(dir?: string): Promise<DepsFound | undefined>;
|
|
54
|
+
}
|