@dynamicagents/plugins 0.12.0 → 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 +58 -77
- 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 +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 +342 -0
- package/dist/claude-code/run.d.ts +209 -112
- package/dist/claude-code/run.js +339 -138
- 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 +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-fault.d.ts +3 -3
- package/dist/{computer/host → workspace}/container-fault.js +23 -8
- 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 +43 -47
- package/dist/workspace/index.js +52 -112
- package/dist/{computer/host → workspace}/install-job.d.ts +4 -4
- package/dist/{computer/host → workspace}/install-job.js +276 -176
- package/dist/{computer → workspace}/install.d.ts +8 -8
- package/dist/{computer → workspace}/install.js +3 -3
- package/dist/{computer/host/workspace.d.ts → workspace/object.d.ts} +105 -27
- package/dist/{computer/host/workspace.js → workspace/object.js} +217 -48
- 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 +1 -1
- package/package.json +16 -36
- 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}/container-deps.d.ts +0 -0
- /package/dist/{computer/host → workspace}/container-deps.js +0 -0
- /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();
|
|
@@ -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,94 @@
|
|
|
1
|
+
import { type WorkspaceHost } from "./open.js";
|
|
2
|
+
/**
|
|
3
|
+
* Running a command in a workspace's container, from outside the workspace
|
|
4
|
+
* object: what `/computer`'s tools and `/repo`'s git share, and how a host
|
|
5
|
+
* reaches its own container.
|
|
6
|
+
*/
|
|
7
|
+
/** A command that has not finished in this long is a hung command. */
|
|
8
|
+
export declare const DEFAULT_TIMEOUT_MS: number;
|
|
9
|
+
/** Where checkouts live, in the container and in the workspace alike. */
|
|
10
|
+
export declare const DEFAULT_CWD = "/workspace";
|
|
11
|
+
/** Which workspace a caller reaches, and how its commands run there. */
|
|
12
|
+
export interface WorkspaceClientConfig {
|
|
13
|
+
/**
|
|
14
|
+
* The Durable Object namespace holding workspaces, closed over at
|
|
15
|
+
* instantiation so it is never model input.
|
|
16
|
+
*/
|
|
17
|
+
binding: DurableObjectNamespace<WorkspaceHost>;
|
|
18
|
+
/**
|
|
19
|
+
* Which workspace this agent gets, by name. A thunk because the useful name —
|
|
20
|
+
* caller plus repository — does not exist when the plugin list is built.
|
|
21
|
+
*
|
|
22
|
+
* One workspace is one container is one repository: `@cloudflare/computer`
|
|
23
|
+
* pairs a Durable Object with exactly one container, so "never mix two
|
|
24
|
+
* repositories" is structural here rather than a convention to maintain.
|
|
25
|
+
*/
|
|
26
|
+
workspaceName: () => string;
|
|
27
|
+
/** Working directory for every command. Defaults to `/workspace`. */
|
|
28
|
+
cwd?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Run every command through this shell, e.g. `"bash"`. Unset, the command
|
|
31
|
+
* string goes to the runtime as-is and lands on whatever `/bin/sh` is.
|
|
32
|
+
*
|
|
33
|
+
* Worth setting, because "whatever `/bin/sh` is" is **dash** on Debian and
|
|
34
|
+
* Ubuntu, and a model writing shell writes *bash*. `${PIPESTATUS[0]}` is the
|
|
35
|
+
* one that costs most — a bash builtin a model reaches for to read a piped exit
|
|
36
|
+
* code, which dash fails with exit 2, after the command it wrapped has already
|
|
37
|
+
* run. It is one member of a family (`[[ ]]`, arrays, `set -o pipefail`,
|
|
38
|
+
* process substitution), so the fix is the shell rather than a note in a prompt
|
|
39
|
+
* telling the model to write POSIX.
|
|
40
|
+
*
|
|
41
|
+
* Set it only if the image actually has that shell: a missing one fails *every*
|
|
42
|
+
* command rather than the bash-flavoured ones.
|
|
43
|
+
*/
|
|
44
|
+
shell?: string;
|
|
45
|
+
/** Per-command timeout. Defaults to ten minutes. */
|
|
46
|
+
timeoutMs?: number;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* An `exec` bound to this workspace, for plugins that need a shell but should
|
|
50
|
+
* not own a container.
|
|
51
|
+
*
|
|
52
|
+
* `@dynamicagents/plugins/repo` is the reason this exists: it needs `git` on a real
|
|
53
|
+
* shell, but depending on this module would weld the two together and stop a
|
|
54
|
+
* host from pointing it at its own container. Structurally typed for the same
|
|
55
|
+
* reason — the two plugins compose without either importing the other.
|
|
56
|
+
*
|
|
57
|
+
* ## The agent tools' `env` is deliberately **not** merged here
|
|
58
|
+
*
|
|
59
|
+
* `/computer`'s `ComputerConfig.env` is merged into every command its tools
|
|
60
|
+
* run; this takes no environment of its own.
|
|
61
|
+
*
|
|
62
|
+
* What this hands out is a raw shell to *another plugin*, whose commands this
|
|
63
|
+
* module neither writes nor sees. `/repo` runs its unauthenticated git through
|
|
64
|
+
* it and pins the environment those need — `GIT_TERMINAL_PROMPT=0` plus the
|
|
65
|
+
* model-authored values it passes as variables. A host environment merged
|
|
66
|
+
* underneath lets through every key it does *not* pin: `http_proxy`,
|
|
67
|
+
* `GIT_PROXY_COMMAND`, `GIT_SSL_CAINFO`, `GIT_CONFIG_GLOBAL`, `GIT_EXEC_PATH` —
|
|
68
|
+
* the last two redirect the config git reads and relocate its helper binaries.
|
|
69
|
+
*
|
|
70
|
+
* No token is at stake, since the credentialed operations do not run here at
|
|
71
|
+
* all. The point is that changing what git does inside another plugin's commands
|
|
72
|
+
* should not be something a tools plugin's config field does by accident,
|
|
73
|
+
* from a host that only meant to set a registry. A host that wants it composes
|
|
74
|
+
* it where the merge is visible:
|
|
75
|
+
*
|
|
76
|
+
* ```ts
|
|
77
|
+
* exec: (command, options) =>
|
|
78
|
+
* workspaceExec(config)(command, {
|
|
79
|
+
* ...options,
|
|
80
|
+
* env: { ...mine, ...options?.env }
|
|
81
|
+
* });
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export declare function workspaceExec(config: WorkspaceClientConfig): (command: string, options?: {
|
|
85
|
+
cwd?: string;
|
|
86
|
+
env?: Record<string, string | undefined>;
|
|
87
|
+
timeout?: number;
|
|
88
|
+
runtime?: unknown;
|
|
89
|
+
}) => Promise<{
|
|
90
|
+
success: boolean;
|
|
91
|
+
stdout: string;
|
|
92
|
+
stderr: string;
|
|
93
|
+
exitCode: number;
|
|
94
|
+
}>;
|
|
@@ -0,0 +1,153 @@
|
|
|
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 { cancelledNote } from "./format.js";
|
|
54
|
+
import { openWorkspace, workspaceNameFromRuntime } from "./open.js";
|
|
55
|
+
import { withShell } from "./shell.js";
|
|
56
|
+
/**
|
|
57
|
+
* Running a command in a workspace's container, from outside the workspace
|
|
58
|
+
* object: what `/computer`'s tools and `/repo`'s git share, and how a host
|
|
59
|
+
* reaches its own container.
|
|
60
|
+
*/
|
|
61
|
+
/** A command that has not finished in this long is a hung command. */
|
|
62
|
+
export const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
63
|
+
/** Where checkouts live, in the container and in the workspace alike. */
|
|
64
|
+
export const DEFAULT_CWD = "/workspace";
|
|
65
|
+
/**
|
|
66
|
+
* An `exec` bound to this workspace, for plugins that need a shell but should
|
|
67
|
+
* not own a container.
|
|
68
|
+
*
|
|
69
|
+
* `@dynamicagents/plugins/repo` is the reason this exists: it needs `git` on a real
|
|
70
|
+
* shell, but depending on this module would weld the two together and stop a
|
|
71
|
+
* host from pointing it at its own container. Structurally typed for the same
|
|
72
|
+
* reason — the two plugins compose without either importing the other.
|
|
73
|
+
*
|
|
74
|
+
* ## The agent tools' `env` is deliberately **not** merged here
|
|
75
|
+
*
|
|
76
|
+
* `/computer`'s `ComputerConfig.env` is merged into every command its tools
|
|
77
|
+
* run; this takes no environment of its own.
|
|
78
|
+
*
|
|
79
|
+
* What this hands out is a raw shell to *another plugin*, whose commands this
|
|
80
|
+
* module neither writes nor sees. `/repo` runs its unauthenticated git through
|
|
81
|
+
* it and pins the environment those need — `GIT_TERMINAL_PROMPT=0` plus the
|
|
82
|
+
* model-authored values it passes as variables. A host environment merged
|
|
83
|
+
* underneath lets through every key it does *not* pin: `http_proxy`,
|
|
84
|
+
* `GIT_PROXY_COMMAND`, `GIT_SSL_CAINFO`, `GIT_CONFIG_GLOBAL`, `GIT_EXEC_PATH` —
|
|
85
|
+
* the last two redirect the config git reads and relocate its helper binaries.
|
|
86
|
+
*
|
|
87
|
+
* No token is at stake, since the credentialed operations do not run here at
|
|
88
|
+
* all. The point is that changing what git does inside another plugin's commands
|
|
89
|
+
* should not be something a tools plugin's config field does by accident,
|
|
90
|
+
* from a host that only meant to set a registry. A host that wants it composes
|
|
91
|
+
* it where the merge is visible:
|
|
92
|
+
*
|
|
93
|
+
* ```ts
|
|
94
|
+
* exec: (command, options) =>
|
|
95
|
+
* workspaceExec(config)(command, {
|
|
96
|
+
* ...options,
|
|
97
|
+
* env: { ...mine, ...options?.env }
|
|
98
|
+
* });
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
export function workspaceExec(config) {
|
|
102
|
+
return async (command, options) => {
|
|
103
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
104
|
+
try {
|
|
105
|
+
// The caller forwards its sub-agent's runtime opaquely; only this side
|
|
106
|
+
// knows the key it might carry. That is what lets `/repo` reach a sub-agent's
|
|
107
|
+
// shared workspace without importing anything from here.
|
|
108
|
+
const name = workspaceNameFromRuntime(options?.runtime) ?? config.workspaceName();
|
|
109
|
+
const ws = __addDisposableResource(env_1, await openWorkspace(config.binding.get(config.binding.idFromName(name))), false);
|
|
110
|
+
const env = options?.env
|
|
111
|
+
? Object.fromEntries(Object.entries(options.env).filter((entry) => entry[1] !== undefined))
|
|
112
|
+
: undefined;
|
|
113
|
+
// Named rather than inlined, because the note below has to state it: a
|
|
114
|
+
// ceiling the model cannot see is one it cannot work within.
|
|
115
|
+
const timeoutMs = options?.timeout ?? config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
116
|
+
// `withShell`, never `withShellTranscript`: the caller reads this result in
|
|
117
|
+
// code. `/repo` compares `stdout` against a URL, tests it for emptiness to
|
|
118
|
+
// decide a tree is clean, and reads a sha out of it to push — all of which
|
|
119
|
+
// need `stdout` to carry the answer alone, with git's diagnostics on the
|
|
120
|
+
// channel git put them on.
|
|
121
|
+
const handle = __addDisposableResource(env_1, await ws.runtime.exec(withShell(command, config.shell), {
|
|
122
|
+
cwd: options?.cwd ?? config.cwd ?? DEFAULT_CWD,
|
|
123
|
+
encoding: "utf8",
|
|
124
|
+
timeoutMs,
|
|
125
|
+
...(env ? { env } : {})
|
|
126
|
+
}), false);
|
|
127
|
+
const result = await handle.result();
|
|
128
|
+
const killed = cancelledNote(result.status, result.exitCode, timeoutMs);
|
|
129
|
+
return {
|
|
130
|
+
// `/repo` branches on `success`, which this runtime does not report — it
|
|
131
|
+
// has `status` and `exitCode`. Derived from the exit code rather than the
|
|
132
|
+
// status, because a command that runs and fails is `completed` here.
|
|
133
|
+
success: result.exitCode === 0,
|
|
134
|
+
stdout: result.stdout,
|
|
135
|
+
// Appended rather than substituted: a command killed at the ceiling may
|
|
136
|
+
// well have written plenty first, and that output is the more useful half.
|
|
137
|
+
// This is the only field the note can travel on — `stdout` is a data
|
|
138
|
+
// channel the caller parses, and the four fields are the whole contract.
|
|
139
|
+
stderr: killed
|
|
140
|
+
? [result.stderr, killed].filter(Boolean).join("\n")
|
|
141
|
+
: result.stderr,
|
|
142
|
+
exitCode: result.exitCode
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
catch (e_1) {
|
|
146
|
+
env_1.error = e_1;
|
|
147
|
+
env_1.hasError = true;
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
__disposeResources(env_1);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { WorkspaceRuntimeStatus } from "@cloudflare/computer";
|
|
2
|
+
/**
|
|
3
|
+
* Text shared by the workspace and the tools over it: the output bound, the
|
|
4
|
+
* elapsed-time phrase, and what a killed command is told.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Middle-out truncation, so both the first error and the final summary survive.
|
|
8
|
+
*
|
|
9
|
+
* The guard is not defensive padding. Without it a `max` at or below the marker's
|
|
10
|
+
* own length makes `half` zero or negative, and `slice(-0)` is `slice(0)` — the
|
|
11
|
+
* *whole* string — so the function returns more than it was given: 500 characters
|
|
12
|
+
* in, 543 out at `max: 80`. A silent inversion of the one thing it does,
|
|
13
|
+
* reachable from a public config field.
|
|
14
|
+
*/
|
|
15
|
+
export declare function truncateOutput(text: string, max: number): string;
|
|
16
|
+
/** Elapsed time in the shape a sentence wants. */
|
|
17
|
+
export declare function humanMs(ms: number): string;
|
|
18
|
+
/**
|
|
19
|
+
* Why a command has nothing to show for itself.
|
|
20
|
+
*
|
|
21
|
+
* {@link renderResult} states a non-`completed` status on its verdict line;
|
|
22
|
+
* {@link file://./exec.ts workspaceExec} has no verdict line, only the four fields its caller
|
|
23
|
+
* branches on. A *killed* process writes nothing, so without this a `git clone`
|
|
24
|
+
* that hit the ten-minute ceiling reaches the model through
|
|
25
|
+
* `@dynamicagents/plugins/repo` as `clone failed:` with an empty reason — which reads
|
|
26
|
+
* like a bug in the plugin rather than a limit it can work within.
|
|
27
|
+
*
|
|
28
|
+
* Only `cancelled`. A `failed` status is an ordinary non-zero exit, where the
|
|
29
|
+
* command's own stderr is the better explanation and this would be noise on top
|
|
30
|
+
* of it.
|
|
31
|
+
*
|
|
32
|
+
* Both plausible causes are named because the exit code cannot separate them:
|
|
33
|
+
* 137 is SIGKILL, which is what the timeout sends *and* what the kernel sends a
|
|
34
|
+
* container that ran out of memory.
|
|
35
|
+
*/
|
|
36
|
+
export declare function cancelledNote(status: WorkspaceRuntimeStatus | undefined, exitCode: number, timeoutMs: number): string | undefined;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text shared by the workspace and the tools over it: the output bound, the
|
|
3
|
+
* elapsed-time phrase, and what a killed command is told.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Middle-out truncation, so both the first error and the final summary survive.
|
|
7
|
+
*
|
|
8
|
+
* The guard is not defensive padding. Without it a `max` at or below the marker's
|
|
9
|
+
* own length makes `half` zero or negative, and `slice(-0)` is `slice(0)` — the
|
|
10
|
+
* *whole* string — so the function returns more than it was given: 500 characters
|
|
11
|
+
* in, 543 out at `max: 80`. A silent inversion of the one thing it does,
|
|
12
|
+
* reachable from a public config field.
|
|
13
|
+
*/
|
|
14
|
+
export function truncateOutput(text, max) {
|
|
15
|
+
if (text.length <= max)
|
|
16
|
+
return text;
|
|
17
|
+
const marker = (dropped) => `\n\n… [${dropped} characters omitted from the middle] …\n\n`;
|
|
18
|
+
// No budget for two halves plus the marker: keep the head, which is where the
|
|
19
|
+
// first error is, and say nothing clever.
|
|
20
|
+
const half = Math.floor((max - marker(text.length).length) / 2);
|
|
21
|
+
if (half < 1)
|
|
22
|
+
return text.slice(0, Math.max(0, max));
|
|
23
|
+
return (text.slice(0, half) + marker(text.length - half * 2) + text.slice(-half));
|
|
24
|
+
}
|
|
25
|
+
/** Elapsed time in the shape a sentence wants. */
|
|
26
|
+
export function humanMs(ms) {
|
|
27
|
+
const s = Math.round(ms / 1000);
|
|
28
|
+
return s < 60
|
|
29
|
+
? `${s}s`
|
|
30
|
+
: `${Math.floor(s / 60)}m${String(s % 60).padStart(2, "0")}s`;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Why a command has nothing to show for itself.
|
|
34
|
+
*
|
|
35
|
+
* {@link renderResult} states a non-`completed` status on its verdict line;
|
|
36
|
+
* {@link file://./exec.ts workspaceExec} has no verdict line, only the four fields its caller
|
|
37
|
+
* branches on. A *killed* process writes nothing, so without this a `git clone`
|
|
38
|
+
* that hit the ten-minute ceiling reaches the model through
|
|
39
|
+
* `@dynamicagents/plugins/repo` as `clone failed:` with an empty reason — which reads
|
|
40
|
+
* like a bug in the plugin rather than a limit it can work within.
|
|
41
|
+
*
|
|
42
|
+
* Only `cancelled`. A `failed` status is an ordinary non-zero exit, where the
|
|
43
|
+
* command's own stderr is the better explanation and this would be noise on top
|
|
44
|
+
* of it.
|
|
45
|
+
*
|
|
46
|
+
* Both plausible causes are named because the exit code cannot separate them:
|
|
47
|
+
* 137 is SIGKILL, which is what the timeout sends *and* what the kernel sends a
|
|
48
|
+
* container that ran out of memory.
|
|
49
|
+
*/
|
|
50
|
+
export function cancelledNote(status, exitCode, timeoutMs) {
|
|
51
|
+
if (status !== "cancelled")
|
|
52
|
+
return undefined;
|
|
53
|
+
return (`the command was killed rather than exiting on its own (exit ${exitCode}), so ` +
|
|
54
|
+
`anything it had not yet written is gone. The two usual causes are the ` +
|
|
55
|
+
`per-command time limit — ${humanMs(timeoutMs)} here — and the container ` +
|
|
56
|
+
`running out of memory.`);
|
|
57
|
+
}
|