severally-mcp 2.4.0 → 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 +44 -19
- package/README.md +44 -18
- package/package.json +3 -1
- 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 +585 -255
- 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 +57 -54
- package/scripts/doctor.mjs +15 -4
- package/scripts/install.mjs +73 -9
- package/scripts/live-check.mjs +11 -5
- package/scripts/live-mcp-check.mjs +10 -4
- package/src/adapters/antigravity-sandbox.mjs +24 -6
- package/src/adapters/antigravity.mjs +80 -17
- package/src/adapters/claude-code.mjs +49 -8
- package/src/adapters/codex.mjs +56 -6
- package/src/adapters/opencode-sandbox.mjs +20 -2
- package/src/adapters/opencode.mjs +115 -19
- package/src/brief.mjs +84 -7
- package/src/consultants.mjs +184 -25
- package/src/cross-spawn-escape.d.ts +7 -0
- package/src/export.mjs +50 -6
- package/src/expose-paths.mjs +149 -7
- package/src/failures.mjs +8 -1
- package/src/jobs.mjs +578 -132
- package/src/jsonc.mjs +11 -3
- package/src/parse-result.mjs +91 -14
- package/src/platform.mjs +52 -2
- package/src/policy.mjs +151 -28
- package/src/redact.mjs +22 -3
- package/src/result-schema.mjs +13 -0
- package/src/run.mjs +158 -12
- package/src/schema.mjs +84 -11
- package/src/server.mjs +49 -14
- package/src/status.mjs +16 -4
- package/src/store.mjs +287 -6
package/src/policy.mjs
CHANGED
|
@@ -7,7 +7,10 @@ import os from 'node:os';
|
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import { parseJsonc } from './jsonc.mjs';
|
|
9
9
|
import { findCommand } from './platform.mjs';
|
|
10
|
-
import { buildConsultants, normalizeName } from './consultants.mjs';
|
|
10
|
+
import { buildConsultants, isObject, normalizeName } from './consultants.mjs';
|
|
11
|
+
|
|
12
|
+
/** @typedef {import('./consultants.mjs').Consultant} Consultant */
|
|
13
|
+
/** @typedef {import('./consultants.mjs').CliId} CliId */
|
|
11
14
|
|
|
12
15
|
// One file, read once at start, so an operator configures a machine in a
|
|
13
16
|
// single place instead of editing each client's MCP registration. Precedence
|
|
@@ -22,7 +25,9 @@ const CONFIG_PATH =
|
|
|
22
25
|
[path.join(CONFIG_HOME, 'config.jsonc'), path.join(CONFIG_HOME, 'config.json')].find((p) => fs.existsSync(p)) ||
|
|
23
26
|
path.join(CONFIG_HOME, 'config.json');
|
|
24
27
|
|
|
28
|
+
/** @type {string | null} */
|
|
25
29
|
let configError = null;
|
|
30
|
+
/** @type {unknown} the parsed config file, before validation */
|
|
26
31
|
const CONFIG = (() => {
|
|
27
32
|
try {
|
|
28
33
|
return parseJsonc(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
|
@@ -30,7 +35,8 @@ const CONFIG = (() => {
|
|
|
30
35
|
// A missing file is the normal case. A malformed one is not: silently
|
|
31
36
|
// ignoring it would run the machine on defaults the operator thinks they
|
|
32
37
|
// overrode, so it is surfaced on stderr when the server starts.
|
|
33
|
-
|
|
38
|
+
const code = err instanceof Error && 'code' in err ? err.code : undefined;
|
|
39
|
+
if (code !== 'ENOENT') configError = `${CONFIG_PATH}: ${err instanceof Error ? err.message : String(err)}`;
|
|
34
40
|
return {};
|
|
35
41
|
}
|
|
36
42
|
})();
|
|
@@ -50,7 +56,10 @@ if (!configError && ![undefined, '', 'on', 'off'].includes(process.env.SEVERALLY
|
|
|
50
56
|
const FROM = configError ? buildConsultants({}) : BUILT;
|
|
51
57
|
const LOCAL = configError ? [] : BUILT.problems.filter((p) => !BUILT.global.includes(p));
|
|
52
58
|
|
|
53
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* What is wrong with the config file as a whole, or null. Reported at startup; while set, nothing can be started.
|
|
61
|
+
* @returns {string | null}
|
|
62
|
+
*/
|
|
54
63
|
export function configProblem() {
|
|
55
64
|
return configError;
|
|
56
65
|
}
|
|
@@ -65,34 +74,66 @@ export function brokenConsultantNames() {
|
|
|
65
74
|
return configError ? [] : Object.keys(BUILT.broken);
|
|
66
75
|
}
|
|
67
76
|
|
|
68
|
-
/**
|
|
77
|
+
/**
|
|
78
|
+
* Why the consultant of this name is out of use because of a config mistake, or null.
|
|
79
|
+
* @param {string} name
|
|
80
|
+
*/
|
|
69
81
|
export function brokenConsultant(name) {
|
|
70
82
|
const p = !configError && Object.hasOwn(BUILT.broken, name) ? BUILT.broken[name] : null;
|
|
71
83
|
return p ? `the config file has a mistake in it: ${p.join('; ')}` : null;
|
|
72
84
|
}
|
|
73
85
|
|
|
74
|
-
|
|
86
|
+
/**
|
|
87
|
+
* One CLI's section of the config file. `args`, `env` and `provider` are read
|
|
88
|
+
* only after buildConsultants has checked them (a section with a mistake is
|
|
89
|
+
* replaced by a disabled one in cfgCli), so they have the types given here.
|
|
90
|
+
* @typedef {object} CliSection
|
|
91
|
+
* @property {unknown} [enabled]
|
|
92
|
+
* @property {unknown} [note]
|
|
93
|
+
* @property {unknown} [bin]
|
|
94
|
+
* @property {string[]} [args]
|
|
95
|
+
* @property {Record<string, string>} [env]
|
|
96
|
+
* @property {Record<string, Record<string, unknown>>} [provider]
|
|
97
|
+
*/
|
|
98
|
+
|
|
99
|
+
/** @type {Record<string, unknown>} */
|
|
100
|
+
const usable = configError || !isObject(CONFIG) ? {} : CONFIG;
|
|
75
101
|
// A CLI whose own section has a mistake is off, whatever else the section says.
|
|
102
|
+
/**
|
|
103
|
+
* @param {string} id
|
|
104
|
+
* @returns {CliSection}
|
|
105
|
+
*/
|
|
76
106
|
const cfgCli = (id) => {
|
|
77
|
-
|
|
107
|
+
const broken = Object.hasOwn(BUILT.brokenClis, id) ? BUILT.brokenClis[id] : undefined;
|
|
108
|
+
if (broken && !configError) {
|
|
78
109
|
return {
|
|
79
110
|
enabled: false,
|
|
80
|
-
note: `turned off: the config file has a mistake in clis.${id}: ${
|
|
111
|
+
note: `turned off: the config file has a mistake in clis.${id}: ${broken.join('; ')}`,
|
|
81
112
|
};
|
|
82
113
|
}
|
|
83
|
-
|
|
114
|
+
const def = isObject(usable.clis) ? usable.clis[id] : undefined;
|
|
115
|
+
// A section reaches here only when buildConsultants found no mistake in it.
|
|
116
|
+
return isObject(def) ? /** @type {CliSection} */ (def) : {};
|
|
84
117
|
};
|
|
85
|
-
const cfgServer = usable.server
|
|
118
|
+
const cfgServer = isObject(usable.server) ? usable.server : {};
|
|
86
119
|
|
|
87
120
|
/**
|
|
88
121
|
* Is this command runnable here? Absolute/relative paths are checked as given.
|
|
89
122
|
* Exported so the config generator can report what a machine has without
|
|
90
123
|
* going through POLICY, which already reflects any config file it found.
|
|
124
|
+
* @param {string} command
|
|
91
125
|
*/
|
|
92
126
|
export function isInstalled(command) {
|
|
93
127
|
return findCommand(command) !== null;
|
|
94
128
|
}
|
|
95
129
|
|
|
130
|
+
/**
|
|
131
|
+
* @param {string} name
|
|
132
|
+
* @param {number | null} dflt
|
|
133
|
+
* @param {number} min
|
|
134
|
+
* @param {number} max
|
|
135
|
+
* @returns {number | null}
|
|
136
|
+
*/
|
|
96
137
|
const num = (name, dflt, min, max) => {
|
|
97
138
|
const raw = process.env[name];
|
|
98
139
|
if (raw === undefined || raw === '') return dflt;
|
|
@@ -105,6 +146,14 @@ const num = (name, dflt, min, max) => {
|
|
|
105
146
|
// the same range whichever one supplied it. Codex and OpenCode do not pass the
|
|
106
147
|
// user's shell environment to an MCP server, so for them the file is the only
|
|
107
148
|
// place an operator can reach without editing the client's registration.
|
|
149
|
+
/**
|
|
150
|
+
* @param {string} name
|
|
151
|
+
* @param {unknown} fromCfg
|
|
152
|
+
* @param {number} dflt
|
|
153
|
+
* @param {number} min
|
|
154
|
+
* @param {number} max
|
|
155
|
+
* @returns {number}
|
|
156
|
+
*/
|
|
108
157
|
const limit = (name, fromCfg, dflt, min, max) => {
|
|
109
158
|
const fromEnv = num(name, null, min, max);
|
|
110
159
|
if (fromEnv !== null) return fromEnv;
|
|
@@ -112,6 +161,11 @@ const limit = (name, fromCfg, dflt, min, max) => {
|
|
|
112
161
|
return dflt;
|
|
113
162
|
};
|
|
114
163
|
|
|
164
|
+
/**
|
|
165
|
+
* @param {string} name
|
|
166
|
+
* @param {string} dflt
|
|
167
|
+
* @returns {string}
|
|
168
|
+
*/
|
|
115
169
|
const str = (name, dflt) => {
|
|
116
170
|
const raw = process.env[name];
|
|
117
171
|
return raw === undefined || raw === '' ? dflt : raw;
|
|
@@ -120,6 +174,7 @@ const str = (name, dflt) => {
|
|
|
120
174
|
// The CLI a lead runs in, by any everyday name. Only `caller` uses this:
|
|
121
175
|
// which consultant a name reaches is consultants.mjs's resolveConsultant.
|
|
122
176
|
// Object.create(null) so 'constructor' is an unknown name, not a function.
|
|
177
|
+
/** @type {Readonly<Record<string, CliId>>} */
|
|
123
178
|
export const CALLER_ALIASES = Object.freeze(
|
|
124
179
|
Object.assign(Object.create(null), {
|
|
125
180
|
codex: 'codex',
|
|
@@ -140,14 +195,22 @@ export const CALLER_ALIASES = Object.freeze(
|
|
|
140
195
|
|
|
141
196
|
export const CALLER_INPUTS = Object.freeze(Object.keys(CALLER_ALIASES));
|
|
142
197
|
|
|
143
|
-
/**
|
|
198
|
+
/**
|
|
199
|
+
* The CLI id for any accepted spelling of a caller, or null.
|
|
200
|
+
* @param {unknown} raw
|
|
201
|
+
* @returns {CliId | null}
|
|
202
|
+
*/
|
|
144
203
|
export function resolveCaller(raw) {
|
|
145
204
|
const key = normalizeName(raw);
|
|
146
|
-
return typeof key === 'string'
|
|
205
|
+
return typeof key === 'string' ? (CALLER_ALIASES[key] ?? null) : null;
|
|
147
206
|
}
|
|
148
207
|
|
|
149
208
|
// Which CLI is hosting this server. Used only to annotate a consultation that
|
|
150
209
|
// goes to the caller's own vendor; it never affects permissions or limits.
|
|
210
|
+
/**
|
|
211
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
212
|
+
* @returns {CliId | null}
|
|
213
|
+
*/
|
|
151
214
|
export function detectCaller(env = process.env) {
|
|
152
215
|
if (env.CLAUDECODE === '1' || env.CLAUDE_CODE_ENTRYPOINT) return 'claude-code';
|
|
153
216
|
if (env.CODEX_HOME || env.CODEX_SANDBOX || env.CODEX_SANDBOX_NETWORK_DISABLED) return 'codex';
|
|
@@ -167,12 +230,15 @@ export const VERDICTS = ['unverified', 'confirmed', 'not_applicable', 'unverifia
|
|
|
167
230
|
// The consultant's own one-word bottom line. Also what the lead may predict
|
|
168
231
|
// before asking -- the same vocabulary, so a prediction and an answer can be
|
|
169
232
|
// read side by side without anyone translating between two scales.
|
|
170
|
-
export const STANCES = ['proceed', 'do_not_proceed', 'alternative', 'undetermined'];
|
|
233
|
+
export const STANCES = /** @type {const} */ (['proceed', 'do_not_proceed', 'alternative', 'undetermined']);
|
|
171
234
|
|
|
172
235
|
/**
|
|
173
236
|
* A leading ~ is a shell courtesy, not a path: nothing expands it here, so
|
|
174
237
|
* "~/.local/bin/claudex" would simply not exist and the consultant would drop
|
|
175
238
|
* out of the available list without saying why. Expand it where a path is read.
|
|
239
|
+
* @template T
|
|
240
|
+
* @param {T} value
|
|
241
|
+
* @returns {T | string}
|
|
176
242
|
*/
|
|
177
243
|
function expandHome(value) {
|
|
178
244
|
if (typeof value !== 'string') return value;
|
|
@@ -181,6 +247,12 @@ function expandHome(value) {
|
|
|
181
247
|
}
|
|
182
248
|
|
|
183
249
|
// env > config file > built-in default, for a CLI's bin.
|
|
250
|
+
/**
|
|
251
|
+
* @param {string} id
|
|
252
|
+
* @param {string} envName
|
|
253
|
+
* @param {string} dflt
|
|
254
|
+
* @returns {string}
|
|
255
|
+
*/
|
|
184
256
|
const binFor = (id, envName, dflt) => {
|
|
185
257
|
const fromEnv = process.env[envName];
|
|
186
258
|
if (fromEnv !== undefined && fromEnv !== '') return expandHome(fromEnv);
|
|
@@ -191,11 +263,16 @@ const binFor = (id, envName, dflt) => {
|
|
|
191
263
|
// Why a consultant is off, in the operator's words: "quota exhausted until
|
|
192
264
|
// 15:00" is more use to a lead than a bare refusal, and it is the reason a CLI
|
|
193
265
|
// that IS installed gets excluded.
|
|
266
|
+
/** @param {string} id */
|
|
194
267
|
const disabledNote = (id) => {
|
|
195
268
|
const n = cfgCli(id).note;
|
|
196
269
|
return typeof n === 'string' && n.trim() !== '' ? n.trim() : null;
|
|
197
270
|
};
|
|
198
271
|
|
|
272
|
+
/**
|
|
273
|
+
* @param {string} id
|
|
274
|
+
* @param {string} cli
|
|
275
|
+
*/
|
|
199
276
|
const isEnabled = (id, cli) => {
|
|
200
277
|
const flag = cfgCli(id).enabled;
|
|
201
278
|
if (typeof flag === 'boolean') return flag;
|
|
@@ -210,7 +287,14 @@ const OPENCODE_BIN = binFor('opencode', 'SEVERALLY_OPENCODE_BIN', 'opencode');
|
|
|
210
287
|
// Operator-only extras for the child process (config: args, env). Config-only,
|
|
211
288
|
// never from a request, and validated at launch (jobs.mjs) where the isolation
|
|
212
289
|
// flags and variables they must not undo are known.
|
|
213
|
-
|
|
290
|
+
/**
|
|
291
|
+
* @param {string} id
|
|
292
|
+
* @returns {{ extraArgs: string[], extraEnv: Record<string, string> }}
|
|
293
|
+
*/
|
|
294
|
+
const extras = (id) => ({
|
|
295
|
+
extraArgs: cfgCli(id).args ?? [],
|
|
296
|
+
extraEnv: cfgCli(id).env ?? {},
|
|
297
|
+
});
|
|
214
298
|
|
|
215
299
|
export const POLICY = Object.freeze({
|
|
216
300
|
home: str('SEVERALLY_HOME', path.join(os.homedir(), '.severally')),
|
|
@@ -316,6 +400,8 @@ export const POLICY = Object.freeze({
|
|
|
316
400
|
}),
|
|
317
401
|
});
|
|
318
402
|
|
|
403
|
+
/** @typedef {typeof POLICY} Policy */
|
|
404
|
+
|
|
319
405
|
/**
|
|
320
406
|
* Wall-clock budget for one consultation of this consultant. Read at job
|
|
321
407
|
* start (not frozen into the module) so an operator restart is not needed to
|
|
@@ -323,6 +409,7 @@ export const POLICY = Object.freeze({
|
|
|
323
409
|
* same time: a long Gemini answer should not force the same patience on every
|
|
324
410
|
* other consultant. The two hour ceiling stays -- past that the answer is a
|
|
325
411
|
* different request, not a slower one.
|
|
412
|
+
* @param {Consultant | null} [consultant]
|
|
326
413
|
*/
|
|
327
414
|
export function timeoutMs(consultant) {
|
|
328
415
|
return consultant?.timeoutMs ?? limit('SEVERALLY_TIMEOUT_MS', cfgServer.timeout_ms, 1_800_000, 1_000, 7_200_000);
|
|
@@ -417,8 +504,12 @@ export const PROTECTED_FILE_NAMES = Object.freeze([
|
|
|
417
504
|
'id_dsa',
|
|
418
505
|
]);
|
|
419
506
|
|
|
420
|
-
/**
|
|
507
|
+
/**
|
|
508
|
+
* Absolute protected roots, with forward slashes; `*` stands for one path segment.
|
|
509
|
+
* @returns {string[]}
|
|
510
|
+
*/
|
|
421
511
|
export function protectedRoots() {
|
|
512
|
+
/** @param {string} p */
|
|
422
513
|
const fwd = (p) => p.replaceAll('\\', '/');
|
|
423
514
|
return [
|
|
424
515
|
...PROTECTED_HOME_PATHS.map((rel) => fwd(path.join(os.homedir(), rel))),
|
|
@@ -426,10 +517,14 @@ export function protectedRoots() {
|
|
|
426
517
|
];
|
|
427
518
|
}
|
|
428
519
|
|
|
520
|
+
/** @param {string} s */
|
|
429
521
|
const escapeRe = (s) => s.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
|
430
522
|
// Case-insensitive where the default file system is: Windows and macOS.
|
|
431
523
|
export const PATHS_CASE_INSENSITIVE = process.platform === 'win32' || process.platform === 'darwin';
|
|
432
|
-
/**
|
|
524
|
+
/**
|
|
525
|
+
* Regex sources: one per protected root, matching it and anything under it.
|
|
526
|
+
* @param {string[]} [extraRoots]
|
|
527
|
+
*/
|
|
433
528
|
export function protectedRootPatterns(extraRoots = []) {
|
|
434
529
|
return [...protectedRoots(), ...extraRoots.map((r) => r.replaceAll('\\', '/'))].map(
|
|
435
530
|
(r) => `^${escapeRe(r.replace(/\/+$/, '')).replaceAll('*', '[^/]+')}(/|$)`,
|
|
@@ -440,7 +535,10 @@ export function protectedNamePatterns() {
|
|
|
440
535
|
return PROTECTED_FILE_NAMES.map((n) => `^${escapeRe(n).replaceAll('*', '.*')}$`);
|
|
441
536
|
}
|
|
442
537
|
|
|
443
|
-
/**
|
|
538
|
+
/**
|
|
539
|
+
* Whether an absolute path, or the file it links to, is protected.
|
|
540
|
+
* @param {string} abs
|
|
541
|
+
*/
|
|
444
542
|
export function isProtectedPath(abs) {
|
|
445
543
|
const flags = PATHS_CASE_INSENSITIVE ? 'i' : '';
|
|
446
544
|
const roots = protectedRootPatterns().map((p) => new RegExp(p, flags));
|
|
@@ -459,12 +557,31 @@ export function isProtectedPath(abs) {
|
|
|
459
557
|
|
|
460
558
|
export const artifactKinds = ['code', 'log', 'doc', 'data', 'diff', 'spec', 'test-output', 'config'];
|
|
461
559
|
|
|
462
|
-
/**
|
|
560
|
+
/**
|
|
561
|
+
* The consultant with this exact name, or undefined.
|
|
562
|
+
* @param {string} name
|
|
563
|
+
*/
|
|
463
564
|
export function consultantByName(name) {
|
|
464
565
|
return POLICY.consultants.find((c) => c.name === name);
|
|
465
566
|
}
|
|
466
567
|
|
|
467
|
-
/**
|
|
568
|
+
/**
|
|
569
|
+
* The consultant with this exact name, for a caller that holds a name it got
|
|
570
|
+
* from a consultant already (a started job's target). Throws when there is
|
|
571
|
+
* none; use consultantByName when the name may be unknown.
|
|
572
|
+
* @param {string} name
|
|
573
|
+
* @returns {Consultant}
|
|
574
|
+
*/
|
|
575
|
+
export function consultantNamed(name) {
|
|
576
|
+
const c = consultantByName(name);
|
|
577
|
+
if (!c) throw new Error(`no consultant named "${name}"`);
|
|
578
|
+
return c;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Can this consultant run here: turned on itself, and its CLI usable.
|
|
583
|
+
* @param {Consultant} c
|
|
584
|
+
*/
|
|
468
585
|
export function consultantAvailable(c) {
|
|
469
586
|
return c.enabled && POLICY.clis[c.cli].available;
|
|
470
587
|
}
|
|
@@ -472,6 +589,8 @@ export function consultantAvailable(c) {
|
|
|
472
589
|
/**
|
|
473
590
|
* Why this consultant cannot be reached, or null when it can. The operator's
|
|
474
591
|
* own note wins: "installed but rate-limited" is a case only they can state.
|
|
592
|
+
* @param {Consultant | null | undefined} c
|
|
593
|
+
* @returns {string | null}
|
|
475
594
|
*/
|
|
476
595
|
export function unavailableReason(c) {
|
|
477
596
|
if (!c || consultantAvailable(c)) return null;
|
|
@@ -489,6 +608,7 @@ export function availableConsultants() {
|
|
|
489
608
|
|
|
490
609
|
export function limitsSummary() {
|
|
491
610
|
const list = availableConsultants();
|
|
611
|
+
/** @param {(c: Consultant) => unknown} f */
|
|
492
612
|
const by = (f) => Object.fromEntries(list.map((c) => [c.name, f(c)]));
|
|
493
613
|
return {
|
|
494
614
|
available_targets: list.map((c) => c.name),
|
|
@@ -504,16 +624,19 @@ export function limitsSummary() {
|
|
|
504
624
|
max_concurrent_jobs: POLICY.maxConcurrent,
|
|
505
625
|
max_wait_ms: POLICY.maxWaitMs,
|
|
506
626
|
input_char_budget: POLICY.input.totalCharsMax,
|
|
507
|
-
// Stated so it is true of all
|
|
508
|
-
//
|
|
509
|
-
//
|
|
510
|
-
//
|
|
511
|
-
//
|
|
627
|
+
// Stated so it is true of all four CLIs. Reading is allowed for every one
|
|
628
|
+
// of them -- what keeps a consultation brief-only is that the child starts
|
|
629
|
+
// in an empty working directory and is never told where the repository
|
|
630
|
+
// is, not that it cannot read. Only Claude Code drops its whole-disk read
|
|
631
|
+
// when files are exposed (claude-code.mjs). Codex is additionally
|
|
632
|
+
// sandboxed read-only rather than execution-free, so do not advertise
|
|
633
|
+
// "no execution".
|
|
512
634
|
consultant_permissions:
|
|
513
|
-
'web search/browse and reading local files allowed
|
|
514
|
-
'directory and is not told where your repository is
|
|
515
|
-
'directories
|
|
516
|
-
'
|
|
517
|
-
'
|
|
635
|
+
'web search/browse and reading local files allowed; the consultant starts in an empty working ' +
|
|
636
|
+
'directory and is not told where your repository is. context.expose_paths copies the files or ' +
|
|
637
|
+
'directories it names read-only into that working directory, but that is not a read boundary: apart ' +
|
|
638
|
+
'from the protected credential paths, Codex, Antigravity and OpenCode can read any file your account ' +
|
|
639
|
+
'can, and so can Claude Code when nothing is exposed; file edits, network access, MCP tools and ' +
|
|
640
|
+
'further consultations denied; the Codex consultant additionally has a read-only shell',
|
|
518
641
|
};
|
|
519
642
|
}
|
package/src/redact.mjs
CHANGED
|
@@ -20,6 +20,10 @@ const CODE_WORDS = new Set([
|
|
|
20
20
|
// variable reference (${X}, $X, process.env.X, os.environ[...]), and -- unless
|
|
21
21
|
// it is a long value under a full PASSWORD/PASSWD name, which stays masked
|
|
22
22
|
// whatever it looks like -- a dotted name, a call or a borrow (&str).
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} name
|
|
25
|
+
* @param {string} value
|
|
26
|
+
*/
|
|
23
27
|
function looksLikeCode(name, value) {
|
|
24
28
|
if (
|
|
25
29
|
CODE_WORDS.has(value.toLowerCase()) ||
|
|
@@ -28,12 +32,12 @@ function looksLikeCode(name, value) {
|
|
|
28
32
|
/^(?:process\.env\.|os\.environ\b)/.test(value)
|
|
29
33
|
)
|
|
30
34
|
return true;
|
|
31
|
-
if (value.length >= 8 && /^PASSW(?:OR)?D$/i.test(name.split('_').at(-1))) return false;
|
|
35
|
+
if (value.length >= 8 && /^PASSW(?:OR)?D$/i.test(name.split('_').at(-1) ?? '')) return false;
|
|
32
36
|
return /^[A-Za-z_][\w$]*(?:\.[A-Za-z_$][\w$]*)+$/.test(value) || /^[A-Za-z_][\w$.]*\(/.test(value) || value.startsWith('&');
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
// A replacement is a string or a function of the match.
|
|
36
|
-
/** @type {Array<[RegExp,
|
|
40
|
+
/** @type {Array<[RegExp, string | ((...groups: string[]) => string)]>} */
|
|
37
41
|
const PATTERNS = [
|
|
38
42
|
// Provider API keys
|
|
39
43
|
[/\bsk-ant-[A-Za-z0-9_-]{12,}/g, '[REDACTED:anthropic-key]'],
|
|
@@ -75,15 +79,29 @@ const PATTERNS = [
|
|
|
75
79
|
[/\b([A-Z0-9_]+_KEY)(\s*=\s*["']?)([^\s"',;)]{8,})/g, (_m, k, sep, _v) => `${k}${sep}[REDACTED]`],
|
|
76
80
|
];
|
|
77
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Any JSON-like value in, the same shape out with its strings scrubbed.
|
|
84
|
+
* @template T
|
|
85
|
+
* @overload
|
|
86
|
+
* @param {T} value
|
|
87
|
+
* @returns {T}
|
|
88
|
+
*/
|
|
89
|
+
/**
|
|
90
|
+
* @param {unknown} value
|
|
91
|
+
* @returns {unknown}
|
|
92
|
+
*/
|
|
78
93
|
export function redact(value) {
|
|
79
94
|
if (value === null || value === undefined) return value;
|
|
80
95
|
if (typeof value === 'string') {
|
|
96
|
+
/** @type {string} */
|
|
81
97
|
let out = value;
|
|
82
|
-
|
|
98
|
+
// The same call either way: replace() has one overload for a string and one for a function.
|
|
99
|
+
for (const [re, rep] of PATTERNS) out = typeof rep === 'string' ? out.replace(re, rep) : out.replace(re, rep);
|
|
83
100
|
return out;
|
|
84
101
|
}
|
|
85
102
|
if (Array.isArray(value)) return value.map(redact);
|
|
86
103
|
if (typeof value === 'object') {
|
|
104
|
+
/** @type {Record<string, unknown>} */
|
|
87
105
|
const out = {};
|
|
88
106
|
for (const [k, v] of Object.entries(value)) out[k] = redact(v);
|
|
89
107
|
return out;
|
|
@@ -91,6 +109,7 @@ export function redact(value) {
|
|
|
91
109
|
return value;
|
|
92
110
|
}
|
|
93
111
|
|
|
112
|
+
/** @param {unknown} text */
|
|
94
113
|
export function containsSecret(text) {
|
|
95
114
|
if (typeof text !== 'string') return false;
|
|
96
115
|
return redact(text) !== text;
|
package/src/result-schema.mjs
CHANGED
|
@@ -3,9 +3,18 @@
|
|
|
3
3
|
// no validation keywords beyond type/enum) so it can be handed verbatim to
|
|
4
4
|
// Codex's --output-schema and Claude Code's --json-schema.
|
|
5
5
|
|
|
6
|
+
/** @param {string} desc */
|
|
6
7
|
const s = (desc) => ({ type: 'string', description: desc });
|
|
8
|
+
/**
|
|
9
|
+
* @param {string[]} values
|
|
10
|
+
* @param {string} desc
|
|
11
|
+
*/
|
|
7
12
|
const enumOf = (values, desc) => ({ type: 'string', enum: values, description: desc });
|
|
8
13
|
|
|
14
|
+
/**
|
|
15
|
+
* @param {Record<string, object>} properties
|
|
16
|
+
* @param {string} [description]
|
|
17
|
+
*/
|
|
9
18
|
const obj = (properties, description) => ({
|
|
10
19
|
type: 'object',
|
|
11
20
|
description,
|
|
@@ -14,6 +23,10 @@ const obj = (properties, description) => ({
|
|
|
14
23
|
additionalProperties: false,
|
|
15
24
|
});
|
|
16
25
|
|
|
26
|
+
/**
|
|
27
|
+
* @param {object} items
|
|
28
|
+
* @param {string} description
|
|
29
|
+
*/
|
|
17
30
|
const arr = (items, description) => ({ type: 'array', description, items });
|
|
18
31
|
|
|
19
32
|
export const CONSULT_RESULT_SCHEMA = obj(
|