severally-mcp 2.3.1 → 2.4.2
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.ja.md +52 -17
- package/README.md +52 -16
- package/bin/severally-mcp.mjs +14 -2
- package/docs/install.ja.md +48 -20
- package/docs/install.md +48 -21
- package/package.json +15 -3
- package/plugins/severally/.claude-plugin/plugin.json +1 -1
- package/plugins/severally/.codex-plugin/plugin.json +1 -1
- package/plugins/severally/dist/severally-mcp.mjs +6573 -5978
- package/plugins/severally/skills/_template/reference.md.tmpl +2 -2
- package/plugins/severally/skills/antigravity/severally/reference.md +2 -2
- package/plugins/severally/skills/claude/severally/reference.md +2 -2
- package/plugins/severally/skills/codex/severally/reference.md +2 -2
- package/plugins/severally/skills/opencode/severally/reference.md +2 -2
- package/scripts/build.mjs +76 -68
- package/scripts/doctor.mjs +66 -20
- package/scripts/init-config.mjs +8 -6
- package/scripts/install.mjs +327 -90
- package/scripts/live-check.mjs +62 -23
- package/scripts/live-mcp-check.mjs +52 -16
- package/scripts/test.mjs +10 -4
- package/src/adapters/antigravity-sandbox.mjs +62 -28
- package/src/adapters/antigravity.mjs +111 -30
- package/src/adapters/claude-code.mjs +121 -28
- package/src/adapters/codex.mjs +125 -33
- package/src/adapters/opencode-sandbox.mjs +59 -22
- package/src/adapters/opencode.mjs +144 -30
- package/src/brief.mjs +148 -47
- package/src/consultants.mjs +255 -48
- package/src/cross-spawn-escape.d.ts +7 -0
- package/src/export.mjs +99 -18
- package/src/expose-paths.mjs +310 -50
- package/src/failures.mjs +31 -14
- package/src/jobs.mjs +863 -249
- package/src/jsonc.mjs +11 -3
- package/src/parse-result.mjs +172 -40
- package/src/platform.mjs +56 -8
- package/src/policy.mjs +245 -52
- package/src/redact.mjs +59 -16
- package/src/result-schema.mjs +13 -0
- package/src/run.mjs +225 -30
- package/src/schema.mjs +132 -27
- package/src/server.mjs +178 -81
- package/src/status.mjs +94 -13
- package/src/store.mjs +418 -35
|
@@ -30,6 +30,16 @@ import { POLICY } from '../policy.mjs';
|
|
|
30
30
|
import { classifyMessage } from '../failures.mjs';
|
|
31
31
|
import { spawnCommand } from '../platform.mjs';
|
|
32
32
|
|
|
33
|
+
/** @typedef {import('../run.mjs').BuildRequest} BuildRequest */
|
|
34
|
+
/** @typedef {import('../run.mjs').ChildResult} ChildResult */
|
|
35
|
+
/** @typedef {import('../run.mjs').Invocation} Invocation */
|
|
36
|
+
/** @typedef {import('../run.mjs').Interpreted} Interpreted */
|
|
37
|
+
/** @typedef {import('../run.mjs').Sandbox} Sandbox */
|
|
38
|
+
/** @typedef {import('../run.mjs').UsageRecord} UsageRecord */
|
|
39
|
+
/** @typedef {import('../run.mjs').Boundary} Boundary */
|
|
40
|
+
/** @typedef {import('../run.mjs').InterpretedOk} OpencodeOk */
|
|
41
|
+
/** @typedef {import('../run.mjs').InterpretedFail} InterpretedFail */
|
|
42
|
+
|
|
33
43
|
// Checked against opencode 1.18.31. The list exists so a future edit that
|
|
34
44
|
// reaches for one of them is refused rather than quietly widening the
|
|
35
45
|
// consultant:
|
|
@@ -82,21 +92,28 @@ export { prepareSandbox } from './opencode-sandbox.mjs';
|
|
|
82
92
|
// server lives and dies with the child's process group. 1.18.31 has no such
|
|
83
93
|
// flag (unknown flags die with a usage dump), so support is probed once per
|
|
84
94
|
// process through `run --help` and the invocation stays 1.x-exact without it.
|
|
95
|
+
/** @type {boolean | null} */
|
|
85
96
|
let standaloneSupport = null;
|
|
97
|
+
/** @type {Promise<void> | null} */
|
|
86
98
|
let probePromise = null;
|
|
87
99
|
|
|
88
100
|
/** Probe the CLI's capabilities once per process; safe to call per job. */
|
|
89
101
|
export async function prepare() {
|
|
90
102
|
if (standaloneSupport !== null) return;
|
|
91
103
|
if (!probePromise) {
|
|
92
|
-
probePromise = new Promise((resolve) => {
|
|
104
|
+
probePromise = new Promise((/** @type {() => void} */ resolve) => {
|
|
93
105
|
let settled = false;
|
|
106
|
+
/** @type {NodeJS.Timeout | null} */
|
|
94
107
|
let timer = null;
|
|
95
108
|
// Only a SUCCESSFUL help invocation may cache its answer: a spawn
|
|
96
109
|
// error, a timeout or a nonzero exit is transient, and caching it as
|
|
97
110
|
// "unsupported" would pin a 2.x install to the shared-service route
|
|
98
111
|
// for the process lifetime. Transient outcomes resolve without
|
|
99
112
|
// caching, so the next job probes again.
|
|
113
|
+
/**
|
|
114
|
+
* @param {boolean} supported
|
|
115
|
+
* @param {boolean} cache
|
|
116
|
+
*/
|
|
100
117
|
const finishProbe = (supported, cache) => {
|
|
101
118
|
if (settled) return;
|
|
102
119
|
settled = true;
|
|
@@ -110,12 +127,18 @@ export async function prepare() {
|
|
|
110
127
|
// STUB_* observables (a stubbed CLI cannot tell it ran), keeping the
|
|
111
128
|
// STUB_HELP knobs so a test can still shape the advertised flag list
|
|
112
129
|
// and the probe's latency.
|
|
113
|
-
const env = Object.fromEntries(
|
|
114
|
-
.filter(([k]) => !k.startsWith('STUB_') || k === 'STUB_HELP' || k === 'STUB_HELP_DELAY_MS')
|
|
130
|
+
const env = Object.fromEntries(
|
|
131
|
+
Object.entries(process.env).filter(([k]) => !k.startsWith('STUB_') || k === 'STUB_HELP' || k === 'STUB_HELP_DELAY_MS'),
|
|
132
|
+
);
|
|
115
133
|
const child = spawnCommand(POLICY.clis.opencode.cli, ['run', '--help'], { env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
116
134
|
let out = '';
|
|
117
|
-
|
|
118
|
-
child.
|
|
135
|
+
// stdio above is 'pipe' for both, so the streams exist.
|
|
136
|
+
/** @type {NonNullable<typeof child.stdout>} */ (child.stdout).on('data', (c) => {
|
|
137
|
+
out += c;
|
|
138
|
+
});
|
|
139
|
+
/** @type {NonNullable<typeof child.stderr>} */ (child.stderr).on('data', (c) => {
|
|
140
|
+
out += c;
|
|
141
|
+
});
|
|
119
142
|
child.on('error', () => finishProbe(false, false));
|
|
120
143
|
child.on('close', (code) => {
|
|
121
144
|
// A nonzero exit proves nothing either way -- a diagnostic that
|
|
@@ -123,7 +146,14 @@ export async function prepare() {
|
|
|
123
146
|
if (code !== 0) return finishProbe(false, false);
|
|
124
147
|
finishProbe(out.includes('--standalone'), true);
|
|
125
148
|
});
|
|
126
|
-
timer = setTimeout(() => {
|
|
149
|
+
timer = setTimeout(() => {
|
|
150
|
+
try {
|
|
151
|
+
child.kill('SIGKILL');
|
|
152
|
+
} catch {
|
|
153
|
+
/* gone */
|
|
154
|
+
}
|
|
155
|
+
finishProbe(false, false);
|
|
156
|
+
}, 10_000);
|
|
127
157
|
timer.unref();
|
|
128
158
|
} catch {
|
|
129
159
|
finishProbe(false, false);
|
|
@@ -150,6 +180,8 @@ export async function resetProbe() {
|
|
|
150
180
|
* and the caller retries once. Strictly once per job; every other failure
|
|
151
181
|
* kind -- an auth rejection, a usage limit, invalid output -- means a
|
|
152
182
|
* respawn is wasted compute.
|
|
183
|
+
* @param {{ interpreted: Interpreted, sandbox: Sandbox | undefined, model: string }} args
|
|
184
|
+
* @returns {boolean}
|
|
153
185
|
*/
|
|
154
186
|
export function recoverAuthFailure({ interpreted, sandbox, model }) {
|
|
155
187
|
if (!interpreted || interpreted.ok !== false || interpreted.failureKind !== 'model_unavailable') return false;
|
|
@@ -164,13 +196,18 @@ export function recoverAuthFailure({ interpreted, sandbox, model }) {
|
|
|
164
196
|
// adapter adds or leaves out by capability probe.
|
|
165
197
|
export const RESERVED_FLAGS = ['--model', '--standalone', '--wizard'];
|
|
166
198
|
|
|
199
|
+
/**
|
|
200
|
+
* @param {BuildRequest} req
|
|
201
|
+
* @returns {Invocation}
|
|
202
|
+
*/
|
|
167
203
|
export function buildInvocation({ consultant }) {
|
|
168
204
|
const t = POLICY.clis.opencode;
|
|
169
205
|
const chosen = consultant.model;
|
|
170
206
|
const effort = consultant.effort ?? 'high';
|
|
171
207
|
const args = [
|
|
172
208
|
'run',
|
|
173
|
-
'--format',
|
|
209
|
+
'--format',
|
|
210
|
+
'json',
|
|
174
211
|
...(standaloneSupport ? ['--standalone'] : []),
|
|
175
212
|
// A failing 2.x run can exit nonzero with no error event and an empty
|
|
176
213
|
// stderr (observed on GLM after ~5 min), leaving nothing to say why. Its
|
|
@@ -183,14 +220,51 @@ export function buildInvocation({ consultant }) {
|
|
|
183
220
|
// name the operator wrote with its own `#variant` is passed as is, and a
|
|
184
221
|
// bare trailing `#` means "no variant", for models that have none
|
|
185
222
|
// ("Variant unavailable for opencode/nemotron-3-ultra-free: high").
|
|
186
|
-
'-m',
|
|
187
|
-
'
|
|
223
|
+
'-m',
|
|
224
|
+
chosen.endsWith('#') ? chosen.slice(0, -1) : chosen.includes('#') ? chosen : `${chosen}#${effort}`,
|
|
225
|
+
'--title',
|
|
226
|
+
'severally',
|
|
188
227
|
];
|
|
189
228
|
return { command: t.cli, args, model: chosen };
|
|
190
229
|
}
|
|
191
230
|
|
|
192
|
-
/**
|
|
231
|
+
/**
|
|
232
|
+
* One NDJSON event from `opencode run --format json`; the CLI controls it, so
|
|
233
|
+
* every field is optional. `error` is {type, message} on 2.x and
|
|
234
|
+
* {name, data:{message}} on 1.x.
|
|
235
|
+
* @typedef {object} OpencodeEvent
|
|
236
|
+
* @property {string} [type]
|
|
237
|
+
* @property {{
|
|
238
|
+
* type?: string,
|
|
239
|
+
* text?: unknown,
|
|
240
|
+
* time?: { end?: unknown },
|
|
241
|
+
* tokens?: OpencodeTokens,
|
|
242
|
+
* cost?: unknown,
|
|
243
|
+
* tool?: string,
|
|
244
|
+
* callID?: string,
|
|
245
|
+
* id?: string,
|
|
246
|
+
* state?: { status?: string },
|
|
247
|
+
* }} [part]
|
|
248
|
+
* @property {{ message?: unknown, name?: unknown, data?: { message?: unknown } }} [error]
|
|
249
|
+
*/
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* The token figures of a step-finish part.
|
|
253
|
+
* @typedef {{ input?: unknown, output?: unknown, total?: unknown, reasoning?: unknown, cache_read?: unknown, cache?: { read?: unknown } }} OpencodeTokens
|
|
254
|
+
*/
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* The usage figures interpret collects from the event stream.
|
|
258
|
+
* @typedef {{ tokens?: OpencodeTokens | null, cost?: unknown, steps?: unknown }} OpencodeUsage
|
|
259
|
+
*/
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Every complete NDJSON object opencode wrote to stdout, oldest first.
|
|
263
|
+
* @param {string | undefined} stdout
|
|
264
|
+
* @returns {OpencodeEvent[]}
|
|
265
|
+
*/
|
|
193
266
|
export function events(stdout) {
|
|
267
|
+
/** @type {OpencodeEvent[]} */
|
|
194
268
|
const out = [];
|
|
195
269
|
for (const line of (stdout || '').split('\n')) {
|
|
196
270
|
const t = line.trim();
|
|
@@ -198,7 +272,9 @@ export function events(stdout) {
|
|
|
198
272
|
try {
|
|
199
273
|
const parsed = JSON.parse(t);
|
|
200
274
|
if (parsed && typeof parsed === 'object') out.push(parsed);
|
|
201
|
-
} catch {
|
|
275
|
+
} catch {
|
|
276
|
+
/* a partial line, e.g. when the child was killed mid-write */
|
|
277
|
+
}
|
|
202
278
|
}
|
|
203
279
|
return out;
|
|
204
280
|
}
|
|
@@ -210,19 +286,28 @@ export function events(stdout) {
|
|
|
210
286
|
* filled rawCaptureMax and cut the final answer off the stream. Nothing here
|
|
211
287
|
* reads tool results: interpret wants text/step_finish/error, and progress
|
|
212
288
|
* only the tool name.
|
|
289
|
+
* @param {string} line
|
|
290
|
+
* @returns {string}
|
|
213
291
|
*/
|
|
214
292
|
export function compactLine(line) {
|
|
215
293
|
if (!line.includes('"tool_use"')) return line;
|
|
216
294
|
try {
|
|
217
295
|
const e = JSON.parse(line);
|
|
218
296
|
if (e?.type !== 'tool_use') return line;
|
|
219
|
-
return JSON.stringify({
|
|
297
|
+
return JSON.stringify({
|
|
298
|
+
type: e.type,
|
|
299
|
+
part: { type: e.part?.type, tool: e.part?.tool, state: { status: e.part?.state?.status } },
|
|
300
|
+
});
|
|
220
301
|
} catch {
|
|
221
302
|
return line;
|
|
222
303
|
}
|
|
223
304
|
}
|
|
224
305
|
|
|
225
|
-
/**
|
|
306
|
+
/**
|
|
307
|
+
* One line naming the error opencode reported, in classifyMessage's terms.
|
|
308
|
+
* @param {NonNullable<OpencodeEvent['error']>} error
|
|
309
|
+
* @returns {string}
|
|
310
|
+
*/
|
|
226
311
|
function errorMessage(error) {
|
|
227
312
|
if (!error || typeof error !== 'object') return JSON.stringify(error);
|
|
228
313
|
// 2.x: {type, message} -- the message is already a complete human sentence,
|
|
@@ -230,9 +315,7 @@ function errorMessage(error) {
|
|
|
230
315
|
// message are joined, as before. A CLI upgrade must degrade the wording and
|
|
231
316
|
// never the classification.
|
|
232
317
|
if (typeof error.message === 'string' && error.message) return error.message;
|
|
233
|
-
const detail = error.data && typeof error.data === 'object' && error.data.message
|
|
234
|
-
? String(error.data.message)
|
|
235
|
-
: '';
|
|
318
|
+
const detail = error.data && typeof error.data === 'object' && error.data.message ? String(error.data.message) : '';
|
|
236
319
|
return [error.name, detail].filter(Boolean).join(': ') || JSON.stringify(error);
|
|
237
320
|
}
|
|
238
321
|
|
|
@@ -242,6 +325,8 @@ function errorMessage(error) {
|
|
|
242
325
|
* log line lands). Raw stdout is never the message: it is the event stream
|
|
243
326
|
* of a run that was working fine, its head says nothing about the failure, and
|
|
244
327
|
* numbers inside it ("reasoning":403) would be classified as HTTP statuses.
|
|
328
|
+
* @param {{ errors: string[], stderr?: string | undefined, stdout?: string | undefined, code?: number | null | undefined, signal?: NodeJS.Signals | null | undefined, what: string }} run
|
|
329
|
+
* @returns {{ msg: string, kind: string }}
|
|
245
330
|
*/
|
|
246
331
|
function failureMessage({ errors, stderr, stdout, code, signal, what }) {
|
|
247
332
|
if (errors.length) {
|
|
@@ -258,20 +343,25 @@ function failureMessage({ errors, stderr, stdout, code, signal, what }) {
|
|
|
258
343
|
* Parse the event stream `opencode run --format json` writes to stdout.
|
|
259
344
|
* The answer is the LAST completed text part: a consultation can emit prose
|
|
260
345
|
* between tool calls, and only the final part carries the reply contract.
|
|
346
|
+
* @param {Partial<ChildResult>} run
|
|
347
|
+
* @returns {OpencodeOk | InterpretedFail}
|
|
261
348
|
*/
|
|
262
349
|
export function interpret({ stdout, stderr, code, signal = null, truncated = false }) {
|
|
263
350
|
const all = events(stdout);
|
|
351
|
+
/** @type {string | null} */
|
|
264
352
|
let text = null;
|
|
265
353
|
let textIndex = -1;
|
|
266
354
|
let lastErrorIndex = -1;
|
|
355
|
+
let lastError = '';
|
|
267
356
|
let finishedAfterText = false;
|
|
268
357
|
let tokens = null;
|
|
269
358
|
let cost = null;
|
|
270
359
|
let steps = 0;
|
|
360
|
+
/** @type {string[]} */
|
|
271
361
|
const errors = [];
|
|
362
|
+
/** @type {string[]} */
|
|
272
363
|
const earlierTextParts = [];
|
|
273
|
-
for (
|
|
274
|
-
const e = all[i];
|
|
364
|
+
for (const [i, e] of all.entries()) {
|
|
275
365
|
const part = e.part;
|
|
276
366
|
if (e.type === 'text' && part?.type === 'text' && part.time?.end && typeof part.text === 'string' && part.text.trim()) {
|
|
277
367
|
// The last completed part wins as the answer, but every part that had
|
|
@@ -291,7 +381,8 @@ export function interpret({ stdout, stderr, code, signal = null, truncated = fal
|
|
|
291
381
|
if (part.tokens && typeof part.tokens === 'object') tokens = part.tokens;
|
|
292
382
|
if (typeof part.cost === 'number') cost = part.cost;
|
|
293
383
|
} else if (e.type === 'error' && e.error) {
|
|
294
|
-
|
|
384
|
+
lastError = errorMessage(e.error);
|
|
385
|
+
errors.push(lastError);
|
|
295
386
|
lastErrorIndex = i;
|
|
296
387
|
}
|
|
297
388
|
}
|
|
@@ -311,7 +402,7 @@ export function interpret({ stdout, stderr, code, signal = null, truncated = fal
|
|
|
311
402
|
// failure. What text exists is interim prose, and 2.x exits 0 even on
|
|
312
403
|
// provider errors, so order -- not the exit code -- is what tells this
|
|
313
404
|
// apart from the recovered-retry case below.
|
|
314
|
-
const msg =
|
|
405
|
+
const msg = lastError;
|
|
315
406
|
return { ok: false, failureKind: classifyMessage(msg), message: msg, usageRaw };
|
|
316
407
|
}
|
|
317
408
|
if (code !== 0) {
|
|
@@ -321,7 +412,7 @@ export function interpret({ stdout, stderr, code, signal = null, truncated = fal
|
|
|
321
412
|
const { msg, kind } = failureMessage({ errors, stderr, stdout, code, signal, what: 'after producing an answer' });
|
|
322
413
|
return { ok: false, failureKind: kind, message: msg, usageRaw };
|
|
323
414
|
}
|
|
324
|
-
if (truncated && !(finishedAfterText && all.
|
|
415
|
+
if (truncated && !(finishedAfterText && all.at(-1)?.type === 'step_finish')) {
|
|
325
416
|
// stdout hit the rawCaptureMax before the consultation finished, so the
|
|
326
417
|
// final answer event may never have been read. Under truncation only a
|
|
327
418
|
// stream that ENDS on a step-finish -- the terminal event of a completed
|
|
@@ -344,34 +435,53 @@ export function interpret({ stdout, stderr, code, signal = null, truncated = fal
|
|
|
344
435
|
return { ok: true, text, earlierTextParts, usageRaw };
|
|
345
436
|
}
|
|
346
437
|
|
|
438
|
+
/** The tools that read a file's contents (glob only lists names); a consultation whose calls to them all failed read none of its exposed files. */
|
|
439
|
+
export const FILE_READ_TOOLS = ['read', 'grep'];
|
|
440
|
+
|
|
347
441
|
/**
|
|
348
442
|
* What this consultation has done so far -- read while it runs. Separates
|
|
349
443
|
* "working steadily through tool calls" from "wedged on one call", which is
|
|
350
444
|
* the difference between raising the budget and narrowing the brief.
|
|
351
445
|
*/
|
|
352
|
-
/**
|
|
446
|
+
/**
|
|
447
|
+
* Tool calls seen in the event stream, one per call id; `:error` when the call failed or was refused.
|
|
448
|
+
* @param {{ stdout?: string }} run
|
|
449
|
+
* @returns {Record<string, number>}
|
|
450
|
+
*/
|
|
353
451
|
export function toolCalls({ stdout }) {
|
|
452
|
+
/** @type {Map<unknown, string>} */
|
|
354
453
|
const seen = new Map();
|
|
355
454
|
for (const e of events(stdout)) {
|
|
356
455
|
if (e.type === 'tool_use' && e.part?.tool) {
|
|
357
456
|
seen.set(e.part.callID ?? e.part.id ?? seen.size, e.part.state?.status === 'error' ? `${e.part.tool}:error` : e.part.tool);
|
|
358
457
|
}
|
|
359
458
|
}
|
|
360
|
-
return [...seen.values()].reduce((o, n) => ({ ...o, [n]: (o[n] ?? 0) + 1 }), {});
|
|
459
|
+
return [...seen.values()].reduce((/** @type {Record<string, number>} */ o, n) => ({ ...o, [n]: (o[n] ?? 0) + 1 }), {});
|
|
361
460
|
}
|
|
362
461
|
|
|
462
|
+
/**
|
|
463
|
+
* @param {{ stdout?: string | undefined }} run
|
|
464
|
+
* @returns {string | null}
|
|
465
|
+
*/
|
|
363
466
|
export function progress({ stdout }) {
|
|
364
|
-
const seen = events(stdout).filter(
|
|
365
|
-
|
|
366
|
-
|
|
467
|
+
const seen = events(stdout).filter(
|
|
468
|
+
(e) => e.type !== undefined && ['step_start', 'step_finish', 'text', 'tool_use'].includes(e.type),
|
|
469
|
+
);
|
|
470
|
+
const last = seen.at(-1);
|
|
471
|
+
if (!last) return null;
|
|
367
472
|
const what = last.type === 'tool_use' && last.part?.tool ? `tool ${last.part.tool}` : last.type;
|
|
368
473
|
return `${seen.length} event(s), last was ${what}`;
|
|
369
474
|
}
|
|
370
475
|
|
|
476
|
+
/**
|
|
477
|
+
* @param {unknown} raw the usage figures interpret collected from the event stream
|
|
478
|
+
* @returns {UsageRecord}
|
|
479
|
+
*/
|
|
371
480
|
export function usageRecord(raw) {
|
|
372
|
-
|
|
481
|
+
// interpret's usageRaw is the OpencodeUsage it built (or null).
|
|
482
|
+
const r = /** @type {OpencodeUsage | null} */ (raw) ?? {};
|
|
373
483
|
const t = r.tokens && typeof r.tokens === 'object' ? r.tokens : {};
|
|
374
|
-
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : null);
|
|
484
|
+
const num = (/** @type {unknown} */ v) => (typeof v === 'number' && Number.isFinite(v) ? v : null);
|
|
375
485
|
const cache = t.cache && typeof t.cache === 'object' ? t.cache : {};
|
|
376
486
|
return {
|
|
377
487
|
input_tokens: num(t.input),
|
|
@@ -389,15 +499,19 @@ export function usageRecord(raw) {
|
|
|
389
499
|
* What this consultant was launched with, read off the invocation: the
|
|
390
500
|
* boundary record kept beside each answer. Facts about the launch, not a
|
|
391
501
|
* claim about what the consultant did.
|
|
502
|
+
* @param {{ sandbox: Sandbox | null }} launch
|
|
503
|
+
* @returns {Boundary}
|
|
392
504
|
*/
|
|
393
505
|
export function boundary({ sandbox }) {
|
|
506
|
+
// buildInvocation always runs with the sandbox prepareSandbox made.
|
|
507
|
+
const { tools } = /** @type {Sandbox} */ (sandbox);
|
|
394
508
|
return {
|
|
395
509
|
reads: 'whole disk',
|
|
396
510
|
shell: 'none',
|
|
397
|
-
web:
|
|
511
|
+
web: tools.includes('webfetch'),
|
|
398
512
|
writes: false,
|
|
399
513
|
mcp: false,
|
|
400
514
|
protected_paths: 'denied (permission rules)',
|
|
401
|
-
tools: [...
|
|
515
|
+
tools: [...tools],
|
|
402
516
|
};
|
|
403
517
|
}
|