@dynamicagents/plugins 0.12.0 → 0.14.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 +58 -77
- package/dist/browser/index.d.ts +0 -2
- package/dist/browser/index.js +21 -27
- package/dist/claude-code/cards.d.ts +55 -0
- package/dist/claude-code/cards.js +283 -0
- package/dist/claude-code/config.d.ts +28 -63
- package/dist/claude-code/config.js +0 -11
- package/dist/claude-code/egress.d.ts +8 -1
- package/dist/claude-code/egress.js +42 -4
- package/dist/claude-code/events.d.ts +106 -9
- package/dist/claude-code/events.js +201 -56
- package/dist/claude-code/github.d.ts +39 -0
- package/dist/claude-code/github.js +82 -0
- package/dist/claude-code/index.d.ts +16 -90
- package/dist/claude-code/index.js +14 -276
- package/dist/claude-code/model.d.ts +152 -0
- package/dist/claude-code/model.js +366 -0
- package/dist/claude-code/run.d.ts +230 -116
- package/dist/claude-code/run.js +380 -167
- package/dist/claude-code/session.d.ts +131 -0
- package/dist/claude-code/session.js +288 -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 +1 -5
- package/dist/computer/context.js +7 -10
- package/dist/computer/file-lock.js +5 -4
- package/dist/computer/gate.d.ts +4 -4
- package/dist/computer/gate.js +5 -5
- package/dist/computer/index.d.ts +40 -258
- package/dist/computer/index.js +71 -324
- package/dist/computer/paths.d.ts +36 -16
- package/dist/computer/paths.js +42 -29
- package/dist/computer/proxy.d.ts +45 -0
- package/dist/computer/proxy.js +234 -0
- package/dist/computer/render.d.ts +1 -40
- package/dist/computer/render.js +2 -68
- package/dist/computer/tools-exec.js +12 -13
- package/dist/computer/tools-file.d.ts +9 -1
- package/dist/computer/tools-file.js +22 -82
- package/dist/computer/{tools-find.d.ts → tools-grep.d.ts} +1 -1
- package/dist/computer/tools-grep.js +129 -0
- package/dist/repo/checkout.d.ts +4 -5
- package/dist/repo/checkout.js +31 -8
- package/dist/repo/context.d.ts +35 -9
- package/dist/repo/context.js +101 -49
- package/dist/repo/index.d.ts +73 -28
- package/dist/repo/index.js +112 -96
- package/dist/repo/tools-checks.d.ts +3 -0
- package/dist/repo/tools-checks.js +112 -0
- package/dist/repo/tools-clone.js +1 -1
- package/dist/repo/tools-pr.d.ts +14 -0
- package/dist/repo/{tools-forge.js → tools-pr.js} +53 -36
- package/dist/repo/tools-review.d.ts +9 -0
- package/dist/repo/tools-review.js +67 -47
- package/dist/repo/tools-worktree.js +144 -60
- 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 +4 -4
- package/dist/{computer → workspace}/advisory.js +2 -2
- package/dist/{computer/host → workspace}/ca-trust.d.ts +8 -1
- package/dist/{computer/host → workspace}/ca-trust.js +12 -3
- package/dist/{computer/host → workspace}/container-deps.d.ts +4 -0
- package/dist/{computer/host → workspace}/container-deps.js +26 -1
- package/dist/{computer/host → workspace}/container-fault.d.ts +3 -3
- package/dist/{computer/host → workspace}/container-fault.js +23 -8
- package/dist/workspace/deps-snapshot.d.ts +110 -0
- package/dist/workspace/deps-snapshot.js +424 -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/{computer/host → workspace}/git-host.d.ts +21 -4
- package/dist/{computer/host → workspace}/git-host.js +72 -2
- package/dist/{computer/host → workspace}/git-identity.d.ts +1 -1
- package/dist/{computer/host → workspace}/git-identity.js +1 -1
- package/dist/workspace/index.d.ts +44 -47
- package/dist/workspace/index.js +52 -112
- package/dist/{computer/host → workspace}/install-job.d.ts +53 -4
- package/dist/{computer/host → workspace}/install-job.js +346 -178
- package/dist/{computer → workspace}/install.d.ts +17 -8
- package/dist/{computer → workspace}/install.js +24 -6
- package/dist/{computer/host/workspace.d.ts → workspace/object.d.ts} +125 -27
- package/dist/{computer/host/workspace.js → workspace/object.js} +341 -58
- 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/{computer → workspace}/shell.d.ts +5 -5
- package/dist/{computer → workspace}/shell.js +6 -6
- package/dist/{computer/host → workspace}/wake.d.ts +3 -1
- package/package.json +17 -37
- package/dist/arc-agi/analysis.d.ts +0 -322
- package/dist/arc-agi/analysis.js +0 -802
- 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 -24
- package/dist/arc-agi/game-tools.js +0 -26
- package/dist/arc-agi/index.d.ts +0 -40
- package/dist/arc-agi/index.js +0 -175
- package/dist/arc-agi/main-agent.d.ts +0 -24
- package/dist/arc-agi/main-agent.js +0 -62
- package/dist/arc-agi/recipe.d.ts +0 -48
- package/dist/arc-agi/recipe.js +0 -97
- 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 -29
- package/dist/arc-agi/soul.js +0 -79
- package/dist/arc-agi/store.d.ts +0 -153
- package/dist/arc-agi/store.js +0 -215
- package/dist/arc-agi/tools.d.ts +0 -36
- package/dist/arc-agi/tools.js +0 -574
- package/dist/arc-agi/types.d.ts +0 -211
- 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 -133
- package/dist/computer/read.js +0 -200
- package/dist/computer/tools-find.js +0 -216
- package/dist/recall/index.d.ts +0 -105
- package/dist/recall/index.js +0 -181
- package/dist/repo/tools-forge.d.ts +0 -4
- package/dist/triage/index.d.ts +0 -360
- package/dist/triage/index.js +0 -311
- /package/dist/{computer/host → workspace}/sync.d.ts +0 -0
- /package/dist/{computer/host → workspace}/sync.js +0 -0
- /package/dist/{computer/host → workspace}/wake.js +0 -0
package/dist/repo/url.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* naming it": every pattern here turns away a string that resolves to something
|
|
7
7
|
* other than what it appears to name.
|
|
8
8
|
*/
|
|
9
|
-
/** The
|
|
9
|
+
/** The hosts a clone URL may name. `RepoConfig.allowedHosts` overrides it. */
|
|
10
10
|
export const DEFAULT_ALLOWED_HOSTS = ["github.com"];
|
|
11
11
|
/** Branch names a push must never target, whatever the model believes. */
|
|
12
12
|
export const PROTECTED_BRANCHES = new Set([
|
|
@@ -88,7 +88,7 @@ export function sameRepoUrl(a, b) {
|
|
|
88
88
|
return canonical(a) === canonical(b);
|
|
89
89
|
}
|
|
90
90
|
/**
|
|
91
|
-
* A name
|
|
91
|
+
* A name GitHub would actually issue — and one that is safe to use as a path
|
|
92
92
|
* segment and as a key.
|
|
93
93
|
*
|
|
94
94
|
* GitHub's rule for an owner and a repository alike is alphanumerics, `-`, `_`
|
|
@@ -101,17 +101,17 @@ export function sameRepoUrl(a, b) {
|
|
|
101
101
|
* is a separator in somebody else's key format, which is how two callers end up
|
|
102
102
|
* sharing one workspace.
|
|
103
103
|
*/
|
|
104
|
-
const
|
|
105
|
-
function
|
|
104
|
+
const GITHUB_NAME = /^[A-Za-z0-9._-]+$/;
|
|
105
|
+
function isGitHubName(name) {
|
|
106
106
|
// `.` and `..` pass the character class and are exactly the two that must not.
|
|
107
|
-
return name !== "." && name !== ".." &&
|
|
107
|
+
return name !== "." && name !== ".." && GITHUB_NAME.test(name);
|
|
108
108
|
}
|
|
109
109
|
/**
|
|
110
110
|
* `owner/repo` out of a repository URL, or `undefined` if it is not one.
|
|
111
111
|
*
|
|
112
112
|
* Anchored on the **parsed host** rather than matched in the string, so
|
|
113
113
|
* `https://evil.example.com/github.com/owner/repo` is not a github.com URL.
|
|
114
|
-
* Both names are then checked against {@link
|
|
114
|
+
* Both names are then checked against {@link isGitHubName}, so a name that could
|
|
115
115
|
* traverse a path or split somebody else's key never gets as far as being one.
|
|
116
116
|
*
|
|
117
117
|
* `undefined` is a refusal at every caller, never a reason to carry on with a
|
|
@@ -125,7 +125,7 @@ export function parseRepo(url, allowedHosts = DEFAULT_ALLOWED_HOSTS) {
|
|
|
125
125
|
if (!match)
|
|
126
126
|
return undefined;
|
|
127
127
|
const [, owner, repo] = match;
|
|
128
|
-
if (!
|
|
128
|
+
if (!isGitHubName(owner) || !isGitHubName(repo))
|
|
129
129
|
return undefined;
|
|
130
130
|
return { owner, repo };
|
|
131
131
|
}
|
package/dist/scratch/index.d.ts
CHANGED
|
@@ -42,7 +42,7 @@ import type { AgentPlugin } from "@dynamicagents/core";
|
|
|
42
42
|
/** Where a scratchpad lives, unless the host says otherwise. */
|
|
43
43
|
export declare const DEFAULT_SCRATCH_DIR = "/workspace/scratch";
|
|
44
44
|
/**
|
|
45
|
-
* Run one command in the host's container. Matches `
|
|
45
|
+
* Run one command in the host's container. Matches `workspaceExec`'s shape.
|
|
46
46
|
*
|
|
47
47
|
* Injected rather than imported, like `/repo`'s: `npm run verify:exports` fails
|
|
48
48
|
* any subpath whose module graph reaches a sibling's, and a scratchpad that
|
|
@@ -54,7 +54,7 @@ export type ScratchExec = (command: string, options?: {
|
|
|
54
54
|
env?: Record<string, string | undefined>;
|
|
55
55
|
timeout?: number;
|
|
56
56
|
/**
|
|
57
|
-
* The
|
|
57
|
+
* The running sub-agent's `runtime()`, forwarded **opaquely** — the same
|
|
58
58
|
* pass-through `/repo` does, for the same reason. This plugin never looks
|
|
59
59
|
* inside it.
|
|
60
60
|
*/
|
|
@@ -96,7 +96,7 @@ export interface ScratchConfig {
|
|
|
96
96
|
* Runs commands in the container holding the workspace.
|
|
97
97
|
*
|
|
98
98
|
* Uncredentialed, and nothing here ever needs a credential: a scratchpad has
|
|
99
|
-
* no remote, so no operation in this plugin talks to
|
|
99
|
+
* no remote, so no operation in this plugin talks to GitHub.
|
|
100
100
|
*/
|
|
101
101
|
exec: ScratchExec;
|
|
102
102
|
/** Where the scratchpad lives. Defaults to {@link DEFAULT_SCRATCH_DIR}. */
|
|
@@ -153,6 +153,6 @@ export interface ScratchConfig {
|
|
|
153
153
|
* carries the same copy for the same reason.
|
|
154
154
|
*/
|
|
155
155
|
export declare function truncateOutput(text: string, max: number): string;
|
|
156
|
-
/** The tool name, exported so a host restricting
|
|
156
|
+
/** The tool name, exported so a host restricting the plugin can name it. */
|
|
157
157
|
export declare const SCRATCH_OPEN_TOOL = "scratch_open";
|
|
158
158
|
export declare function scratch(config: ScratchConfig): AgentPlugin;
|
package/dist/scratch/index.js
CHANGED
|
@@ -68,10 +68,10 @@ export function truncateOutput(text, max) {
|
|
|
68
68
|
return text.slice(0, Math.max(0, max));
|
|
69
69
|
return (text.slice(0, half) + marker(text.length - half * 2) + text.slice(-half));
|
|
70
70
|
}
|
|
71
|
-
/** The tool name, exported so a host restricting
|
|
71
|
+
/** The tool name, exported so a host restricting the plugin can name it. */
|
|
72
72
|
export const SCRATCH_OPEN_TOOL = "scratch_open";
|
|
73
73
|
/**
|
|
74
|
-
* What the
|
|
74
|
+
* What the model is told.
|
|
75
75
|
*
|
|
76
76
|
* The mixing rule at the end is the line worth its tokens. A host that keys one
|
|
77
77
|
* workspace selection per caller — which is what makes a scratchpad routable at
|
|
@@ -294,19 +294,20 @@ export function scratch(config) {
|
|
|
294
294
|
.filter(Boolean)
|
|
295
295
|
.join(" "), maxChars);
|
|
296
296
|
};
|
|
297
|
+
const context = capabilityFor(dir);
|
|
297
298
|
return definePlugin({
|
|
298
|
-
|
|
299
|
+
name: "scratch",
|
|
299
300
|
/**
|
|
300
|
-
*
|
|
301
|
-
*
|
|
301
|
+
* For a parent, never a sub-agent — which the host decides by installing it
|
|
302
|
+
* on one and not the other.
|
|
302
303
|
*
|
|
303
304
|
* Opening a scratchpad *selects a workspace*, exactly as `repo_clone` does.
|
|
304
|
-
* A
|
|
305
|
+
* A sub-agent holding this could re-point the workspace its parent prepared
|
|
305
306
|
* half-way through its own run, which is the hazard a host's per-caller
|
|
306
|
-
* selection already has to document. A
|
|
307
|
-
*
|
|
307
|
+
* selection already has to document. A sub-agent works in whatever it was
|
|
308
|
+
* given.
|
|
308
309
|
*/
|
|
309
|
-
|
|
310
|
+
tools: (ctx) => ({
|
|
310
311
|
[SCRATCH_OPEN_TOOL]: tool({
|
|
311
312
|
description: "Open a scratchpad: a git repository with no remote, in your container, " +
|
|
312
313
|
"for work that does not need a cloned repository. Use it before " +
|
|
@@ -318,9 +319,9 @@ export function scratch(config) {
|
|
|
318
319
|
.optional()
|
|
319
320
|
.describe("Discard everything in the scratchpad first, including files an earlier task left")
|
|
320
321
|
}),
|
|
321
|
-
execute: ({ reset }) => open(reset,
|
|
322
|
+
execute: ({ reset }) => open(reset, ctx.runtime())
|
|
322
323
|
})
|
|
323
324
|
}),
|
|
324
|
-
|
|
325
|
+
context: [{ provider: { get: async () => context } }]
|
|
325
326
|
});
|
|
326
327
|
}
|
|
@@ -17,9 +17,9 @@ import type { InstallState } from "./install.js";
|
|
|
17
17
|
* A ceiling is a hard wall about the Durable Object, and the two are then told
|
|
18
18
|
* apart only by reading prose.
|
|
19
19
|
* - Capacity must not travel through a channel gated by
|
|
20
|
-
* {@link file
|
|
20
|
+
* {@link file://../computer/gate.ts needsDependencies}, or a full workspace says nothing
|
|
21
21
|
* at all to `echo hi > file.txt` — the command whose write is being lost.
|
|
22
|
-
* - "The tree is fine now", after
|
|
22
|
+
* - "The tree is fine now", after an agent installed by hand, has no honest
|
|
23
23
|
* spelling as a job record: fabricating a `done` rests on the presence of a
|
|
24
24
|
* `node_modules` directory, which the wreckage of the very install being
|
|
25
25
|
* overridden already satisfies.
|
|
@@ -56,7 +56,7 @@ export type WorkspaceAdvisory = {
|
|
|
56
56
|
* Reported rather than acted on, because its presence proves nothing in
|
|
57
57
|
* either direction. A half-finished `npm ci` leaves the directory behind
|
|
58
58
|
* with an incomplete tree, so treating existence as success hides the
|
|
59
|
-
* common failure outright; and
|
|
59
|
+
* common failure outright; and an agent that re-ran the install by hand
|
|
60
60
|
* leaves a record the host cannot update, since the host cannot see an
|
|
61
61
|
* install it did not start. Both look identical from here, so the reader
|
|
62
62
|
* is told which way the ambiguity runs rather than being handed a guess.
|
|
@@ -181,7 +181,7 @@ export declare function deriveAdvisories(input: AdvisoryInput): readonly Workspa
|
|
|
181
181
|
/**
|
|
182
182
|
* Everything a session should know before it starts, or `undefined` for silence.
|
|
183
183
|
*
|
|
184
|
-
* The counterpart to {@link file
|
|
184
|
+
* The counterpart to {@link file://../computer/gate.ts execGate}, for the reader that is a
|
|
185
185
|
* whole run rather than one command. It cannot be blocked and does not need to
|
|
186
186
|
* be: it can install dependencies itself, and the only thing it must not do is
|
|
187
187
|
* work silently against a workspace that is not what it assumes.
|
|
@@ -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) {
|
|
@@ -188,7 +188,7 @@ export function deriveAdvisories(input) {
|
|
|
188
188
|
/**
|
|
189
189
|
* Everything a session should know before it starts, or `undefined` for silence.
|
|
190
190
|
*
|
|
191
|
-
* The counterpart to {@link file
|
|
191
|
+
* The counterpart to {@link file://../computer/gate.ts execGate}, for the reader that is a
|
|
192
192
|
* whole run rather than one command. It cannot be blocked and does not need to
|
|
193
193
|
* be: it can install dependencies itself, and the only thing it must not do is
|
|
194
194
|
* work silently against a workspace that is not what it assumes.
|
|
@@ -70,7 +70,14 @@ export declare class ContainerTrust {
|
|
|
70
70
|
#private;
|
|
71
71
|
private readonly deps;
|
|
72
72
|
constructor(deps: ContainerTrustDeps);
|
|
73
|
-
/**
|
|
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
|
+
*/
|
|
74
81
|
forget(): void;
|
|
75
82
|
/**
|
|
76
83
|
* Trust the CA if this container has not been trusted yet.
|
|
@@ -127,7 +127,7 @@ export class ContainerTrust {
|
|
|
127
127
|
#trusted = false;
|
|
128
128
|
/** Whether an exit watch is standing for the container now running. */
|
|
129
129
|
#watching = false;
|
|
130
|
-
/** Bumped by {@link forget}, so a watch
|
|
130
|
+
/** Bumped by {@link forget}, so a watch armed before it is ignored. */
|
|
131
131
|
#generation = 0;
|
|
132
132
|
/**
|
|
133
133
|
* Whether this container's deployment fault has already been reported.
|
|
@@ -141,10 +141,18 @@ export class ContainerTrust {
|
|
|
141
141
|
constructor(deps) {
|
|
142
142
|
this.deps = deps;
|
|
143
143
|
}
|
|
144
|
-
/**
|
|
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
|
+
*/
|
|
145
152
|
forget() {
|
|
146
153
|
this.#generation += 1;
|
|
147
154
|
this.#trusted = false;
|
|
155
|
+
this.#watching = false;
|
|
148
156
|
this.#faultReported = false;
|
|
149
157
|
}
|
|
150
158
|
/**
|
|
@@ -171,8 +179,9 @@ export class ContainerTrust {
|
|
|
171
179
|
return;
|
|
172
180
|
const generation = this.#generation;
|
|
173
181
|
try {
|
|
182
|
+
// A stale watch returns before touching anything: `#watching` belongs to
|
|
183
|
+
// whichever watch was armed since.
|
|
174
184
|
const done = () => {
|
|
175
|
-
this.#watching = false;
|
|
176
185
|
if (generation !== this.#generation)
|
|
177
186
|
return;
|
|
178
187
|
this.forget();
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { type Workspace } from "@cloudflare/computer";
|
|
2
2
|
/** Written into the tree by a successful host install; see `./install-job.ts`. */
|
|
3
3
|
export declare const INSTALLED_MARKER = ".da-installed";
|
|
4
|
+
/** Every package root with a tree on this container's disk, one per line. */
|
|
5
|
+
export declare const DEPS_ROOTS_COMMAND = "for f in /var/lib/workspace-deps/*.root; do [ -f \"$f\" ] && { cat \"$f\"; echo; }; done; true";
|
|
6
|
+
/** {@link DEPS_ROOTS_COMMAND}'s output as paths, each once. */
|
|
7
|
+
export declare function parseDepsRoots(stdout: string): string[];
|
|
4
8
|
/**
|
|
5
9
|
* The per-container setup, and a read of the marker under `dir`.
|
|
6
10
|
*
|
|
@@ -65,6 +65,11 @@ import { deploymentFault } from "./container-fault.js";
|
|
|
65
65
|
* this installs ahead of the real binaries on `PATH`. Mounting earlier cannot
|
|
66
66
|
* work: a bootstrap that initialises submodules and installs each of them in one
|
|
67
67
|
* command creates the directories the mounts belong in.
|
|
68
|
+
*
|
|
69
|
+
* A container restored from a snapshot (see `./deps-snapshot.ts`) has the trees
|
|
70
|
+
* on its disk and none of the mounts, which are kernel state. So each mount
|
|
71
|
+
* records its package root beside its tree, and the setup mounts every recorded
|
|
72
|
+
* root again before it reads the marker through one.
|
|
68
73
|
*/
|
|
69
74
|
/** Where the trees live on the container's disk. */
|
|
70
75
|
const DEPS_DISK = "/var/lib/workspace-deps";
|
|
@@ -86,6 +91,23 @@ const WRAPPED = [
|
|
|
86
91
|
];
|
|
87
92
|
/** Written into the tree by a successful host install; see `./install-job.ts`. */
|
|
88
93
|
export const INSTALLED_MARKER = ".da-installed";
|
|
94
|
+
/**
|
|
95
|
+
* The package root a tree belongs to, written beside it as `<tree>.root`.
|
|
96
|
+
*
|
|
97
|
+
* Beside rather than inside: `npm ci` empties the tree it installs into, and the
|
|
98
|
+
* hash a tree is named by cannot be turned back into the path.
|
|
99
|
+
*/
|
|
100
|
+
const ROOT_SUFFIX = ".root";
|
|
101
|
+
/** Every package root with a tree on this container's disk, one per line. */
|
|
102
|
+
export const DEPS_ROOTS_COMMAND = `for f in ${DEPS_DISK}/*${ROOT_SUFFIX}; do [ -f "$f" ] && { cat "$f"; echo; }; done; true`;
|
|
103
|
+
/** {@link DEPS_ROOTS_COMMAND}'s output as paths, each once. */
|
|
104
|
+
export function parseDepsRoots(stdout) {
|
|
105
|
+
const roots = stdout
|
|
106
|
+
.split("\n")
|
|
107
|
+
.map((line) => line.trim())
|
|
108
|
+
.filter((line) => line.startsWith("/"));
|
|
109
|
+
return [...new Set(roots)];
|
|
110
|
+
}
|
|
89
111
|
/**
|
|
90
112
|
* The per-container setup, and a read of the marker under `dir`.
|
|
91
113
|
*
|
|
@@ -118,7 +140,7 @@ export function depsSetupCommand(workspaceDir, dir) {
|
|
|
118
140
|
'nm="$dir/node_modules"',
|
|
119
141
|
'mountpoint -q "$nm" && exit 0',
|
|
120
142
|
`disk="${DEPS_DISK}/$(printf %s "$dir" | sha256sum | cut -c1-16)"`,
|
|
121
|
-
|
|
143
|
+
`mkdir -p "$disk" "$nm" && mount --bind "$disk" "$nm" && { printf %s "$dir" > "$disk${ROOT_SUFFIX}"; exit 0; }`,
|
|
122
144
|
`echo "workspace-deps: could not put $nm on the container's disk" >&2`,
|
|
123
145
|
"exit 1"
|
|
124
146
|
].join("\n");
|
|
@@ -139,6 +161,9 @@ export function depsSetupCommand(workspaceDir, dir) {
|
|
|
139
161
|
`install -m 755 /tmp/workspace-deps-mount ${MOUNT_HELPER} || exit 1`,
|
|
140
162
|
`printf '%s\\n' ${shellQuote(wrapper)} > /tmp/workspace-deps-wrap`,
|
|
141
163
|
`for tool in ${WRAPPED.join(" ")}; do install -m 755 /tmp/workspace-deps-wrap "${WRAPPER_DIR}/$tool" || exit 1; done`,
|
|
164
|
+
// A root that is not in this checkout is left alone by the helper, and one
|
|
165
|
+
// that cannot be mounted reads as no marker below — an install, not a fault.
|
|
166
|
+
`for f in ${DEPS_DISK}/*${ROOT_SUFFIX}; do [ -f "$f" ] && ${MOUNT_HELPER} "$(cat "$f")"; done`,
|
|
142
167
|
...(dir
|
|
143
168
|
? [
|
|
144
169
|
`echo "MARKER $(cat ${shellQuote(`${dir}/node_modules/${INSTALLED_MARKER}`)} 2>/dev/null || echo -)"`
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Everything else this host catches is the kind of thing a retry clears: a
|
|
5
5
|
* container that is starting, a connection that dropped, a command that died
|
|
6
|
-
* with it. These
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* with it. These are not. They describe a Worker and a container application
|
|
7
|
+
* that cannot work together no matter how many times they are asked, and they
|
|
8
|
+
* end only when an operator deploys.
|
|
9
9
|
*
|
|
10
10
|
* **This changes what is said, not what is done.** No caller branches its
|
|
11
11
|
* control flow on the answer, and none should start: the match is on a string
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Everything else this host catches is the kind of thing a retry clears: a
|
|
5
5
|
* container that is starting, a connection that dropped, a command that died
|
|
6
|
-
* with it. These
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* with it. These are not. They describe a Worker and a container application
|
|
7
|
+
* that cannot work together no matter how many times they are asked, and they
|
|
8
|
+
* end only when an operator deploys.
|
|
9
9
|
*
|
|
10
10
|
* **This changes what is said, not what is done.** No caller branches its
|
|
11
11
|
* control flow on the answer, and none should start: the match is on a string
|
|
@@ -21,14 +21,18 @@
|
|
|
21
21
|
/**
|
|
22
22
|
* Match on the shape the backend formats, not on the prose after it.
|
|
23
23
|
*
|
|
24
|
-
* `[stage=auth]` is the stage label `
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* `
|
|
28
|
-
*
|
|
24
|
+
* `[stage=auth]` is the stage label `ContainerBackend` writes into every
|
|
25
|
+
* message it throws from that stage, and the container-application sentence is
|
|
26
|
+
* the runtime's own, surfaced verbatim through the backend's `priorExit=` field.
|
|
27
|
+
* `prepared images` is in both of the backend's refusals to start without an
|
|
28
|
+
* image — none prepared at all, and none under the key it was asked for — and
|
|
29
|
+
* survives the reconnect wrapper's truncation of either. All are stabler than
|
|
30
|
+
* the explanations they introduce, and none has a `code` to key on the way a
|
|
31
|
+
* lost exec does.
|
|
29
32
|
*/
|
|
30
33
|
const AUTH_STAGE = "[stage=auth]";
|
|
31
34
|
const NO_APPLICATION = "no container application";
|
|
35
|
+
const NO_IMAGE = "prepared images";
|
|
32
36
|
/**
|
|
33
37
|
* Whether this error says the deployment is wrong, and what to say about it.
|
|
34
38
|
*
|
|
@@ -54,5 +58,16 @@ export function deploymentFault(err) {
|
|
|
54
58
|
"creates it — deleting it does not cause it to be rebuilt — so this " +
|
|
55
59
|
"clears when an operator redeploys, and not before."
|
|
56
60
|
};
|
|
61
|
+
if (text.includes(NO_IMAGE))
|
|
62
|
+
return {
|
|
63
|
+
summary: "this Worker prepared no image for the workspace container",
|
|
64
|
+
remedy: "the container application has no image under the name this workspace " +
|
|
65
|
+
"starts, so no container can start. The workspace schedules its own " +
|
|
66
|
+
'containers, which takes `scheduling_policy: "durable_object"` and an ' +
|
|
67
|
+
"`images` entry for that name in the Worker's containers block, and an " +
|
|
68
|
+
"application created under another policy is deleted before that " +
|
|
69
|
+
"deploy rather than switched. This clears when an operator redeploys " +
|
|
70
|
+
"with both in place, and not before."
|
|
71
|
+
};
|
|
57
72
|
return undefined;
|
|
58
73
|
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { Workspace } from "@cloudflare/computer";
|
|
2
|
+
export interface DepsSnapshotRecord {
|
|
3
|
+
/** The platform's snapshot id. */
|
|
4
|
+
id: string;
|
|
5
|
+
/** In bytes, as the platform reported it. */
|
|
6
|
+
size: number;
|
|
7
|
+
/** The image reference it was taken on — see the file comment. */
|
|
8
|
+
image: string;
|
|
9
|
+
/** Where the install ran. */
|
|
10
|
+
dir: string;
|
|
11
|
+
/** The install fingerprint its marker holds. */
|
|
12
|
+
fingerprint: string;
|
|
13
|
+
/** Every package root with a tree on the disk, and its inputs' digest then. */
|
|
14
|
+
roots: {
|
|
15
|
+
dir: string;
|
|
16
|
+
fingerprint: string | null;
|
|
17
|
+
}[];
|
|
18
|
+
/** {@link packageRootSet} of the checkout then. */
|
|
19
|
+
rootSet: string;
|
|
20
|
+
at: number;
|
|
21
|
+
}
|
|
22
|
+
/** What a restore is checked against: the install a cold start would run. */
|
|
23
|
+
export interface ExpectedTree {
|
|
24
|
+
dir: string;
|
|
25
|
+
fingerprint: string;
|
|
26
|
+
}
|
|
27
|
+
/** A tree an install finished, as its record names it — `at` tells two apart. */
|
|
28
|
+
export interface InstalledTree extends ExpectedTree {
|
|
29
|
+
at: number;
|
|
30
|
+
}
|
|
31
|
+
export interface DepsSnapshotDeps {
|
|
32
|
+
storage: DurableObjectStorage;
|
|
33
|
+
container: () => Container | undefined;
|
|
34
|
+
/** The image a start from no snapshot would boot. */
|
|
35
|
+
image: () => string | undefined;
|
|
36
|
+
workspace: () => Workspace;
|
|
37
|
+
rootFingerprint: (dir: string) => Promise<string | null>;
|
|
38
|
+
/** The tree the install record says this container holds. */
|
|
39
|
+
tree: () => Promise<{
|
|
40
|
+
dir: string;
|
|
41
|
+
fingerprint: string | null;
|
|
42
|
+
at: number;
|
|
43
|
+
} | undefined>;
|
|
44
|
+
tag: () => string;
|
|
45
|
+
id: () => string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Which package roots a checkout has, as one digest: every directory under
|
|
49
|
+
* `dir` with a `package.json`, and the submodules `.gitmodules` declares — one a
|
|
50
|
+
* bootstrap clones in the container has no `package.json` here until then.
|
|
51
|
+
*/
|
|
52
|
+
export declare function packageRootSet(fs: Workspace["fs"], dir: string): Promise<string>;
|
|
53
|
+
export declare class DepsSnapshot {
|
|
54
|
+
#private;
|
|
55
|
+
private readonly deps;
|
|
56
|
+
readonly launch: {
|
|
57
|
+
readonly containerSnapshot: ContainerSnapshotRestoreParams | undefined;
|
|
58
|
+
};
|
|
59
|
+
constructor(deps: DepsSnapshotDeps);
|
|
60
|
+
/**
|
|
61
|
+
* Restore with the next start if the record fits it, and say which record.
|
|
62
|
+
*
|
|
63
|
+
* `expected` is a thunk because it reads the checkout, and a workspace with no
|
|
64
|
+
* snapshot or one from another image should not pay for that.
|
|
65
|
+
*/
|
|
66
|
+
arm(expected: () => Promise<ExpectedTree | undefined>): Promise<DepsSnapshotRecord | undefined>;
|
|
67
|
+
/** Forget an armed restore, taken or not. */
|
|
68
|
+
disarm(): void;
|
|
69
|
+
/**
|
|
70
|
+
* Whether the container that came up holds `record`'s tree, judged by the
|
|
71
|
+
* marker the setup read in it.
|
|
72
|
+
*
|
|
73
|
+
* Drops the record when a start took it and the tree is not there; leaves it
|
|
74
|
+
* when no start did, since nothing was learned about it.
|
|
75
|
+
*/
|
|
76
|
+
restored(record: DepsSnapshotRecord, marker: string | null | undefined): Promise<boolean>;
|
|
77
|
+
/**
|
|
78
|
+
* Snapshot the running container, which has just installed `tree`, after any
|
|
79
|
+
* snapshot already in flight — a later install's request is never dropped for
|
|
80
|
+
* an earlier one's.
|
|
81
|
+
*
|
|
82
|
+
* Never throws: a container that cannot be snapshotted installs again next
|
|
83
|
+
* time, which is today's cost and no worse.
|
|
84
|
+
*/
|
|
85
|
+
take(tree: InstalledTree): Promise<void>;
|
|
86
|
+
/** Snapshots in flight, finished — for whatever is about to stop the container. */
|
|
87
|
+
settled(): Promise<void>;
|
|
88
|
+
/**
|
|
89
|
+
* The entries that ask for `tree`'s snapshot, for the install to write with
|
|
90
|
+
* its verdict — see the file comment, and {@link takeDue}.
|
|
91
|
+
*/
|
|
92
|
+
dueEntries(tree: InstalledTree): Record<string, unknown>;
|
|
93
|
+
/** Take the snapshot an install asked for, if one is waiting. */
|
|
94
|
+
takeDue(): Promise<void>;
|
|
95
|
+
/** The container went, and the disk an asked-for snapshot was of with it. */
|
|
96
|
+
forgetDue(): Promise<void>;
|
|
97
|
+
get(): Promise<DepsSnapshotRecord | undefined>;
|
|
98
|
+
/**
|
|
99
|
+
* Start from another workspace's snapshot — one taken for the same checkout
|
|
100
|
+
* path on the same image, as a worktree shares with the checkout it was cut
|
|
101
|
+
* from.
|
|
102
|
+
*
|
|
103
|
+
* Kept out when this workspace's own snapshot already fits, and refused when
|
|
104
|
+
* the offered one does not.
|
|
105
|
+
*/
|
|
106
|
+
seed(record: DepsSnapshotRecord, expected: () => Promise<ExpectedTree | undefined>): Promise<{
|
|
107
|
+
seeded: boolean;
|
|
108
|
+
reason?: string;
|
|
109
|
+
}>;
|
|
110
|
+
}
|