@dynamicagents/plugins 0.16.1 → 0.17.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.
@@ -52,78 +52,31 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
52
52
  });
53
53
  import { withFileLock } from "./file-lock.js";
54
54
  import { writeGate } from "./gate.js";
55
- import { guardPath, WALK_SKIPS } from "./paths.js";
55
+ import { guardPath, walkSkips } from "./paths.js";
56
56
  /**
57
57
  * The key the route is carried under. `Symbol.for`, so a second copy of this
58
58
  * module in one bundle still recognises the first's workspaces.
59
59
  */
60
60
  const ROUTE = Symbol.for("@dynamicagents/plugins/computer:workspace");
61
- /**
62
- * The most one `glob` returns. Its caller is Think's `find`, which shows 200
63
- * and says when there were more — so one past that is all it can use, and
64
- * reading the rest of a large checkout into the object would buy nothing.
65
- */
66
- const GLOB_LIMIT = 201;
67
61
  /** The route of a workspace {@link routedWorkspace} built, or `undefined`. */
68
62
  export function workspaceRoute(workspace) {
69
63
  if (typeof workspace !== "object" || workspace === null)
70
64
  return undefined;
71
65
  return workspace[ROUTE];
72
66
  }
73
- /**
74
- * Missing, as the filesystem says it across the boundary. The code does not
75
- * always survive RPC, so the message is read too.
76
- */
77
- function isMissing(err) {
78
- if (!err || typeof err !== "object")
79
- return false;
80
- const { code, message } = err;
81
- if (code === "ENOENT")
82
- return true;
83
- return typeof message === "string" && /ENOENT|no such/i.test(message);
84
- }
85
- /** `null` for a path that is not there, which is what Think's tools expect. */
86
- async function orNull(read) {
87
- try {
88
- return await read;
89
- }
90
- catch (err) {
91
- if (isMissing(err))
92
- return null;
93
- throw err;
94
- }
95
- }
67
+ /** The bytes of a stream, for a host RPC that takes a value rather than one. */
96
68
  async function drain(stream) {
97
69
  return new Uint8Array(await new Response(stream).arrayBuffer());
98
70
  }
99
- function fileInfo(entry) {
100
- const type = entry.isDirectory ? "directory" : "file";
101
- return {
102
- path: entry.path,
103
- name: entry.path.slice(entry.path.lastIndexOf("/") + 1),
104
- type,
105
- // Generic on purpose: Think's `read` sniffs a generic type from the bytes,
106
- // and an extension is not evidence of what a file holds.
107
- mimeType: type === "directory" ? "inode/directory" : "application/octet-stream",
108
- size: entry.size ?? 0,
109
- createdAt: entry.mtime ?? 0,
110
- updatedAt: entry.mtime ?? 0
111
- };
112
- }
113
- /** The directory a glob walks from, and the pattern relative to it. */
114
- function splitGlob(pattern, cwd) {
115
- const absolute = pattern.startsWith("/") ? pattern : `${cwd}/${pattern}`;
116
- const wildcard = absolute.search(/[*?[{]/);
117
- const cut = wildcard === -1
118
- ? absolute.lastIndexOf("/")
119
- : absolute.lastIndexOf("/", wildcard);
120
- return {
121
- dir: cut <= 0 ? "/" : absolute.slice(0, cut),
122
- rest: absolute.slice(cut + 1)
123
- };
124
- }
125
71
  /** Think's file tools over `route`, carrying it for the computer tools to find. */
126
- export function routedWorkspace(route, cwd) {
72
+ /**
73
+ * `root` is the sandbox's served root and never a working directory. The
74
+ * directories Think writes its own files into are fixed under that root, so
75
+ * anchoring their exclusions anywhere else both hides a checkout's own
76
+ * `attachments/` and leaves Think's in a search of the root: see
77
+ * {@link file://./paths.ts walkSkips}.
78
+ */
79
+ export function routedWorkspace(route, root) {
127
80
  /** One call, against the workspace this turn names. */
128
81
  const withFs = async (path, verb, body) => {
129
82
  const env_1 = { stack: [], error: void 0, hasError: false };
@@ -158,42 +111,79 @@ export function routedWorkspace(route, cwd) {
158
111
  throw new Error(lost);
159
112
  return body(fs, name);
160
113
  });
161
- const workspace = {
162
- readFile: (path) =>
163
- // Whole, never truncated: Think's `edit` writes back what it read.
164
- withFs(path, "read", (fs) => orNull(fs.readFile(path, "utf8"))),
165
- readFileBytes: (path) => withFs(path, "read", async (fs) => {
166
- const stream = await orNull(fs.readFile(path));
167
- return stream === null ? null : drain(stream);
168
- }),
169
- stat: (path) => withFs(path, "read", async (fs) => {
170
- const stat = await orNull(fs.stat(path));
171
- return stat === null ? null : fileInfo({ ...stat, path });
172
- }),
173
- readDir: (dir = cwd, opts) => withFs(dir, "list", async (fs) => (await fs.readdir(dir, opts)).map((entry) => fileInfo({
114
+ /**
115
+ * Read whole or ranged, and as text or as bytes, in the one overloaded
116
+ * method Computer's filesystem declares. The options go through untouched:
117
+ * an `edit` reads text and writes back what it read, and a `read` of an
118
+ * evicted image asks for the bytes.
119
+ *
120
+ * **A host's `readFile` has to materialise what it answers**, because the
121
+ * client is released when this call returns and a stream that outlived it
122
+ * would be read against a disposed one. `/sandbox`'s does: it awaits the
123
+ * bytes and hands back a stream over them.
124
+ */
125
+ const readFile = (path, options) => withFs(path, "read", (fs) => options === undefined
126
+ ? fs.readFile(path)
127
+ : fs.readFile(path, options));
128
+ const fs = {
129
+ readFile: readFile,
130
+ stat: (path) => withFs(path, "read", async (fs) => ({
131
+ ...(await fs.stat(path)),
132
+ // The container has one, and the sandbox does not report it. Nothing
133
+ // downstream reads it: Computer's tools take `size`, `mtime`, `mode`
134
+ // and the three kind flags, and a number is all the type needs.
135
+ inode: 0
136
+ })),
137
+ readdir: (path, options) => withFs(path, "list", async (fs) => (await fs.readdir(path, options)).map((entry) => ({
174
138
  ...entry,
175
- path: dir === "/" ? `/${entry.name}` : `${dir}/${entry.name}`
139
+ // The directory this listing was read from, which is what a caller
140
+ // joins a name onto. The sandbox lists names and kinds, so the size
141
+ // and the time are the entry's own where it has them and zero where
142
+ // it does not: `ls` shows both to the model, and a second round trip
143
+ // per entry to fill them in would cost a directory's worth of RPCs.
144
+ parentPath: path,
145
+ size: entry.size ?? 0,
146
+ mtime: entry.mtime ?? 0,
147
+ isSymbolicLink: entry.isSymbolicLink ?? false
176
148
  }))),
177
149
  // Pruned rather than filtered: at a repository root either skipped
178
- // directory outnumbers everything else.
179
- glob: (pattern) => {
180
- const { dir, rest } = splitGlob(pattern, cwd);
181
- return withFs(dir, "find", async (fs) => (await fs.find(dir, rest, {
182
- limit: GLOB_LIMIT,
183
- exclude: WALK_SKIPS.map((segment) => `**/${segment}`)
184
- })).map((entry) => fileInfo({ path: entry.path, isDirectory: entry.type === "dir" })));
185
- },
186
- // Locked per file, so a write never lands inside the `edit` tool's read and
187
- // write of the same file — see `./file-lock.ts`.
188
- writeFile: (path, content) => withWritableFs(path, "write", (fs, name) => withFileLock(name, path, () => fs.writeFile(path, content))),
189
- mkdir: (path, opts) => withWritableFs(path, "write", (fs) => fs.mkdir(path, opts)),
150
+ // directory outnumbers everything else. No limit of this layer's own —
151
+ // `./sandbox.ts` caps a walk that arrives without one.
152
+ find: (directory, pattern, options) => withFs(directory, "find", (fs) => fs.find(directory, pattern, {
153
+ ...options,
154
+ exclude: [...walkSkips(directory, root), ...(options?.exclude ?? [])]
155
+ })),
156
+ grep: (pattern, path, options) => withFs(path, "search", (fs) => fs.grep(pattern, path, {
157
+ ...options,
158
+ exclude: [
159
+ ...walkSkips(path, root),
160
+ ...(options?.exclude ?? [])
161
+ ]
162
+ })),
163
+ /**
164
+ * Locked per file, so a write never lands inside the `edit` tool's read and
165
+ * write of the same file — see `./file-lock.ts`.
166
+ *
167
+ * A stream is drained here because the sandbox RPC takes a value. The
168
+ * `mode` a caller may send is dropped: the sandbox's write has nowhere to
169
+ * put one, so a mode this cannot set is not one it can claim to preserve.
170
+ */
171
+ writeFile: (path, content) => withWritableFs(path, "write", async (fs, name) => {
172
+ const body = content instanceof ReadableStream ? await drain(content) : content;
173
+ return withFileLock(name, path, () => fs.writeFile(path, body));
174
+ }),
175
+ mkdir: (path, options) => withWritableFs(path, "write", (fs) => fs.mkdir(path, options)),
190
176
  // Locked too: a delete landing between an edit's read and its write would
191
177
  // be undone by the write, and both would report success.
192
- rm: (path, opts) => withWritableFs(path, "delete", (fs, name) => withFileLock(name, path, () => fs.rm(path, opts)))
193
- // No `writeFileBytes`. Think writes through it on its own account —
194
- // evicted media, projected skills — and none of that belongs in a
195
- // repository's checkout. Without it Think skips both, with a warning.
178
+ rm: (path, options) => withWritableFs(path, "delete", (fs, name) => withFileLock(name, path, () => fs.rm(path, options)))
196
179
  };
180
+ /**
181
+ * `fs` alone. Computer's exec tool is offered only for a workspace carrying a
182
+ * `runtime`, so withholding one is how this keeps Think from building a
183
+ * second shell over the same container — `./tools-exec.ts` is the one the
184
+ * model gets, and it is this package's.
185
+ */
186
+ const workspace = { fs };
197
187
  Object.defineProperty(workspace, ROUTE, { value: route });
198
188
  return workspace;
199
189
  }
@@ -1,13 +1,13 @@
1
- import type { CommandResult, GrepMatch } from "./client.js";
1
+ import type { GrepMatch } from "./client.js";
2
2
  /**
3
3
  * Emit whole items until the byte budget is spent, and report exactly how many.
4
4
  *
5
- * {@link truncateOutput} is wrong for every list this module produces, and is
5
+ * Core's `truncateOutput` is wrong for every list this module produces, and is
6
6
  * deliberately not used by them. It keeps both ends and drops the middle, which is
7
- * right for a build log — first error, final summary — and destructive for a match
8
- * list or a directory listing, where the middle is a whole file's worth of hits or a
9
- * whole subtree that silently vanishes. So the budget is spent item by item, in
10
- * order, and the caller learns where it stopped.
7
+ * right for a build log and destructive for a match list or a directory listing,
8
+ * where the middle is a whole file's worth of hits or a whole subtree that silently
9
+ * vanishes. So the budget is spent item by item, in order, and the caller learns
10
+ * where it stopped.
11
11
  *
12
12
  * `shown` is the load-bearing return value: it is what makes the next page's offset
13
13
  * exact rather than a guess. A block is committed whole or not at all, because half
@@ -44,32 +44,3 @@ export declare function renderGrepMatches(matches: GrepMatch[], maxChars: number
44
44
  shown: number;
45
45
  capped: boolean;
46
46
  };
47
- /**
48
- * Render a completed command the way a terminal would, minus the noise.
49
- *
50
- * ## Every command ends with its verdict, including the successful ones
51
- *
52
- * Printing the exit code only when it is non-zero looks like saving noise and is
53
- * not: a model with no verdict appends `; echo "EXIT_CODE=$?"` itself, and
54
- * reaches for `${PIPESTATUS[0]}` to read a piped one — which dash does not
55
- * implement, so the whole line fails after the command it wrapped has run.
56
- *
57
- * ## `status` is not the exit code
58
- *
59
- * A `status` is `completed | failed | cancelled`. Dropping it
60
- * renders a command killed at the `timeoutMs` ceiling exactly like one that ran
61
- * to completion and failed — "your test suite was killed at ten minutes" and
62
- * "your test suite has a failing test", which want opposite responses. Anything
63
- * other than `completed` is stated.
64
- *
65
- * ## One budget, applied once
66
- *
67
- * Truncating per stream makes `maxOutputChars` mean "up to twice this". The bound
68
- * is on the rendered transcript, which is what reaches the context window.
69
- */
70
- export declare function renderResult(result: {
71
- exitCode: number;
72
- stdout: string;
73
- stderr: string;
74
- status?: CommandResult["status"];
75
- }, maxChars: number): string;
@@ -1,4 +1,3 @@
1
- import { truncateOutput } from "@dynamicagents/core";
2
1
  /**
3
2
  * Turning a result into the text a model reads.
4
3
  *
@@ -31,12 +30,12 @@ function capLine(text) {
31
30
  /**
32
31
  * Emit whole items until the byte budget is spent, and report exactly how many.
33
32
  *
34
- * {@link truncateOutput} is wrong for every list this module produces, and is
33
+ * Core's `truncateOutput` is wrong for every list this module produces, and is
35
34
  * deliberately not used by them. It keeps both ends and drops the middle, which is
36
- * right for a build log — first error, final summary — and destructive for a match
37
- * list or a directory listing, where the middle is a whole file's worth of hits or a
38
- * whole subtree that silently vanishes. So the budget is spent item by item, in
39
- * order, and the caller learns where it stopped.
35
+ * right for a build log and destructive for a match list or a directory listing,
36
+ * where the middle is a whole file's worth of hits or a whole subtree that silently
37
+ * vanishes. So the budget is spent item by item, in order, and the caller learns
38
+ * where it stopped.
40
39
  *
41
40
  * `shown` is the load-bearing return value: it is what makes the next page's offset
42
41
  * exact rather than a guess. A block is committed whole or not at all, because half
@@ -102,40 +101,3 @@ export function renderGrepMatches(matches, maxChars) {
102
101
  // Only a line that actually made it out is worth explaining.
103
102
  return { body, shown, capped: hasLongLine.slice(0, shown).includes(true) };
104
103
  }
105
- /**
106
- * Render a completed command the way a terminal would, minus the noise.
107
- *
108
- * ## Every command ends with its verdict, including the successful ones
109
- *
110
- * Printing the exit code only when it is non-zero looks like saving noise and is
111
- * not: a model with no verdict appends `; echo "EXIT_CODE=$?"` itself, and
112
- * reaches for `${PIPESTATUS[0]}` to read a piped one — which dash does not
113
- * implement, so the whole line fails after the command it wrapped has run.
114
- *
115
- * ## `status` is not the exit code
116
- *
117
- * A `status` is `completed | failed | cancelled`. Dropping it
118
- * renders a command killed at the `timeoutMs` ceiling exactly like one that ran
119
- * to completion and failed — "your test suite was killed at ten minutes" and
120
- * "your test suite has a failing test", which want opposite responses. Anything
121
- * other than `completed` is stated.
122
- *
123
- * ## One budget, applied once
124
- *
125
- * Truncating per stream makes `maxOutputChars` mean "up to twice this". The bound
126
- * is on the rendered transcript, which is what reaches the context window.
127
- */
128
- export function renderResult(result, maxChars) {
129
- const parts = [];
130
- if (result.stdout)
131
- parts.push(result.stdout);
132
- // Empty whenever the host configured a `shell`, which merges the streams at the
133
- // source so the transcript keeps its causal order. Kept for hosts that did not:
134
- // two labelled blocks beat silently dropping half the output.
135
- if (result.stderr)
136
- parts.push(`--- stderr ---\n${result.stderr}`);
137
- const body = truncateOutput(parts.join("\n"), maxChars);
138
- const state = result.status && result.status !== "completed" ? ` (${result.status})` : "";
139
- const verdict = `--- exit ${result.exitCode}${state} ---`;
140
- return body ? `${body}\n${verdict}` : `(no output)\n${verdict}`;
141
- }
@@ -1,4 +1,4 @@
1
- import type { WorkspaceLike } from "@cloudflare/think";
1
+ import type { ThinkWorkspace } from "@cloudflare/think";
2
2
  import type { SandboxAdvisory } from "../sandbox/advisory.js";
3
3
  import type { ComputerClient } from "./client.js";
4
4
  /**
@@ -27,11 +27,12 @@ export interface SandboxComputerHost extends Rpc.DurableObjectBranded {
27
27
  stderr: string;
28
28
  }>;
29
29
  readFile(path: string): Promise<Uint8Array | null>;
30
- writeFile(path: string, content: string): Promise<void>;
30
+ writeFile(path: string, content: string | Uint8Array): Promise<void>;
31
31
  stat(path: string): Promise<{
32
32
  type: string;
33
33
  size: number;
34
34
  modifiedAt: number;
35
+ mode: number;
35
36
  } | null>;
36
37
  readDir(path: string): Promise<{
37
38
  name: string;
@@ -50,6 +51,6 @@ export interface SandboxComputerConfig {
50
51
  sandboxName: () => string;
51
52
  cwd?: string;
52
53
  }
53
- export declare function computerWorkspace(config: SandboxComputerConfig, runtime?: () => unknown): WorkspaceLike;
54
+ export declare function computerWorkspace(config: SandboxComputerConfig, runtime?: () => unknown): ThinkWorkspace;
54
55
  /** The computer tools' client over one sandbox. */
55
56
  export declare function sandboxClient(host: SandboxComputerHost): ComputerClient;
@@ -1,4 +1,5 @@
1
1
  import { RUN_DIR } from "../sandbox/constants.js";
2
+ import { SANDBOX_ROOT } from "../sandbox/header.js";
2
3
  import { sandboxNameFromRuntime } from "../sandbox/name.js";
3
4
  import { routedWorkspace } from "./proxy.js";
4
5
  /** After `timeout`'s TERM, how long before its KILL. */
@@ -39,7 +40,10 @@ export function computerWorkspace(config, runtime) {
39
40
  name: () => sandboxNameFromRuntime(runtime?.()) ?? config.sandboxName(),
40
41
  open: async (name) => sandboxClient(host(name)),
41
42
  advisories: (name) => host(name).advisories()
42
- }, config.cwd ?? "/workspace");
43
+ },
44
+ // The served root, not `config.cwd`: a host may point the tools at a
45
+ // checkout, and Think's own directories stay where Think puts them.
46
+ SANDBOX_ROOT);
43
47
  }
44
48
  function missing(path) {
45
49
  return Object.assign(new Error(`ENOENT: no such file or directory, ${path}`), { code: "ENOENT" });
@@ -61,8 +65,10 @@ export function sandboxClient(host) {
61
65
  if (!stat)
62
66
  throw missing(path);
63
67
  return {
68
+ name: path.slice(path.lastIndexOf("/") + 1),
64
69
  size: stat.size,
65
70
  mtime: stat.modifiedAt,
71
+ mode: stat.mode,
66
72
  isFile: stat.type === "file",
67
73
  isDirectory: stat.type === "directory",
68
74
  isSymbolicLink: stat.type === "symlink"
@@ -75,7 +81,8 @@ export function sandboxClient(host) {
75
81
  .map((entry) => ({
76
82
  name: entry.name,
77
83
  isFile: entry.type === "file",
78
- isDirectory: entry.type === "directory"
84
+ isDirectory: entry.type === "directory",
85
+ isSymbolicLink: entry.type === "symlink"
79
86
  }));
80
87
  },
81
88
  find: (directory, pattern, options = {}) => find(host, directory, pattern, options),
@@ -3,7 +3,8 @@
3
3
  * two output streams merged into one transcript, in the order they were
4
4
  * written, or hand it back untouched when no shell is configured.
5
5
  *
6
- * `bash`'s consumer is **a model reading output**. A project's check is a chain
6
+ * The shell tool's consumer is **a model reading output**. A project's check is a
7
+ * chain
7
8
  * — `wrangler types && prettier && eslint && tsc` — and *which tool spoke last*
8
9
  * is how you know which one failed. A stdout block and a separate stderr block
9
10
  * destroy that ordering, and a model that wants it back re-runs the whole gate
@@ -25,12 +26,12 @@
25
26
  *
26
27
  * The trade is worth stating. `pipefail` surfaces SIGPIPE too, so
27
28
  * `ls big-dir | head -1` reports 141 rather than 0 — noisy, but *visible*. The
28
- * tool description tells the model not to pipe into `head`/`tail` at all, since
29
- * output is already truncated with both ends kept. It needs a shell that
30
- * implements it — `bash`, `zsh`, `ksh`, not `sh`/dash.
29
+ * tool's description is Computer's and says nothing about either, so the
30
+ * plugin's own context block in {@link file://./index.ts} is where the model is
31
+ * told not to pipe into `head`/`tail`. It needs a shell that implements it —
32
+ * `bash`, `zsh`, `ksh`, not `sh`/dash.
31
33
  *
32
- * With no shell configured the two streams arrive separate, and
33
- * {@link file://./render.ts renderResult} renders them as a labelled
34
- * `--- stderr ---` block.
34
+ * With no shell configured the two streams arrive separate, and the tool reports
35
+ * them as the `stdout` and `stderr` of its result.
35
36
  */
36
37
  export declare function withShellTranscript(command: string, shell: string | undefined): string;
@@ -4,7 +4,8 @@ import { shellQuote } from "../sandbox/agent-exec.js";
4
4
  * two output streams merged into one transcript, in the order they were
5
5
  * written, or hand it back untouched when no shell is configured.
6
6
  *
7
- * `bash`'s consumer is **a model reading output**. A project's check is a chain
7
+ * The shell tool's consumer is **a model reading output**. A project's check is a
8
+ * chain
8
9
  * — `wrangler types && prettier && eslint && tsc` — and *which tool spoke last*
9
10
  * is how you know which one failed. A stdout block and a separate stderr block
10
11
  * destroy that ordering, and a model that wants it back re-runs the whole gate
@@ -26,13 +27,13 @@ import { shellQuote } from "../sandbox/agent-exec.js";
26
27
  *
27
28
  * The trade is worth stating. `pipefail` surfaces SIGPIPE too, so
28
29
  * `ls big-dir | head -1` reports 141 rather than 0 — noisy, but *visible*. The
29
- * tool description tells the model not to pipe into `head`/`tail` at all, since
30
- * output is already truncated with both ends kept. It needs a shell that
31
- * implements it — `bash`, `zsh`, `ksh`, not `sh`/dash.
30
+ * tool's description is Computer's and says nothing about either, so the
31
+ * plugin's own context block in {@link file://./index.ts} is where the model is
32
+ * told not to pipe into `head`/`tail`. It needs a shell that implements it —
33
+ * `bash`, `zsh`, `ksh`, not `sh`/dash.
32
34
  *
33
- * With no shell configured the two streams arrive separate, and
34
- * {@link file://./render.ts renderResult} renders them as a labelled
35
- * `--- stderr ---` block.
35
+ * With no shell configured the two streams arrive separate, and the tool reports
36
+ * them as the `stdout` and `stderr` of its result.
36
37
  */
37
38
  export function withShellTranscript(command, shell) {
38
39
  return shell
@@ -1,3 +1,42 @@
1
1
  import type { ToolSet } from "ai";
2
2
  import type { ComputerContext } from "./context.js";
3
+ /**
4
+ * The container's shell, as Computer's `exec` tool.
5
+ *
6
+ * **The tool is upstream's, and deliberately not wrapped past the advisory
7
+ * gate.** Its schema, its description, its truncation and its result shape are
8
+ * `@cloudflare/computer`'s, so the model sees the same `exec` everywhere that
9
+ * package's tools are installed and this plugin has no second dialect to keep in
10
+ * step. What that costs, each of it accepted rather than overlooked:
11
+ *
12
+ * - **Truncation keeps the tail, not both ends.** Upstream cuts each stream to
13
+ * its last lines, so a failure printed early in a long build is gone from what
14
+ * the model reads. The exit code still reports it, and the model's recourse is
15
+ * a narrower command.
16
+ * - **The description promises a file this workspace does not write.** Upstream
17
+ * tells the model that truncated output is saved where `read` and `grep` can
18
+ * open it. That holds for a Workspace whose runtime saves it; this one reports
19
+ * no saved path, so the reply names none and the sentence is unfulfilled.
20
+ * Nothing can be set to turn it off: the sentence is built into the generated
21
+ * description.
22
+ * - **A command stopped at the time limit or cancelled reads as a plain
23
+ * failure.** Upstream's result has somewhere for an exit code and somewhere
24
+ * for an error, and nowhere for the difference between a command that outran
25
+ * its limit, one whose call was cancelled, and one that simply failed.
26
+ * {@link file://./client.ts CommandResult.status} still carries it for the log
27
+ * line below, which is the only place it is read.
28
+ *
29
+ * What stays this plugin's, because neither belongs to a tool:
30
+ *
31
+ * - **The advisory gate**, below: a command waits out a running install, is
32
+ * refused while something transient makes it pointless, and carries a note
33
+ * about anything permanent. {@link file://./gate.ts ExecGate} is the contract.
34
+ * - **How the container runs a command**, in
35
+ * {@link file://./sandbox.ts sandboxClient}: the launch-and-cancel handshake,
36
+ * the time limit, and the shell transcript. A configured shell merges the two
37
+ * streams onto one of them, so `stderr` arrives empty and the whole transcript
38
+ * is in `stdout` — see {@link file://./shell.ts withShellTranscript} for why a
39
+ * model reading output wants that, and why the plugin's context block rather
40
+ * than the tool description is where the model is told.
41
+ */
3
42
  export declare function execTools(ctx: ComputerContext): ToolSet;