@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
package/dist/computer/paths.d.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Which paths these tools act on, and the sentence a refused one gets.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Each refused directory is also skipped by every walk, though the walk steps
|
|
5
|
+
* over more than it refuses — see {@link WALK_SKIPS}. `.git` is refused as
|
|
6
|
+
* policy: a model that edits `.git/HEAD` corrupts a checkout in a way that
|
|
7
|
+
* surfaces much later. `node_modules` is refused as a fact: it lives on the
|
|
8
|
+
* container's disk (see `../workspace/container-deps.ts`), so these tools, which read
|
|
9
|
+
* the workspace, would find it empty.
|
|
9
10
|
*
|
|
10
11
|
* Everything here is a string comparison: no filesystem, no `await`, which is
|
|
11
12
|
* what lets {@link guardPath} run before a tool opens the workspace at all.
|
|
12
13
|
*/
|
|
13
|
-
export declare function isContainerOnly(path: string): boolean;
|
|
14
14
|
/**
|
|
15
15
|
* Is this path inside git's internal state?
|
|
16
16
|
*
|
|
@@ -18,33 +18,58 @@ export declare function isContainerOnly(path: string): boolean;
|
|
|
18
18
|
* checkout in a way that surfaces much later as an inexplicable git failure.
|
|
19
19
|
*
|
|
20
20
|
* Exact segment rather than substring, so `.gitignore`, `.gitattributes` and
|
|
21
|
-
* `.github/` are untouched
|
|
21
|
+
* `.github/` are untouched.
|
|
22
22
|
*/
|
|
23
23
|
export declare function isGitInternal(path: string): boolean;
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
25
|
+
* Where a session's own state lives inside a workspace, as a directory name.
|
|
26
26
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
27
|
+
* Every Claude Code session keeps its `CLAUDE_CONFIG_DIR` in a directory of
|
|
28
|
+
* this name at the workspace root — the Durable Object's storage, so a
|
|
29
|
+
* conversation outlives its container and a later session can continue it. See
|
|
30
|
+
* {@link file://../claude-code/run.ts SESSION_CONFIG_DIR}.
|
|
31
|
+
*
|
|
32
|
+
* **Spelled here as well as in the plugin that sets the variable**, because the
|
|
33
|
+
* module that has to step over it is the walk below, and neither may import the
|
|
34
|
+
* other: `/computer` reaching into `/claude-code` would pull a whole plugin into
|
|
35
|
+
* every agent that installs the file tools, and `/claude-code` reaches no other
|
|
36
|
+
* subpath — `verify:exports` refuses both. A spec in `/claude-code` holds the
|
|
37
|
+
* two spellings to one.
|
|
38
|
+
*/
|
|
39
|
+
export declare const SESSION_STATE_DIR = ".claude-sessions";
|
|
40
|
+
/**
|
|
41
|
+
* What every walk steps over, as directory names. Both the workspace's `glob`
|
|
42
|
+
* and the `grep` tool hand these to the store as `**\/<name>` exclusions, which
|
|
43
|
+
* prunes them: no directory named here is descended into, so none costs a read.
|
|
44
|
+
*
|
|
45
|
+
* {@link SESSION_STATE_DIR} is here for a reason the refused directories do not
|
|
46
|
+
* share: a transcript is a verbatim record of everything a session read, so a
|
|
47
|
+
* walk that descended into it would answer a `grep` for a line of source with
|
|
48
|
+
* the session that quoted it.
|
|
49
|
+
*/
|
|
50
|
+
export declare const WALK_SKIPS: readonly [".git", "node_modules", ".claude-sessions"];
|
|
51
|
+
/**
|
|
52
|
+
* The path check every file tool and every workspace call makes, before it
|
|
53
|
+
* opens anything.
|
|
54
|
+
*
|
|
55
|
+
* Returns the sentence to hand back, or `undefined` to proceed. One string
|
|
56
|
+
* comparison — no `stat`, no round trip — which is why every file tool can
|
|
57
|
+
* afford to call it before the workspace is opened.
|
|
30
58
|
*
|
|
31
59
|
* ## A redirect, not a boundary
|
|
32
60
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* names the tools it belongs to. Both catch the case that actually happens — a
|
|
36
|
-
* model reaching into `.git/HEAD` to fix a merge, or reading a dependency's
|
|
37
|
-
* source and finding nothing there.
|
|
61
|
+
* The note names the tools repository work belongs to, because the case that
|
|
62
|
+
* actually happens is a model reaching into `.git/HEAD` to fix a merge.
|
|
38
63
|
*
|
|
39
|
-
* It is not containment, and this is the only place worth saying so.
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* directly.
|
|
64
|
+
* It is not containment, and this is the only place worth saying so. An agent
|
|
65
|
+
* that can write files here almost always holds `bash` too, which reads and
|
|
66
|
+
* writes `.git` directly.
|
|
43
67
|
*
|
|
44
68
|
* Symlinks are not resolved, for the same reason: a tracked
|
|
45
69
|
* `docs/notes.md -> ../.git/config` only matters to an agent given these tools
|
|
46
|
-
* *without* the shell
|
|
47
|
-
*
|
|
70
|
+
* *without* the shell. A read-only install — `restrictTools` down to `grep`,
|
|
71
|
+
* with Think's writing tools left out — is that agent, and it cannot write. If a writing agent
|
|
72
|
+
* without the shell ever exists, the **write** path is the half to restore —
|
|
48
73
|
* `.git/config` is an input to the credentialed push, and a planted hook runs
|
|
49
74
|
* under a container-side `repo_commit`. Reading `.git` discloses nothing that is
|
|
50
75
|
* not already readable, so the read half is not worth an `lstat` per call. The
|
package/dist/computer/paths.js
CHANGED
|
@@ -1,35 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Which paths these tools act on, and the sentence a refused one gets.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Each refused directory is also skipped by every walk, though the walk steps
|
|
5
|
+
* over more than it refuses — see {@link WALK_SKIPS}. `.git` is refused as
|
|
6
|
+
* policy: a model that edits `.git/HEAD` corrupts a checkout in a way that
|
|
7
|
+
* surfaces much later. `node_modules` is refused as a fact: it lives on the
|
|
8
|
+
* container's disk (see `../workspace/container-deps.ts`), so these tools, which read
|
|
9
|
+
* the workspace, would find it empty.
|
|
9
10
|
*
|
|
10
11
|
* Everything here is a string comparison: no filesystem, no `await`, which is
|
|
11
12
|
* what lets {@link guardPath} run before a tool opens the workspace at all.
|
|
12
13
|
*/
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
* Only `node_modules` today, and it is not configurable here because it is not
|
|
17
|
-
* our choice: `computerd` applies its own `DEFAULT_IGNORE` container-side, and
|
|
18
|
-
* this list exists to *predict* that so a read can explain itself rather than
|
|
19
|
-
* reporting a missing file. A dependency the model can plainly see in a `ls` but
|
|
20
|
-
* cannot read is the kind of thing that sends it hunting for the wrong bug.
|
|
21
|
-
*/
|
|
22
|
-
const CONTAINER_ONLY_SEGMENTS = ["node_modules"];
|
|
23
|
-
export function isContainerOnly(path) {
|
|
24
|
-
return path
|
|
25
|
-
.split("/")
|
|
26
|
-
.some((segment) => CONTAINER_ONLY_SEGMENTS.includes(segment));
|
|
27
|
-
}
|
|
28
|
-
/** The sentence a container-only path gets instead of "not found". */
|
|
29
|
-
function containerOnlyNote(path, verb) {
|
|
30
|
-
return (`${path} is inside node_modules, which lives only in the container and is ` +
|
|
31
|
-
`not part of the durable workspace — ${verb} cannot see it. Use \`sb_exec\` ` +
|
|
32
|
-
`(for example \`cat ${path}\`) to read it through the shell instead.`);
|
|
14
|
+
/** Whether any segment of `path` is exactly `segment`. */
|
|
15
|
+
function hasSegment(path, segment) {
|
|
16
|
+
return path.split("/").includes(segment);
|
|
33
17
|
}
|
|
34
18
|
/**
|
|
35
19
|
* Is this path inside git's internal state?
|
|
@@ -38,10 +22,43 @@ function containerOnlyNote(path, verb) {
|
|
|
38
22
|
* checkout in a way that surfaces much later as an inexplicable git failure.
|
|
39
23
|
*
|
|
40
24
|
* Exact segment rather than substring, so `.gitignore`, `.gitattributes` and
|
|
41
|
-
* `.github/` are untouched
|
|
25
|
+
* `.github/` are untouched.
|
|
42
26
|
*/
|
|
43
27
|
export function isGitInternal(path) {
|
|
44
|
-
return path
|
|
28
|
+
return hasSegment(path, ".git");
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Where a session's own state lives inside a workspace, as a directory name.
|
|
32
|
+
*
|
|
33
|
+
* Every Claude Code session keeps its `CLAUDE_CONFIG_DIR` in a directory of
|
|
34
|
+
* this name at the workspace root — the Durable Object's storage, so a
|
|
35
|
+
* conversation outlives its container and a later session can continue it. See
|
|
36
|
+
* {@link file://../claude-code/run.ts SESSION_CONFIG_DIR}.
|
|
37
|
+
*
|
|
38
|
+
* **Spelled here as well as in the plugin that sets the variable**, because the
|
|
39
|
+
* module that has to step over it is the walk below, and neither may import the
|
|
40
|
+
* other: `/computer` reaching into `/claude-code` would pull a whole plugin into
|
|
41
|
+
* every agent that installs the file tools, and `/claude-code` reaches no other
|
|
42
|
+
* subpath — `verify:exports` refuses both. A spec in `/claude-code` holds the
|
|
43
|
+
* two spellings to one.
|
|
44
|
+
*/
|
|
45
|
+
export const SESSION_STATE_DIR = ".claude-sessions";
|
|
46
|
+
/**
|
|
47
|
+
* What every walk steps over, as directory names. Both the workspace's `glob`
|
|
48
|
+
* and the `grep` tool hand these to the store as `**\/<name>` exclusions, which
|
|
49
|
+
* prunes them: no directory named here is descended into, so none costs a read.
|
|
50
|
+
*
|
|
51
|
+
* {@link SESSION_STATE_DIR} is here for a reason the refused directories do not
|
|
52
|
+
* share: a transcript is a verbatim record of everything a session read, so a
|
|
53
|
+
* walk that descended into it would answer a `grep` for a line of source with
|
|
54
|
+
* the session that quoted it.
|
|
55
|
+
*/
|
|
56
|
+
export const WALK_SKIPS = [".git", "node_modules", SESSION_STATE_DIR];
|
|
57
|
+
/** The sentence a `node_modules` path gets. */
|
|
58
|
+
function dependencyTreeNote(path, verb) {
|
|
59
|
+
return (`${path} is inside node_modules, which lives on the container's disk rather ` +
|
|
60
|
+
`than in the workspace ${verb} works on. Use bash there — ` +
|
|
61
|
+
`\`cat\`, \`ls\`, \`rg\`.`);
|
|
45
62
|
}
|
|
46
63
|
/**
|
|
47
64
|
* The sentence a `.git` path gets.
|
|
@@ -49,9 +66,9 @@ export function isGitInternal(path) {
|
|
|
49
66
|
* Names a route deliberately: a refusal with no destination is worse than no
|
|
50
67
|
* refusal, because the model retries and then works around it.
|
|
51
68
|
*
|
|
52
|
-
* What it must never name is `
|
|
69
|
+
* What it must never name is `bash`. That hands back the exact capability
|
|
53
70
|
* being withheld, in the one place the model is already looking for a way around
|
|
54
|
-
* it, with the tool's own authority behind it. `
|
|
71
|
+
* it, with the tool's own authority behind it. `bash` is unguarded because a
|
|
55
72
|
* shell takes an opaque command string and pattern-matching git out of one is
|
|
56
73
|
* neither reliable nor this guard's job — a fact about the implementation, not a
|
|
57
74
|
* route to advertise.
|
|
@@ -60,34 +77,32 @@ function gitInternalNote(path, verb) {
|
|
|
60
77
|
return (`${path} is inside .git — git's internal state, which ${verb} does not touch. ` +
|
|
61
78
|
`Reading it tells you less than the repository tools do, and writing it ` +
|
|
62
79
|
`corrupts the checkout. Repository work goes through the repo tools ` +
|
|
63
|
-
`(\`repo_status\`, \`repo_diff\`, \`repo_commit\`, \`repo_push\`)
|
|
64
|
-
`
|
|
65
|
-
`
|
|
80
|
+
`(\`repo_status\`, \`repo_diff\`, \`repo_commit\`, \`repo_push\`). If this ` +
|
|
81
|
+
`task needs git state you cannot get that way, say so in your result rather ` +
|
|
82
|
+
`than reaching into .git yourself.`);
|
|
66
83
|
}
|
|
67
84
|
/**
|
|
68
|
-
* The path check every file tool makes, before it
|
|
85
|
+
* The path check every file tool and every workspace call makes, before it
|
|
86
|
+
* opens anything.
|
|
69
87
|
*
|
|
70
|
-
* Returns the sentence to hand back, or `undefined` to proceed.
|
|
71
|
-
*
|
|
72
|
-
* to call it before the workspace is opened.
|
|
88
|
+
* Returns the sentence to hand back, or `undefined` to proceed. One string
|
|
89
|
+
* comparison — no `stat`, no round trip — which is why every file tool can
|
|
90
|
+
* afford to call it before the workspace is opened.
|
|
73
91
|
*
|
|
74
92
|
* ## A redirect, not a boundary
|
|
75
93
|
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* names the tools it belongs to. Both catch the case that actually happens — a
|
|
79
|
-
* model reaching into `.git/HEAD` to fix a merge, or reading a dependency's
|
|
80
|
-
* source and finding nothing there.
|
|
94
|
+
* The note names the tools repository work belongs to, because the case that
|
|
95
|
+
* actually happens is a model reaching into `.git/HEAD` to fix a merge.
|
|
81
96
|
*
|
|
82
|
-
* It is not containment, and this is the only place worth saying so.
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* directly.
|
|
97
|
+
* It is not containment, and this is the only place worth saying so. An agent
|
|
98
|
+
* that can write files here almost always holds `bash` too, which reads and
|
|
99
|
+
* writes `.git` directly.
|
|
86
100
|
*
|
|
87
101
|
* Symlinks are not resolved, for the same reason: a tracked
|
|
88
102
|
* `docs/notes.md -> ../.git/config` only matters to an agent given these tools
|
|
89
|
-
* *without* the shell
|
|
90
|
-
*
|
|
103
|
+
* *without* the shell. A read-only install — `restrictTools` down to `grep`,
|
|
104
|
+
* with Think's writing tools left out — is that agent, and it cannot write. If a writing agent
|
|
105
|
+
* without the shell ever exists, the **write** path is the half to restore —
|
|
91
106
|
* `.git/config` is an input to the credentialed push, and a planted hook runs
|
|
92
107
|
* under a container-side `repo_commit`. Reading `.git` discloses nothing that is
|
|
93
108
|
* not already readable, so the read half is not worth an `lstat` per call. The
|
|
@@ -96,9 +111,9 @@ function gitInternalNote(path, verb) {
|
|
|
96
111
|
* segment.
|
|
97
112
|
*/
|
|
98
113
|
export function guardPath(path, verb) {
|
|
99
|
-
if (isContainerOnly(path))
|
|
100
|
-
return containerOnlyNote(path, verb);
|
|
101
114
|
if (isGitInternal(path))
|
|
102
115
|
return gitInternalNote(path, verb);
|
|
116
|
+
if (hasSegment(path, "node_modules"))
|
|
117
|
+
return dependencyTreeNote(path, verb);
|
|
103
118
|
return undefined;
|
|
104
119
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { WorkspaceLike } from "@cloudflare/think";
|
|
2
|
+
import type { WorkspaceHost } from "../workspace/open.js";
|
|
3
|
+
import type { ComputerConfig } from "./index.js";
|
|
4
|
+
/**
|
|
5
|
+
* The container's workspace, as the agent's own `this.workspace`.
|
|
6
|
+
*
|
|
7
|
+
* Think's `read`, `write`, `edit` and `delete` run against whatever an agent's
|
|
8
|
+
* workspace is, and `computer()`'s tools replace only the ones it names. So an
|
|
9
|
+
* agent with a container and Think's default workspace would run `bash` in the
|
|
10
|
+
* container and `write` in its own SQLite, and nothing would say so. An agent
|
|
11
|
+
* that installs `computer` sets this instead:
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* override workspace = computerWorkspace(config, () => this.pluginContext().runtime());
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* and `computer(config).tools(ctx)` refuses to start without it — and reaches
|
|
18
|
+
* the workspace through it, so the tools and Think's file tools cannot resolve
|
|
19
|
+
* two different ones.
|
|
20
|
+
*
|
|
21
|
+
* Built on the workspace's own filesystem, not on `@cloudflare/computer`'s
|
|
22
|
+
* Think adapter: that one's `glob` and `readDir` read every entry, and its
|
|
23
|
+
* `writeFile` knows nothing of the paths and the states this plugin refuses.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Where a {@link computerWorkspace} sends each call, carried on it under
|
|
27
|
+
* {@link ROUTE}. `computer()`'s tools route through this rather than through
|
|
28
|
+
* their own config, which is what makes a workspace built from another config,
|
|
29
|
+
* or without its runtime, a wrong workspace for both rather than a split.
|
|
30
|
+
*/
|
|
31
|
+
export interface WorkspaceRoute {
|
|
32
|
+
/** The workspace the running turn names. Read per call. */
|
|
33
|
+
name(): string;
|
|
34
|
+
host(name: string): DurableObjectStub<WorkspaceHost>;
|
|
35
|
+
}
|
|
36
|
+
/** The route of a workspace {@link computerWorkspace} built, or `undefined`. */
|
|
37
|
+
export declare function workspaceRoute(workspace: unknown): WorkspaceRoute | undefined;
|
|
38
|
+
/** Whether `workspace` is one {@link computerWorkspace} built. */
|
|
39
|
+
export declare function isComputerWorkspace(workspace: unknown): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* @param runtime The running sub-agent's `runtime()`, read per call — how a
|
|
42
|
+
* sub-agent's file tools reach the workspace its parent prepared. A parent's
|
|
43
|
+
* is `undefined`, and {@link ComputerConfig.workspaceName} names it.
|
|
44
|
+
*/
|
|
45
|
+
export declare function computerWorkspace(config: ComputerConfig, runtime?: () => unknown): WorkspaceLike;
|
|
@@ -0,0 +1,234 @@
|
|
|
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 { withFileLock } from "./file-lock.js";
|
|
54
|
+
import { writeGate } from "./gate.js";
|
|
55
|
+
import { openWorkspaceFs, workspaceNameFromRuntime } from "../workspace/open.js";
|
|
56
|
+
import { guardPath, WALK_SKIPS } from "./paths.js";
|
|
57
|
+
/**
|
|
58
|
+
* The key the route is carried under. `Symbol.for`, so a second copy of this
|
|
59
|
+
* module in one bundle still recognises the first's workspaces.
|
|
60
|
+
*/
|
|
61
|
+
const ROUTE = Symbol.for("@dynamicagents/plugins/computer:workspace");
|
|
62
|
+
/**
|
|
63
|
+
* The most one `glob` returns. Its caller is Think's `find`, which shows 200
|
|
64
|
+
* and says when there were more — so one past that is all it can use, and
|
|
65
|
+
* reading the rest of a large checkout into the object would buy nothing.
|
|
66
|
+
*/
|
|
67
|
+
const GLOB_LIMIT = 201;
|
|
68
|
+
/** The route of a workspace {@link computerWorkspace} built, or `undefined`. */
|
|
69
|
+
export function workspaceRoute(workspace) {
|
|
70
|
+
if (typeof workspace !== "object" || workspace === null)
|
|
71
|
+
return undefined;
|
|
72
|
+
return workspace[ROUTE];
|
|
73
|
+
}
|
|
74
|
+
/** Whether `workspace` is one {@link computerWorkspace} built. */
|
|
75
|
+
export function isComputerWorkspace(workspace) {
|
|
76
|
+
return workspaceRoute(workspace) !== undefined;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Missing, as the filesystem says it across the boundary. The code does not
|
|
80
|
+
* always survive RPC, so the message is read too.
|
|
81
|
+
*/
|
|
82
|
+
function isMissing(err) {
|
|
83
|
+
if (!err || typeof err !== "object")
|
|
84
|
+
return false;
|
|
85
|
+
const { code, message } = err;
|
|
86
|
+
if (code === "ENOENT")
|
|
87
|
+
return true;
|
|
88
|
+
return typeof message === "string" && /ENOENT|no such/i.test(message);
|
|
89
|
+
}
|
|
90
|
+
/** `null` for a path that is not there, which is what Think's tools expect. */
|
|
91
|
+
async function orNull(read) {
|
|
92
|
+
try {
|
|
93
|
+
return await read;
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
if (isMissing(err))
|
|
97
|
+
return null;
|
|
98
|
+
throw err;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* `stat`, with a missing path answered as `null`.
|
|
103
|
+
*
|
|
104
|
+
* Asked of the stub's own `statOrNull` where it has one, the way
|
|
105
|
+
* {@link file://../workspace/read.ts pathExists} asks for `exists`: Think's
|
|
106
|
+
* `read` stats before it reads, so a missing file is an ordinary answer here,
|
|
107
|
+
* and a `stat` that throws across the boundary leaves the object an unhandled
|
|
108
|
+
* rejection to log for it.
|
|
109
|
+
*/
|
|
110
|
+
function statOrNull(fs, path) {
|
|
111
|
+
// Called as a method, never through `.call`: on an RPC stub `.call` is read
|
|
112
|
+
// as a remote method.
|
|
113
|
+
const remote = fs;
|
|
114
|
+
if (typeof remote.statOrNull === "function")
|
|
115
|
+
return remote.statOrNull(path);
|
|
116
|
+
return orNull(fs.stat(path));
|
|
117
|
+
}
|
|
118
|
+
async function drain(stream) {
|
|
119
|
+
return new Uint8Array(await new Response(stream).arrayBuffer());
|
|
120
|
+
}
|
|
121
|
+
function fileInfo(entry) {
|
|
122
|
+
const type = entry.isDirectory ? "directory" : "file";
|
|
123
|
+
return {
|
|
124
|
+
path: entry.path,
|
|
125
|
+
name: entry.path.slice(entry.path.lastIndexOf("/") + 1),
|
|
126
|
+
type,
|
|
127
|
+
// Generic on purpose: Think's `read` sniffs a generic type from the bytes,
|
|
128
|
+
// and an extension is not evidence of what a file holds.
|
|
129
|
+
mimeType: type === "directory" ? "inode/directory" : "application/octet-stream",
|
|
130
|
+
size: entry.size ?? 0,
|
|
131
|
+
createdAt: entry.mtime ?? 0,
|
|
132
|
+
updatedAt: entry.mtime ?? 0
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/** The directory a glob walks from, and the pattern relative to it. */
|
|
136
|
+
function splitGlob(pattern, cwd) {
|
|
137
|
+
const absolute = pattern.startsWith("/") ? pattern : `${cwd}/${pattern}`;
|
|
138
|
+
const wildcard = absolute.search(/[*?[{]/);
|
|
139
|
+
const cut = wildcard === -1
|
|
140
|
+
? absolute.lastIndexOf("/")
|
|
141
|
+
: absolute.lastIndexOf("/", wildcard);
|
|
142
|
+
return {
|
|
143
|
+
dir: cut <= 0 ? "/" : absolute.slice(0, cut),
|
|
144
|
+
rest: absolute.slice(cut + 1)
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* @param runtime The running sub-agent's `runtime()`, read per call — how a
|
|
149
|
+
* sub-agent's file tools reach the workspace its parent prepared. A parent's
|
|
150
|
+
* is `undefined`, and {@link ComputerConfig.workspaceName} names it.
|
|
151
|
+
*/
|
|
152
|
+
export function computerWorkspace(config, runtime) {
|
|
153
|
+
const cwd = config.cwd ?? "/workspace";
|
|
154
|
+
// Resolved per call, never memoized: the name belongs to the turn, and a
|
|
155
|
+
// stale one would route this caller's writes into another's files.
|
|
156
|
+
const named = () => workspaceNameFromRuntime(runtime?.()) ?? config.workspaceName();
|
|
157
|
+
const host = (name) => config.binding.get(config.binding.idFromName(name));
|
|
158
|
+
/**
|
|
159
|
+
* One call, against the workspace this turn names. Opened without starting a
|
|
160
|
+
* container — see `WorkspaceHost.__getWorkspaceFsStub`.
|
|
161
|
+
*/
|
|
162
|
+
const withFs = async (path, verb, body) => {
|
|
163
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
164
|
+
try {
|
|
165
|
+
const refusal = guardPath(path, verb);
|
|
166
|
+
if (refusal)
|
|
167
|
+
throw new Error(refusal);
|
|
168
|
+
const name = named();
|
|
169
|
+
const ws = __addDisposableResource(env_1, await openWorkspaceFs(host(name)), false);
|
|
170
|
+
return await body(ws.fs, name);
|
|
171
|
+
}
|
|
172
|
+
catch (e_1) {
|
|
173
|
+
env_1.error = e_1;
|
|
174
|
+
env_1.hasError = true;
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
__disposeResources(env_1);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
/**
|
|
181
|
+
* A write, refused while the workspace cannot keep it. Thrown rather than
|
|
182
|
+
* returned: Think's tools report a thrown error to the model, and a write
|
|
183
|
+
* that returned would read as one that landed.
|
|
184
|
+
*/
|
|
185
|
+
const withWritableFs = (path, verb, body) => withFs(path, verb, async (fs, name) => {
|
|
186
|
+
// Fails open, for the reason the tools' own write gate does: a read of
|
|
187
|
+
// another Durable Object's state must not take out a working tool.
|
|
188
|
+
const lost = await host(name)
|
|
189
|
+
.advisories()
|
|
190
|
+
.then(writeGate, () => undefined);
|
|
191
|
+
if (lost)
|
|
192
|
+
throw new Error(lost);
|
|
193
|
+
return body(fs, name);
|
|
194
|
+
});
|
|
195
|
+
const workspace = {
|
|
196
|
+
readFile: (path) =>
|
|
197
|
+
// Whole, never truncated: Think's `edit` writes back what it read.
|
|
198
|
+
withFs(path, "read", (fs) => orNull(fs.readFile(path, "utf8"))),
|
|
199
|
+
readFileBytes: (path) => withFs(path, "read", async (fs) => {
|
|
200
|
+
const stream = await orNull(fs.readFile(path));
|
|
201
|
+
return stream === null ? null : drain(stream);
|
|
202
|
+
}),
|
|
203
|
+
stat: (path) => withFs(path, "read", async (fs) => {
|
|
204
|
+
const stat = await statOrNull(fs, path);
|
|
205
|
+
return stat === null ? null : fileInfo({ ...stat, path });
|
|
206
|
+
}),
|
|
207
|
+
readDir: (dir = cwd, opts) => withFs(dir, "list", async (fs) => (await fs.readdir(dir, opts)).map((entry) => fileInfo({
|
|
208
|
+
...entry,
|
|
209
|
+
path: dir === "/" ? `/${entry.name}` : `${dir}/${entry.name}`
|
|
210
|
+
}))),
|
|
211
|
+
// Pruned rather than filtered: at a repository root either skipped
|
|
212
|
+
// directory outnumbers everything else.
|
|
213
|
+
glob: (pattern) => {
|
|
214
|
+
const { dir, rest } = splitGlob(pattern, cwd);
|
|
215
|
+
return withFs(dir, "find", async (fs) => (await fs.find(dir, rest, {
|
|
216
|
+
limit: GLOB_LIMIT,
|
|
217
|
+
exclude: WALK_SKIPS.map((segment) => `**/${segment}`)
|
|
218
|
+
})).map((entry) => fileInfo({ path: entry.path, isDirectory: entry.type === "dir" })));
|
|
219
|
+
},
|
|
220
|
+
// Locked per file, so a write never lands inside the `edit` tool's read and
|
|
221
|
+
// write of the same file — see `./file-lock.ts`.
|
|
222
|
+
writeFile: (path, content) => withWritableFs(path, "write", (fs, name) => withFileLock(name, path, () => fs.writeFile(path, content))),
|
|
223
|
+
mkdir: (path, opts) => withWritableFs(path, "write", (fs) => fs.mkdir(path, opts)),
|
|
224
|
+
// Locked too: a delete landing between an edit's read and its write would
|
|
225
|
+
// be undone by the write, and both would report success.
|
|
226
|
+
rm: (path, opts) => withWritableFs(path, "delete", (fs, name) => withFileLock(name, path, () => fs.rm(path, opts)))
|
|
227
|
+
// No `writeFileBytes`. Think writes through it on its own account —
|
|
228
|
+
// evicted media, projected skills — and none of that belongs in a
|
|
229
|
+
// repository's checkout. Without it Think skips both, with a warning.
|
|
230
|
+
};
|
|
231
|
+
const route = { name: named, host };
|
|
232
|
+
Object.defineProperty(workspace, ROUTE, { value: route });
|
|
233
|
+
return workspace;
|
|
234
|
+
}
|
|
@@ -1,28 +1,10 @@
|
|
|
1
1
|
import type { WorkspaceClient, WorkspaceRuntimeStatus } from "@cloudflare/computer";
|
|
2
|
-
/**
|
|
3
|
-
* Middle-out truncation, so both the first error and the final summary survive.
|
|
4
|
-
*
|
|
5
|
-
* The guard is not defensive padding. Without it a `max` at or below the marker's
|
|
6
|
-
* own length makes `half` zero or negative, and `slice(-0)` is `slice(0)` — the
|
|
7
|
-
* *whole* string — so the function returns more than it was given: 500 characters
|
|
8
|
-
* in, 543 out at `max: 80`. A silent inversion of the one thing it does,
|
|
9
|
-
* reachable from a public config field.
|
|
10
|
-
*/
|
|
11
|
-
export declare function truncateOutput(text: string, max: number): string;
|
|
12
|
-
/**
|
|
13
|
-
* A byte count in the form a model can act on.
|
|
14
|
-
*
|
|
15
|
-
* A listing that says a file is 4.2 MB tells the model the read it is about to do
|
|
16
|
-
* comes back with a hole in the middle — the one thing it cannot infer from the
|
|
17
|
-
* truncated result itself.
|
|
18
|
-
*/
|
|
19
|
-
export declare function humanBytes(bytes: number): string;
|
|
20
2
|
/**
|
|
21
3
|
* One `fs.grep` hit, derived from the client rather than imported.
|
|
22
4
|
*
|
|
23
5
|
* `@cloudflare/computer` declares `WorkspaceGrepMatch` but does not re-export it,
|
|
24
6
|
* so there is no name to import. Reading it back off the method keeps this correct
|
|
25
|
-
* by construction
|
|
7
|
+
* by construction.
|
|
26
8
|
*/
|
|
27
9
|
type GrepMatch = Awaited<ReturnType<WorkspaceClient["fs"]["grep"]>>[number];
|
|
28
10
|
/**
|
|
@@ -70,8 +52,6 @@ export declare function renderGrepMatches(matches: GrepMatch[], maxChars: number
|
|
|
70
52
|
shown: number;
|
|
71
53
|
capped: boolean;
|
|
72
54
|
};
|
|
73
|
-
/** Elapsed time in the shape a sentence wants. */
|
|
74
|
-
export declare function humanMs(ms: number): string;
|
|
75
55
|
/**
|
|
76
56
|
* Render a completed command the way a terminal would, minus the noise.
|
|
77
57
|
*
|
|
@@ -100,24 +80,33 @@ export declare function renderResult(result: {
|
|
|
100
80
|
stdout: string;
|
|
101
81
|
stderr: string;
|
|
102
82
|
status?: WorkspaceRuntimeStatus;
|
|
83
|
+
sync?: {
|
|
84
|
+
status: "complete" | "pending";
|
|
85
|
+
};
|
|
103
86
|
}, maxChars: number): string;
|
|
104
87
|
/**
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
88
|
+
* The command ran; its changes have not landed in the workspace yet.
|
|
89
|
+
*
|
|
90
|
+
* Every command is bracketed by a sync, and the pull afterwards is what moves
|
|
91
|
+
* what it wrote from the container into the Durable Object. That pull can fail
|
|
92
|
+
* while the command itself succeeds — a transport that dropped, a container
|
|
93
|
+
* replaced underneath — and the runtime reports it in `sync` rather than in the
|
|
94
|
+
* exit code, because the command genuinely did run.
|
|
95
|
+
*
|
|
96
|
+
* Worth a sentence to the model because the *next* thing it does is usually read
|
|
97
|
+
* what it just wrote, and the file tools read the workspace rather than the
|
|
98
|
+
* container. Without this the file looks unchanged and the obvious conclusion is
|
|
99
|
+
* that the command did not work, which is the one conclusion that is wrong.
|
|
100
|
+
*
|
|
101
|
+
* Recovery is deliberately not the model's, and is not automatic either: the
|
|
102
|
+
* runtime schedules no retry of its own, so an outstanding pull lands when the
|
|
103
|
+
* host drives one or when a later command's own bracket carries it. What the
|
|
104
|
+
* model is told therefore stops short of "it will be there in a moment" — it is
|
|
105
|
+
* told the read may be stale, that re-running the command is the wrong fix
|
|
106
|
+
* because it would repeat the side effects, and to say so in its result if the
|
|
107
|
+
* workspace still disagrees with what it wrote.
|
|
121
108
|
*/
|
|
122
|
-
export declare function
|
|
109
|
+
export declare function syncPendingNote(sync: {
|
|
110
|
+
status: "complete" | "pending";
|
|
111
|
+
} | undefined): string | undefined;
|
|
123
112
|
export {};
|