@dynamicagents/plugins 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -77
- package/dist/browser/index.d.ts +0 -2
- package/dist/browser/index.js +21 -27
- package/dist/claude-code/cards.d.ts +55 -0
- package/dist/claude-code/cards.js +283 -0
- package/dist/claude-code/config.d.ts +28 -63
- package/dist/claude-code/config.js +0 -11
- package/dist/claude-code/egress.d.ts +8 -1
- package/dist/claude-code/egress.js +42 -4
- package/dist/claude-code/events.d.ts +106 -9
- package/dist/claude-code/events.js +201 -56
- package/dist/claude-code/github.d.ts +39 -0
- package/dist/claude-code/github.js +82 -0
- package/dist/claude-code/index.d.ts +16 -90
- package/dist/claude-code/index.js +14 -276
- package/dist/claude-code/model.d.ts +152 -0
- package/dist/claude-code/model.js +366 -0
- package/dist/claude-code/run.d.ts +230 -116
- package/dist/claude-code/run.js +380 -167
- package/dist/claude-code/session.d.ts +131 -0
- package/dist/claude-code/session.js +288 -0
- package/dist/claude-code/spec.d.ts +46 -0
- package/dist/claude-code/spec.js +83 -0
- package/dist/claude-code/verified.d.ts +15 -0
- package/dist/claude-code/verified.js +15 -0
- package/dist/computer/context.d.ts +1 -5
- package/dist/computer/context.js +7 -10
- package/dist/computer/file-lock.js +5 -4
- package/dist/computer/gate.d.ts +4 -4
- package/dist/computer/gate.js +5 -5
- package/dist/computer/index.d.ts +40 -258
- package/dist/computer/index.js +71 -324
- package/dist/computer/paths.d.ts +36 -16
- package/dist/computer/paths.js +42 -29
- package/dist/computer/proxy.d.ts +45 -0
- package/dist/computer/proxy.js +234 -0
- package/dist/computer/render.d.ts +1 -40
- package/dist/computer/render.js +2 -68
- package/dist/computer/tools-exec.js +12 -13
- package/dist/computer/tools-file.d.ts +9 -1
- package/dist/computer/tools-file.js +22 -82
- package/dist/computer/{tools-find.d.ts → tools-grep.d.ts} +1 -1
- package/dist/computer/tools-grep.js +129 -0
- package/dist/repo/checkout.d.ts +4 -5
- package/dist/repo/checkout.js +31 -8
- package/dist/repo/context.d.ts +35 -9
- package/dist/repo/context.js +101 -49
- package/dist/repo/index.d.ts +73 -28
- package/dist/repo/index.js +112 -96
- package/dist/repo/tools-checks.d.ts +3 -0
- package/dist/repo/tools-checks.js +112 -0
- package/dist/repo/tools-clone.js +1 -1
- package/dist/repo/tools-pr.d.ts +14 -0
- package/dist/repo/{tools-forge.js → tools-pr.js} +53 -36
- package/dist/repo/tools-review.d.ts +9 -0
- package/dist/repo/tools-review.js +67 -47
- package/dist/repo/tools-worktree.js +144 -60
- package/dist/repo/tools-worktrees.d.ts +12 -0
- package/dist/repo/tools-worktrees.js +48 -0
- package/dist/repo/url.d.ts +2 -2
- package/dist/repo/url.js +7 -7
- package/dist/scratch/index.d.ts +4 -4
- package/dist/scratch/index.js +12 -11
- package/dist/{computer → workspace}/advisory.d.ts +4 -4
- package/dist/{computer → workspace}/advisory.js +2 -2
- package/dist/{computer/host → workspace}/ca-trust.d.ts +8 -1
- package/dist/{computer/host → workspace}/ca-trust.js +12 -3
- package/dist/{computer/host → workspace}/container-deps.d.ts +4 -0
- package/dist/{computer/host → workspace}/container-deps.js +26 -1
- package/dist/{computer/host → workspace}/container-fault.d.ts +3 -3
- package/dist/{computer/host → workspace}/container-fault.js +23 -8
- package/dist/workspace/deps-snapshot.d.ts +110 -0
- package/dist/workspace/deps-snapshot.js +424 -0
- package/dist/workspace/exec.d.ts +94 -0
- package/dist/workspace/exec.js +153 -0
- package/dist/workspace/format.d.ts +36 -0
- package/dist/workspace/format.js +57 -0
- package/dist/{computer/host → workspace}/git-host.d.ts +21 -4
- package/dist/{computer/host → workspace}/git-host.js +72 -2
- package/dist/{computer/host → workspace}/git-identity.d.ts +1 -1
- package/dist/{computer/host → workspace}/git-identity.js +1 -1
- package/dist/workspace/index.d.ts +44 -47
- package/dist/workspace/index.js +52 -112
- package/dist/{computer/host → workspace}/install-job.d.ts +53 -4
- package/dist/{computer/host → workspace}/install-job.js +346 -178
- package/dist/{computer → workspace}/install.d.ts +17 -8
- package/dist/{computer → workspace}/install.js +24 -6
- package/dist/{computer/host/workspace.d.ts → workspace/object.d.ts} +125 -27
- package/dist/{computer/host/workspace.js → workspace/object.js} +341 -58
- package/dist/workspace/open.d.ts +95 -0
- package/dist/workspace/open.js +63 -0
- package/dist/workspace/read.d.ts +16 -0
- package/dist/workspace/read.js +22 -0
- package/dist/{computer → workspace}/shell.d.ts +5 -5
- package/dist/{computer → workspace}/shell.js +6 -6
- package/dist/{computer/host → workspace}/wake.d.ts +3 -1
- package/package.json +17 -37
- package/dist/arc-agi/analysis.d.ts +0 -322
- package/dist/arc-agi/analysis.js +0 -802
- package/dist/arc-agi/client.d.ts +0 -82
- package/dist/arc-agi/client.js +0 -113
- package/dist/arc-agi/game-tools.d.ts +0 -24
- package/dist/arc-agi/game-tools.js +0 -26
- package/dist/arc-agi/index.d.ts +0 -40
- package/dist/arc-agi/index.js +0 -175
- package/dist/arc-agi/main-agent.d.ts +0 -24
- package/dist/arc-agi/main-agent.js +0 -62
- package/dist/arc-agi/recipe.d.ts +0 -48
- package/dist/arc-agi/recipe.js +0 -97
- package/dist/arc-agi/scorecard.d.ts +0 -118
- package/dist/arc-agi/scorecard.js +0 -163
- package/dist/arc-agi/soul.d.ts +0 -29
- package/dist/arc-agi/soul.js +0 -79
- package/dist/arc-agi/store.d.ts +0 -153
- package/dist/arc-agi/store.js +0 -215
- package/dist/arc-agi/tools.d.ts +0 -36
- package/dist/arc-agi/tools.js +0 -574
- package/dist/arc-agi/types.d.ts +0 -211
- package/dist/arc-agi/types.js +0 -19
- package/dist/claude-code/recipe.d.ts +0 -53
- package/dist/claude-code/recipe.js +0 -100
- package/dist/computer/read.d.ts +0 -133
- package/dist/computer/read.js +0 -200
- package/dist/computer/tools-find.js +0 -216
- package/dist/recall/index.d.ts +0 -105
- package/dist/recall/index.js +0 -181
- package/dist/repo/tools-forge.d.ts +0 -4
- package/dist/triage/index.d.ts +0 -360
- package/dist/triage/index.js +0 -311
- /package/dist/{computer/host → workspace}/sync.d.ts +0 -0
- /package/dist/{computer/host → workspace}/sync.js +0 -0
- /package/dist/{computer/host → workspace}/wake.js +0 -0
package/dist/computer/index.js
CHANGED
|
@@ -1,189 +1,42 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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 {
|
|
7
|
+
import { grepTools } from "./tools-grep.js";
|
|
61
8
|
/**
|
|
62
|
-
* `@dynamicagents/plugins/computer` —
|
|
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
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
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
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
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
|
|
79
|
-
* the workspace, so they refuse it;
|
|
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
|
|
95
|
-
*
|
|
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 {
|
|
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
|
-
...
|
|
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
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
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";
|
package/dist/computer/paths.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Which paths these tools act on, and the sentence a refused one gets.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
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
|
|
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
|
-
*
|
|
25
|
-
*
|
|
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
|
|
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.
|
|
45
|
-
*
|
|
46
|
-
*
|
|
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
|
|
52
|
-
*
|
|
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
|
package/dist/computer/paths.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Which paths these tools act on, and the sentence a refused one gets.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
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
|
|
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
|
-
*
|
|
31
|
-
*
|
|
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
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
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 `
|
|
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. `
|
|
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\`)
|
|
69
|
-
`
|
|
70
|
-
`
|
|
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
|
|
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.
|
|
85
|
-
*
|
|
86
|
-
*
|
|
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
|
|
92
|
-
*
|
|
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
|