@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.
Files changed (132) hide show
  1. package/README.md +58 -77
  2. package/dist/browser/index.d.ts +0 -2
  3. package/dist/browser/index.js +21 -27
  4. package/dist/claude-code/cards.d.ts +55 -0
  5. package/dist/claude-code/cards.js +283 -0
  6. package/dist/claude-code/config.d.ts +28 -63
  7. package/dist/claude-code/config.js +0 -11
  8. package/dist/claude-code/egress.d.ts +8 -1
  9. package/dist/claude-code/egress.js +42 -4
  10. package/dist/claude-code/events.d.ts +106 -9
  11. package/dist/claude-code/events.js +201 -56
  12. package/dist/claude-code/github.d.ts +39 -0
  13. package/dist/claude-code/github.js +82 -0
  14. package/dist/claude-code/index.d.ts +16 -90
  15. package/dist/claude-code/index.js +14 -276
  16. package/dist/claude-code/model.d.ts +152 -0
  17. package/dist/claude-code/model.js +366 -0
  18. package/dist/claude-code/run.d.ts +230 -116
  19. package/dist/claude-code/run.js +380 -167
  20. package/dist/claude-code/session.d.ts +131 -0
  21. package/dist/claude-code/session.js +288 -0
  22. package/dist/claude-code/spec.d.ts +46 -0
  23. package/dist/claude-code/spec.js +83 -0
  24. package/dist/claude-code/verified.d.ts +15 -0
  25. package/dist/claude-code/verified.js +15 -0
  26. package/dist/computer/context.d.ts +1 -5
  27. package/dist/computer/context.js +7 -10
  28. package/dist/computer/file-lock.js +5 -4
  29. package/dist/computer/gate.d.ts +4 -4
  30. package/dist/computer/gate.js +5 -5
  31. package/dist/computer/index.d.ts +40 -258
  32. package/dist/computer/index.js +71 -324
  33. package/dist/computer/paths.d.ts +36 -16
  34. package/dist/computer/paths.js +42 -29
  35. package/dist/computer/proxy.d.ts +45 -0
  36. package/dist/computer/proxy.js +234 -0
  37. package/dist/computer/render.d.ts +1 -40
  38. package/dist/computer/render.js +2 -68
  39. package/dist/computer/tools-exec.js +12 -13
  40. package/dist/computer/tools-file.d.ts +9 -1
  41. package/dist/computer/tools-file.js +22 -82
  42. package/dist/computer/{tools-find.d.ts → tools-grep.d.ts} +1 -1
  43. package/dist/computer/tools-grep.js +129 -0
  44. package/dist/repo/checkout.d.ts +4 -5
  45. package/dist/repo/checkout.js +31 -8
  46. package/dist/repo/context.d.ts +35 -9
  47. package/dist/repo/context.js +101 -49
  48. package/dist/repo/index.d.ts +73 -28
  49. package/dist/repo/index.js +112 -96
  50. package/dist/repo/tools-checks.d.ts +3 -0
  51. package/dist/repo/tools-checks.js +112 -0
  52. package/dist/repo/tools-clone.js +1 -1
  53. package/dist/repo/tools-pr.d.ts +14 -0
  54. package/dist/repo/{tools-forge.js → tools-pr.js} +53 -36
  55. package/dist/repo/tools-review.d.ts +9 -0
  56. package/dist/repo/tools-review.js +67 -47
  57. package/dist/repo/tools-worktree.js +144 -60
  58. package/dist/repo/tools-worktrees.d.ts +12 -0
  59. package/dist/repo/tools-worktrees.js +48 -0
  60. package/dist/repo/url.d.ts +2 -2
  61. package/dist/repo/url.js +7 -7
  62. package/dist/scratch/index.d.ts +4 -4
  63. package/dist/scratch/index.js +12 -11
  64. package/dist/{computer → workspace}/advisory.d.ts +4 -4
  65. package/dist/{computer → workspace}/advisory.js +2 -2
  66. package/dist/{computer/host → workspace}/ca-trust.d.ts +8 -1
  67. package/dist/{computer/host → workspace}/ca-trust.js +12 -3
  68. package/dist/{computer/host → workspace}/container-deps.d.ts +4 -0
  69. package/dist/{computer/host → workspace}/container-deps.js +26 -1
  70. package/dist/{computer/host → workspace}/container-fault.d.ts +3 -3
  71. package/dist/{computer/host → workspace}/container-fault.js +23 -8
  72. package/dist/workspace/deps-snapshot.d.ts +110 -0
  73. package/dist/workspace/deps-snapshot.js +424 -0
  74. package/dist/workspace/exec.d.ts +94 -0
  75. package/dist/workspace/exec.js +153 -0
  76. package/dist/workspace/format.d.ts +36 -0
  77. package/dist/workspace/format.js +57 -0
  78. package/dist/{computer/host → workspace}/git-host.d.ts +21 -4
  79. package/dist/{computer/host → workspace}/git-host.js +72 -2
  80. package/dist/{computer/host → workspace}/git-identity.d.ts +1 -1
  81. package/dist/{computer/host → workspace}/git-identity.js +1 -1
  82. package/dist/workspace/index.d.ts +44 -47
  83. package/dist/workspace/index.js +52 -112
  84. package/dist/{computer/host → workspace}/install-job.d.ts +53 -4
  85. package/dist/{computer/host → workspace}/install-job.js +346 -178
  86. package/dist/{computer → workspace}/install.d.ts +17 -8
  87. package/dist/{computer → workspace}/install.js +24 -6
  88. package/dist/{computer/host/workspace.d.ts → workspace/object.d.ts} +125 -27
  89. package/dist/{computer/host/workspace.js → workspace/object.js} +341 -58
  90. package/dist/workspace/open.d.ts +95 -0
  91. package/dist/workspace/open.js +63 -0
  92. package/dist/workspace/read.d.ts +16 -0
  93. package/dist/workspace/read.js +22 -0
  94. package/dist/{computer → workspace}/shell.d.ts +5 -5
  95. package/dist/{computer → workspace}/shell.js +6 -6
  96. package/dist/{computer/host → workspace}/wake.d.ts +3 -1
  97. package/package.json +17 -37
  98. package/dist/arc-agi/analysis.d.ts +0 -322
  99. package/dist/arc-agi/analysis.js +0 -802
  100. package/dist/arc-agi/client.d.ts +0 -82
  101. package/dist/arc-agi/client.js +0 -113
  102. package/dist/arc-agi/game-tools.d.ts +0 -24
  103. package/dist/arc-agi/game-tools.js +0 -26
  104. package/dist/arc-agi/index.d.ts +0 -40
  105. package/dist/arc-agi/index.js +0 -175
  106. package/dist/arc-agi/main-agent.d.ts +0 -24
  107. package/dist/arc-agi/main-agent.js +0 -62
  108. package/dist/arc-agi/recipe.d.ts +0 -48
  109. package/dist/arc-agi/recipe.js +0 -97
  110. package/dist/arc-agi/scorecard.d.ts +0 -118
  111. package/dist/arc-agi/scorecard.js +0 -163
  112. package/dist/arc-agi/soul.d.ts +0 -29
  113. package/dist/arc-agi/soul.js +0 -79
  114. package/dist/arc-agi/store.d.ts +0 -153
  115. package/dist/arc-agi/store.js +0 -215
  116. package/dist/arc-agi/tools.d.ts +0 -36
  117. package/dist/arc-agi/tools.js +0 -574
  118. package/dist/arc-agi/types.d.ts +0 -211
  119. package/dist/arc-agi/types.js +0 -19
  120. package/dist/claude-code/recipe.d.ts +0 -53
  121. package/dist/claude-code/recipe.js +0 -100
  122. package/dist/computer/read.d.ts +0 -133
  123. package/dist/computer/read.js +0 -200
  124. package/dist/computer/tools-find.js +0 -216
  125. package/dist/recall/index.d.ts +0 -105
  126. package/dist/recall/index.js +0 -181
  127. package/dist/repo/tools-forge.d.ts +0 -4
  128. package/dist/triage/index.d.ts +0 -360
  129. package/dist/triage/index.js +0 -311
  130. /package/dist/{computer/host → workspace}/sync.d.ts +0 -0
  131. /package/dist/{computer/host → workspace}/sync.js +0 -0
  132. /package/dist/{computer/host → workspace}/wake.js +0 -0
@@ -1,189 +1,42 @@
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 { definePlugin } from "@dynamicagents/core";
54
- import { getWorkspace } from "@cloudflare/computer";
55
- import { cancelledNote } from "./render.js";
56
- import { computerContext, DEFAULT_CWD, DEFAULT_TIMEOUT_MS } from "./context.js";
57
- import { withShell } from "./shell.js";
1
+ import { definePlugin, PluginSetupError } from "@dynamicagents/core";
2
+ import { openWorkspace, openWorkspaceFs } from "../workspace/open.js";
3
+ import { workspaceRoute } from "./proxy.js";
4
+ import { computerContext } from "./context.js";
58
5
  import { execTools } from "./tools-exec.js";
59
6
  import { fileTools } from "./tools-file.js";
60
- import { findTools } from "./tools-find.js";
7
+ import { grepTools } from "./tools-grep.js";
61
8
  /**
62
- * `@dynamicagents/plugins/computer` — a Linux container whose filesystem outlives it.
9
+ * `@dynamicagents/plugins/computer` — an agent's tools over a container
10
+ * workspace: `bash`, `grep` and `edit`, and `computerWorkspace`, the agent's own
11
+ * `this.workspace`, which Think's `read`, `write`, `delete`, `find` and `list`
12
+ * go through.
63
13
  *
64
- * The filesystem **is** a Durable Object's SQLite, mounted into the container
65
- * over FUSE by `computerd`. Commands see a normal `/workspace`; the Worker reads
66
- * the same tree over RPC; and when the container is replaced the tree is pushed
67
- * back into the new one.
14
+ * The workspace itself — the Durable Object, its container, its git, its
15
+ * install — is `@dynamicagents/plugins/workspace`. This plugin reaches it only
16
+ * through that subpath's client modules, never its object, so an agent that
17
+ * holds the tools carries no container backend and no isomorphic-git.
68
18
  *
69
- * Not to be confused with `@dynamicagents/plugins/workspace`, which is a virtual
70
- * filesystem with no processes and nothing to run. Install exactly one
71
- * filesystem plugin — an agent holding two gives the model no way to know which
72
- * one a path refers to.
19
+ * The agent's own workspace has to be this one — `computerWorkspace` — or
20
+ * Think's `read` and `write` would work on a different tree from `bash`. The
21
+ * plugin refuses to start otherwise; see `./proxy.ts`.
73
22
  *
74
23
  * ## The dependency tree is not in the workspace
75
24
  *
76
25
  * The one thing to internalise before reading further. A package root's
77
26
  * `node_modules` is a bind mount of the container's disk, so installs never
78
- * sync, and a new container reinstalls — see `./host/container-deps.ts`. The file tools read
79
- * the workspace, so they refuse it; `sb_exec` reaches it. `paths.ts` owns what a
80
- * path may be and what walks drop.
81
- *
82
- * Requires the Workers **Paid** plan (containers) and a Durable Object binding
83
- * whose class owns the workspace — see the README for the wrangler block.
84
- */
85
- /**
86
- * How a repository installs its dependencies — the mechanical half.
87
- *
88
- * Re-exported from the plugin's one entry point rather than given a subpath of
89
- * its own: `verify:exports` treats each subpath as an isolated realm, and a host
90
- * needs the resolver and the tools together anyway.
27
+ * sync, and a new container reinstalls — see `/workspace`'s
28
+ * `container-deps.ts`. The file tools read the workspace, so they refuse it;
29
+ * `bash` reaches it. `paths.ts` owns what a path may be and what walks drop.
91
30
  */
92
- export { DEFAULT_INSTALL_PLAN, installFingerprint, resolveInstallCommand } from "./install.js";
93
31
  /**
94
- * The helpers this plugin's own tests and hosts reach for, re-exported from the
95
- * one entry point.
96
- *
97
- * They live in sibling modules — `paths.ts`, `render.ts`, `gate.ts` — because
98
- * this file was 1,900 lines and the seams were obvious. The subpath is the unit a
99
- * consumer imports, so what it exports must not move when the files behind it do:
100
- * `verify:exports` treats `./computer` as one realm, and everything below stays
101
- * inside `dist/computer/`.
32
+ * The helpers this plugin's own tests reach for, re-exported from the one
33
+ * entry point. `verify:exports` treats `./computer` as one realm, so what it
34
+ * exports must not move when the files behind it do.
102
35
  */
103
- export { isGitInternal, WALK_SKIPS } from "./paths.js";
104
- export { cancelledNote, packBlocks, renderGrepMatches, renderResult, syncPendingNote, truncateOutput } from "./render.js";
36
+ export { isGitInternal, SESSION_STATE_DIR, WALK_SKIPS } from "./paths.js";
37
+ export { packBlocks, renderGrepMatches, renderResult, syncPendingNote } from "./render.js";
105
38
  export { execGate, needsDependencies, writeGate } from "./gate.js";
106
- /**
107
- * The workspace-advisory vocabulary, which a host needs all of: the type to
108
- * return over RPC, `deriveAdvisories` to build it from what it already knows,
109
- * and `sessionAdvisory` for a host driving a whole session rather than one
110
- * command. `shapeOf` and `renderAdvisory` are exported for the specs that hold
111
- * their exhaustiveness — a host has no reason to call either, since calling them
112
- * would mean writing policy or wording that has one home already.
113
- */
114
- export { deriveAdvisories, renderAdvisory, sessionAdvisory, shapeOf } from "./advisory.js";
115
- // A host implementing `InstallProbe` needs exactly this, and hand-rolling it
116
- // loses the fast path: across a Durable Object boundary `fs` is a stub carrying
117
- // `exists`, which a `stat`-only probe never asks for.
118
- export { pathExists } from "./read.js";
119
- /**
120
- * This plugin's tool-family name, as a recipe's `toolFamilies` lists it.
121
- *
122
- * `"sandbox"` names the capability — a shell and a filesystem — rather than the
123
- * package providing it, and it appears in recipes, souls and allowlists that
124
- * have nothing to do with either. Renaming it would have `validateRecipe`
125
- * silently drop the family from every recipe that still says `sandbox`: a
126
- * subagent with no tools and no error explaining why.
127
- */
128
- export const SANDBOX_FAMILY = "sandbox";
129
- /**
130
- * Where a parent puts the workspace name so its subagents reach the same one.
131
- *
132
- * A subagent cannot compute this itself. The name is derived from the verified
133
- * caller and the repository, and core deliberately gives a subagent execution a
134
- * `callerKey` thunk that **throws** — "a subagent execution has no caller
135
- * identity". So a facet running a `code` subtask would fail at its first tool
136
- * call, with the parent's checkout sitting in a workspace it cannot name.
137
- *
138
- * `resolveRuntime` runs on the parent, where the name resolves, and its return
139
- * value reaches every tool family as `ToolFamilyContext.runtime`. That is the
140
- * channel core built for exactly this, and it is emphatically *not* `params`,
141
- * which are declared in the subtask type's schema and rendered to the delegating
142
- * model — a workspace name there would be model-authored, and a model naming
143
- * another caller's workspace would get that caller's files.
144
- */
145
- export const WORKSPACE_RUNTIME_KEY = "workspaceName";
146
- /**
147
- * Read a parent-resolved workspace name off a subtask's runtime state.
148
- *
149
- * Returns `undefined` rather than throwing on anything unexpected: the main
150
- * agent's tools have no runtime at all, and falling back to the configured thunk
151
- * is right there.
152
- */
153
- export function workspaceNameFromRuntime(runtime) {
154
- const value = runtime?.[WORKSPACE_RUNTIME_KEY];
155
- return typeof value === "string" && value.length > 0 ? value : undefined;
156
- }
157
- export { withShell, withShellTranscript } from "./shell.js";
158
- /**
159
- * Open the workspace behind a Durable Object stub.
160
- *
161
- * The cast is unavoidable, and narrow enough to be worth isolating here rather
162
- * than repeating. `getWorkspace` wants a handle whose `__getWorkspaceStub()`
163
- * resolves to a `WorkspaceStub` — the concrete class, private fields and all.
164
- * Across a Durable Object boundary Workers RPC hands back a structural
165
- * `Stub<WorkspaceStub>`, which forwards every method faithfully but carries none
166
- * of the class's private brand: runtime-compatible, type-incompatible. This is
167
- * the only place in the plugin that gap is crossed.
168
- */
169
- export function openWorkspace(host) {
170
- return getWorkspace(host);
171
- }
172
- /**
173
- * The same workspace, opened for filesystem work alone.
174
- *
175
- * `getWorkspace` calls exactly one method on what it is handed, so the host is
176
- * wrapped in an object answering it from the other side of the seam — see
177
- * {@link WorkspaceHost.__getWorkspaceFsStub}. Everything past that is identical.
178
- *
179
- * **Only the file tools take this.** `sb_exec` and {@link computerExec} need the
180
- * container started and its CA installed, so they open it the other way.
181
- */
182
- export function openWorkspaceFs(host) {
183
- return getWorkspace({
184
- __getWorkspaceStub: () => host.__getWorkspaceFsStub()
185
- });
186
- }
39
+ export { computerWorkspace, isComputerWorkspace } from "./proxy.js";
187
40
  /**
188
41
  * @param workspace Opens the workspace for a command: container started, CA
189
42
  * installed.
@@ -198,164 +51,58 @@ export function buildComputerTools(workspace, config, advisories, fsWorkspace =
198
51
  return {
199
52
  ...execTools(ctx),
200
53
  ...fileTools(ctx),
201
- ...findTools(ctx)
54
+ ...grepTools(ctx)
202
55
  };
203
56
  }
57
+ const CONTEXT = [
58
+ "You have a Linux container with a shell, a package manager and network access:",
59
+ "- `bash` runs any shell command — builds, tests, installs, git.",
60
+ "- `read` / `write` / `edit` / `delete` work on files; `edit` replaces an exact unique string and is the right tool for a small change.",
61
+ "- `grep` searches file contents; `list` lists a directory; `find` finds files by glob.",
62
+ // The reason to prefer them is the one thing the model cannot infer from a
63
+ // tool description: these read the durable workspace directly, so they are
64
+ // the only tools that still answer while the container is unavailable.
65
+ "Search with `grep` and `find` rather than running them through `bash`. They read the workspace directly, so they keep working while the container is restarting or an install is still running, and they come back with line numbers and a bounded result instead of a wall of text.",
66
+ "Command output is truncated from the middle when large, so run targeted commands and read specific files rather than printing everything.",
67
+ // Both halves of this matter and they pull in opposite directions, which
68
+ // is why they are stated together rather than left for the model to work
69
+ // out from a confusing result.
70
+ "The checkout is durable: it survives between tasks and is still there after the container restarts, so it may already contain work from an earlier task — check before assuming it is empty.",
71
+ "`node_modules` is not durable: it lives on the container's disk, so a new container reinstalls it, and the file tools cannot see inside it — use `bash` there. It is a mount point, so `rm -rf node_modules` fails; `npm ci` clears it itself.",
72
+ // Stated up front rather than left to a refusal, so the model does not spend
73
+ // a turn discovering it. The destination matters as much as the rule: a
74
+ // prohibition with nowhere to go gets worked around.
75
+ "`.git` is off limits to these tools, and searches and recursive listings skip it. It is git's internal state — reading it tells you less than the repository tools do, and writing it corrupts the checkout. Repository work goes through the repo tools (`repo_status`, `repo_diff`, `repo_commit`, `repo_push`); if a task needs git state you cannot get that way, say so in your result rather than reaching into `.git` yourself."
76
+ ].join("\n");
204
77
  /**
205
- * An `exec` bound to this workspace, for plugins that need a shell but should
206
- * not own a container.
207
- *
208
- * `@dynamicagents/plugins/repo` is the reason this exists: it needs `git` on a real
209
- * shell, but depending on this module would weld the two together and stop a
210
- * host from pointing it at its own container. Structurally typed for the same
211
- * reason — the two plugins compose without either importing the other.
212
- *
213
- * ## {@link ComputerConfig.env} is deliberately **not** merged here
214
- *
215
- * {@link buildComputerTools} merges it into every command; this does not.
216
- *
217
- * What this hands out is a raw shell to *another plugin*, whose commands this
218
- * module neither writes nor sees. `/repo` runs its unauthenticated git through
219
- * it and pins the environment those need — `GIT_TERMINAL_PROMPT=0` plus the
220
- * model-authored values it passes as variables. A host environment merged
221
- * underneath lets through every key it does *not* pin: `http_proxy`,
222
- * `GIT_PROXY_COMMAND`, `GIT_SSL_CAINFO`, `GIT_CONFIG_GLOBAL`, `GIT_EXEC_PATH` —
223
- * the last two redirect the config git reads and relocate its helper binaries.
224
- *
225
- * No token is at stake, since the credentialed operations do not run here at
226
- * all. The point is that changing what git does inside another plugin's commands
227
- * should not be something a container plugin's config field does by accident,
228
- * from a host that only meant to set a registry. A host that wants it composes
229
- * it where the merge is visible:
230
- *
231
- * ```ts
232
- * exec: (command, options) =>
233
- * computerExec(config)(command, {
234
- * ...options,
235
- * env: { ...mine, ...options?.env }
236
- * });
237
- * ```
78
+ * The container plugin. Its tools reach the workspace through the agent's own
79
+ * `computerWorkspace` — the one Think's `read` and `write` use — so the two can
80
+ * never resolve different ones. `config` answers everything else: the shell,
81
+ * the limits, the environment.
238
82
  */
239
- export function computerExec(config) {
240
- return async (command, options) => {
241
- const env_1 = { stack: [], error: void 0, hasError: false };
242
- try {
243
- // The caller forwards its execution's runtime state opaquely; only this side
244
- // knows the key it might carry. That is what lets `/repo` reach a subagent's
245
- // shared workspace without importing anything from here.
246
- const name = workspaceNameFromRuntime(options?.runtime) ?? config.workspaceName();
247
- const ws = __addDisposableResource(env_1, await openWorkspace(config.binding.get(config.binding.idFromName(name))), false);
248
- const env = options?.env
249
- ? Object.fromEntries(Object.entries(options.env).filter((entry) => entry[1] !== undefined))
250
- : undefined;
251
- // Named rather than inlined, because the note below has to state it: a
252
- // ceiling the model cannot see is one it cannot work within.
253
- const timeoutMs = options?.timeout ?? config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
254
- // `withShell`, never `withShellTranscript`: the caller reads this result in
255
- // code. `/repo` compares `stdout` against a URL, tests it for emptiness to
256
- // decide a tree is clean, and reads a sha out of it to push — all of which
257
- // need `stdout` to carry the answer alone, with git's diagnostics on the
258
- // channel git put them on.
259
- const handle = __addDisposableResource(env_1, await ws.runtime.exec(withShell(command, config.shell), {
260
- cwd: options?.cwd ?? config.cwd ?? DEFAULT_CWD,
261
- encoding: "utf8",
262
- timeoutMs,
263
- ...(env ? { env } : {})
264
- }), false);
265
- const result = await handle.result();
266
- const killed = cancelledNote(result.status, result.exitCode, timeoutMs);
267
- return {
268
- // `/repo` branches on `success`, which this runtime does not report — it
269
- // has `status` and `exitCode`. Derived from the exit code rather than the
270
- // status, because a command that runs and fails is `completed` here.
271
- success: result.exitCode === 0,
272
- stdout: result.stdout,
273
- // Appended rather than substituted: a command killed at the ceiling may
274
- // well have written plenty first, and that output is the more useful half.
275
- // This is the only field the note can travel on — `stdout` is a data
276
- // channel the caller parses, and the four fields are the whole contract.
277
- stderr: killed
278
- ? [result.stderr, killed].filter(Boolean).join("\n")
279
- : result.stderr,
280
- exitCode: result.exitCode
281
- };
282
- }
283
- catch (e_1) {
284
- env_1.error = e_1;
285
- env_1.hasError = true;
286
- }
287
- finally {
288
- __disposeResources(env_1);
289
- }
290
- };
291
- }
292
83
  export function computer(config) {
293
- // Resolved per call, not memoized: `workspaceName` is a thunk precisely
294
- // because the name is not knowable at construction, and a stale stub would
295
- // silently route a second caller's commands into the first caller's files.
296
- const nameOf = (runtime) => workspaceNameFromRuntime(runtime) ?? config.workspaceName();
297
- const host = (runtime) => config.binding.get(config.binding.idFromName(nameOf(runtime)));
298
- const tools = (runtime) => buildComputerTools(() => openWorkspace(host(runtime)), config,
299
- // No try/catch here: `buildComputerTools` fails the gate open itself, so
300
- // wrapping again would only make it look like the guarantee lives in two
301
- // places.
302
- () => host(runtime).advisories(), () => openWorkspaceFs(host(runtime)), () => nameOf(runtime));
303
84
  return definePlugin({
304
- key: "computer",
305
- mainAgentTools: () => tools(),
306
- // `ctx.runtime` is what makes a delegated subtask land in the workspace its
307
- // parent prepared — see {@link WORKSPACE_RUNTIME_KEY}. Without it the
308
- // fallback thunk runs, and on a subagent that throws.
309
- toolFamilies: {
310
- [SANDBOX_FAMILY]: (ctx) => ({ tools: tools(ctx.runtime) })
85
+ name: "computer",
86
+ tools: (ctx) => {
87
+ // Here, not in `execute`: the start check builds every plugin's tools,
88
+ // so a missing workspace fails the start rather than a turn.
89
+ const route = workspaceRoute(ctx.workspace());
90
+ if (!route)
91
+ throw new PluginSetupError(`plugin "computer" runs \`bash\` in a container, but ${ctx.agentName}'s ` +
92
+ "workspace is not that container's, so Think's `read` and `write` " +
93
+ "would work on a different tree. Set it on the agent class: " +
94
+ "`override workspace = computerWorkspace(config, () => this.pluginContext().runtime())`.");
95
+ // Resolved per call, not memoized: the name belongs to the turn — a
96
+ // sub-agent's comes from `runtime()`, see `/workspace`'s `WORKSPACE_RUNTIME_KEY` —
97
+ // and a stale stub would silently route a second caller's commands into
98
+ // the first caller's files.
99
+ const host = () => route.host(route.name());
100
+ return buildComputerTools(() => openWorkspace(host()), config,
101
+ // No try/catch here: `buildComputerTools` fails the gate open itself,
102
+ // so wrapping again would only make it look like the guarantee lives in
103
+ // two places.
104
+ () => host().advisories(), () => openWorkspaceFs(host()), route.name);
311
105
  },
312
- capability: [
313
- "You have a Linux container with a shell, a package manager and network access:",
314
- "- `sb_exec` runs any shell command — builds, tests, installs, git.",
315
- "- `sb_read` / `sb_write` / `sb_edit` work on files; `sb_edit` replaces an exact unique string and is the right tool for a small change.",
316
- "- `sb_grep` searches file contents; `sb_ls` lists a directory, or finds files by glob with `pattern`; `sb_exists` checks a path.",
317
- // The reason to prefer them is the one thing the model cannot infer from a
318
- // tool description: these read the durable workspace directly, so they are
319
- // the only tools that still answer while the container is unavailable.
320
- "Search with `sb_grep` and `sb_ls` rather than running `grep` or `find` through `sb_exec`. They read the workspace directly, so they keep working while the container is restarting or an install is still running, and they come back with line numbers and a bounded result instead of a wall of text.",
321
- "Command output is truncated from the middle when large, so run targeted commands and read specific files rather than printing everything.",
322
- // Both halves of this matter and they pull in opposite directions, which
323
- // is why they are stated together rather than left for the model to work
324
- // out from a confusing result.
325
- "The checkout is durable: it survives between tasks and is still there after the container restarts, so it may already contain work from an earlier task — check before assuming it is empty.",
326
- "`node_modules` is not durable: it lives on the container's disk, so a new container reinstalls it, and the file tools cannot see inside it — use `sb_exec` there. It is a mount point, so `rm -rf node_modules` fails; `npm ci` clears it itself.",
327
- // Stated up front rather than left to a refusal, so the model does not spend
328
- // a turn discovering it. The destination matters as much as the rule: a
329
- // prohibition with nowhere to go gets worked around.
330
- "`.git` is off limits to these tools, and searches and recursive listings skip it. It is git's internal state — reading it tells you less than the repository tools do, and writing it corrupts the checkout. Repository work goes through the repo tools (`repo_status`, `repo_diff`, `repo_commit`, `repo_push`), which the main agent holds; if a task needs git state beyond what you can see in the working tree, say so in your result rather than reaching into `.git` yourself."
331
- ].join("\n")
106
+ context: [{ provider: { get: async () => CONTEXT } }]
332
107
  });
333
108
  }
334
- // --- the host half ----------------------------------------------------------
335
- /**
336
- * The Durable Object everything above talks to.
337
- *
338
- * `./host/` is a directory rather than a subpath of its own, and that is the
339
- * point: one capability is one import path, so a consumer cannot install the
340
- * tools and miss the object they address. The two halves have opposite bundle
341
- * costs — an agent that only calls `sb_exec` carries no container backend and no
342
- * isomorphic-git — and `"sideEffects": false` is what keeps that true, since
343
- * nothing here references the host unless the consumer does.
344
- *
345
- * The dependency runs one way for a mechanical reason: `./host/` imports the
346
- * leaf modules beside this file, never this barrel, because this barrel
347
- * re-exports `./host/` and a cycle through a module that builds a class at
348
- * import time is a base that evaluates `undefined`.
349
- */
350
- export { WorkspaceObjectBase, WORKSPACE_DIR, workspaceName } from "./host/workspace.js";
351
- // The dependency install, exported because a host that wants to report on one —
352
- // or a spec that drives it — needs the type, not because anything but
353
- // `./host/workspace.ts` constructs it.
354
- export { InstallJob } from "./host/install-job.js";
355
- // The pull the library owns no alarm for. Its constants are exported because a
356
- // host deferring to a drain has to back off at the same rate this does.
357
- export { WorkspaceSync, SYNC_DRAIN_BUDGET_MS, SYNC_DRAIN_RESUME_MS, SYNC_DRAIN_MAX_BACKOFF_MS, syncRetryDelayMs } from "./host/sync.js";
358
- // `TRUST_CA_COMMAND` is exported so a host can assert what its image will be
359
- // asked to run — see the file for why this cannot live in an entrypoint.
360
- export { ContainerTrust, TRUST_CA_COMMAND } from "./host/ca-trust.js";
361
- export { WorkspaceGitHost } from "./host/git-host.js";
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * Which paths these tools act on, and the sentence a refused one gets.
3
3
  *
4
- * Both refused directories are also skipped by every walk. `.git` is refused as
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
5
6
  * policy: a model that edits `.git/HEAD` corrupts a checkout in a way that
6
7
  * surfaces much later. `node_modules` is refused as a fact: it lives on the
7
- * container's disk (see `./host/container-deps.ts`), so these tools, which read
8
+ * container's disk (see `../workspace/container-deps.ts`), so these tools, which read
8
9
  * the workspace, would find it empty.
9
10
  *
10
11
  * Everything here is a string comparison: no filesystem, no `await`, which is
@@ -21,16 +22,35 @@
21
22
  */
22
23
  export declare function isGitInternal(path: string): boolean;
23
24
  /**
24
- * What every walk steps over. `find` prunes these in the store; `grep` takes no
25
- * exclusion, so `sb_grep` filters its results instead.
25
+ * Where a session's own state lives inside a workspace, as a directory name.
26
+ *
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.
26
49
  */
27
- export declare const WALK_SKIPS: readonly [".git", "node_modules"];
28
- /** Whether a walk carrying `skips` steps over `path`. */
29
- export declare function isSkipped(skips: readonly string[], path: string): boolean;
30
- /** The skipped directories, for a sentence: "`.git` and `node_modules`". */
31
- export declare function skipNames(skips: readonly string[]): string;
50
+ export declare const WALK_SKIPS: readonly [".git", "node_modules", ".claude-sessions"];
32
51
  /**
33
- * The path check every file tool makes, before it opens anything.
52
+ * The path check every file tool and every workspace call makes, before it
53
+ * opens anything.
34
54
  *
35
55
  * Returns the sentence to hand back, or `undefined` to proceed. One string
36
56
  * comparison — no `stat`, no round trip — which is why every file tool can
@@ -41,15 +61,15 @@ export declare function skipNames(skips: readonly string[]): string;
41
61
  * The note names the tools repository work belongs to, because the case that
42
62
  * actually happens is a model reaching into `.git/HEAD` to fix a merge.
43
63
  *
44
- * It is not containment, and this is the only place worth saying so. `sb_exec`
45
- * is in the same tool family, granted per family rather than per tool, so every
46
- * agent holding these file tools also holds a shell that reads and writes `.git`
47
- * 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.
48
67
  *
49
68
  * Symlinks are not resolved, for the same reason: a tracked
50
69
  * `docs/notes.md -> ../.git/config` only matters to an agent given these tools
51
- * *without* the shell, which the family granularity makes unbuildable. If that
52
- * ever becomes buildable, the **write** path is the half to restore —
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 —
53
73
  * `.git/config` is an input to the credentialed push, and a planted hook runs
54
74
  * under a container-side `repo_commit`. Reading `.git` discloses nothing that is
55
75
  * not already readable, so the read half is not worth an `lstat` per call. The
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * Which paths these tools act on, and the sentence a refused one gets.
3
3
  *
4
- * Both refused directories are also skipped by every walk. `.git` is refused as
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
5
6
  * policy: a model that edits `.git/HEAD` corrupts a checkout in a way that
6
7
  * surfaces much later. `node_modules` is refused as a fact: it lives on the
7
- * container's disk (see `./host/container-deps.ts`), so these tools, which read
8
+ * container's disk (see `../workspace/container-deps.ts`), so these tools, which read
8
9
  * the workspace, would find it empty.
9
10
  *
10
11
  * Everything here is a string comparison: no filesystem, no `await`, which is
@@ -27,25 +28,36 @@ export function isGitInternal(path) {
27
28
  return hasSegment(path, ".git");
28
29
  }
29
30
  /**
30
- * What every walk steps over. `find` prunes these in the store; `grep` takes no
31
- * exclusion, so `sb_grep` filters its results instead.
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.
32
44
  */
33
- export const WALK_SKIPS = [".git", "node_modules"];
34
- /** Whether a walk carrying `skips` steps over `path`. */
35
- export function isSkipped(skips, path) {
36
- return skips.some((segment) => hasSegment(path, segment));
37
- }
38
- /** The skipped directories, for a sentence: "`.git` and `node_modules`". */
39
- export function skipNames(skips) {
40
- const quoted = skips.map((segment) => `\`${segment}\``);
41
- return quoted.length > 1
42
- ? `${quoted.slice(0, -1).join(", ")} and ${quoted[quoted.length - 1]}`
43
- : (quoted[0] ?? "");
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];
45
57
  /** The sentence a `node_modules` path gets. */
46
58
  function dependencyTreeNote(path, verb) {
47
59
  return (`${path} is inside node_modules, which lives on the container's disk rather ` +
48
- `than in the workspace ${verb} works on. Use sb_exec there — ` +
60
+ `than in the workspace ${verb} works on. Use bash there — ` +
49
61
  `\`cat\`, \`ls\`, \`rg\`.`);
50
62
  }
51
63
  /**
@@ -54,9 +66,9 @@ function dependencyTreeNote(path, verb) {
54
66
  * Names a route deliberately: a refusal with no destination is worse than no
55
67
  * refusal, because the model retries and then works around it.
56
68
  *
57
- * What it must never name is `sb_exec`. That hands back the exact capability
69
+ * What it must never name is `bash`. That hands back the exact capability
58
70
  * being withheld, in the one place the model is already looking for a way around
59
- * it, with the tool's own authority behind it. `sb_exec` is unguarded because a
71
+ * it, with the tool's own authority behind it. `bash` is unguarded because a
60
72
  * shell takes an opaque command string and pattern-matching git out of one is
61
73
  * neither reliable nor this guard's job — a fact about the implementation, not a
62
74
  * route to advertise.
@@ -65,12 +77,13 @@ function gitInternalNote(path, verb) {
65
77
  return (`${path} is inside .git — git's internal state, which ${verb} does not touch. ` +
66
78
  `Reading it tells you less than the repository tools do, and writing it ` +
67
79
  `corrupts the checkout. Repository work goes through the repo tools ` +
68
- `(\`repo_status\`, \`repo_diff\`, \`repo_commit\`, \`repo_push\`), which the ` +
69
- `main agent holds. If this task needs git state you cannot get that way, say ` +
70
- `so in your result rather than reaching into .git yourself.`);
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.`);
71
83
  }
72
84
  /**
73
- * The path check every file tool makes, before it opens anything.
85
+ * The path check every file tool and every workspace call makes, before it
86
+ * opens anything.
74
87
  *
75
88
  * Returns the sentence to hand back, or `undefined` to proceed. One string
76
89
  * comparison — no `stat`, no round trip — which is why every file tool can
@@ -81,15 +94,15 @@ function gitInternalNote(path, verb) {
81
94
  * The note names the tools repository work belongs to, because the case that
82
95
  * actually happens is a model reaching into `.git/HEAD` to fix a merge.
83
96
  *
84
- * It is not containment, and this is the only place worth saying so. `sb_exec`
85
- * is in the same tool family, granted per family rather than per tool, so every
86
- * agent holding these file tools also holds a shell that reads and writes `.git`
87
- * 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.
88
100
  *
89
101
  * Symlinks are not resolved, for the same reason: a tracked
90
102
  * `docs/notes.md -> ../.git/config` only matters to an agent given these tools
91
- * *without* the shell, which the family granularity makes unbuildable. If that
92
- * ever becomes buildable, the **write** path is the half to restore —
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 —
93
106
  * `.git/config` is an input to the credentialed push, and a planted hook runs
94
107
  * under a container-side `repo_commit`. Reading `.git` discloses nothing that is
95
108
  * not already readable, so the read half is not worth an `lstat` per call. The