severally-mcp 2.1.0 → 2.4.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/CODE_OF_CONDUCT.md +83 -0
- package/CONFIG.md +10 -6
- package/README.ja.md +146 -69
- package/README.md +150 -73
- package/bin/severally-mcp.mjs +17 -2
- package/config.example.json +26 -0
- package/docs/how-it-works.ja.md +4 -2
- package/docs/how-it-works.md +5 -3
- package/docs/install.ja.md +59 -24
- package/docs/install.md +60 -24
- package/package.json +16 -4
- 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 +29220 -27764
- package/plugins/severally/skills/_template/SKILL.md.tmpl +16 -22
- package/plugins/severally/skills/_template/reference.md.tmpl +11 -5
- package/plugins/severally/skills/antigravity/severally/SKILL.md +16 -22
- package/plugins/severally/skills/antigravity/severally/reference.md +11 -5
- package/plugins/severally/skills/claude/severally/SKILL.md +16 -22
- package/plugins/severally/skills/claude/severally/reference.md +11 -5
- package/plugins/severally/skills/codex/severally/SKILL.md +16 -22
- package/plugins/severally/skills/codex/severally/reference.md +11 -5
- package/plugins/severally/skills/opencode/severally/SKILL.md +16 -22
- package/plugins/severally/skills/opencode/severally/reference.md +11 -5
- package/scripts/build.mjs +31 -26
- package/scripts/doctor.mjs +52 -17
- package/scripts/init-config.mjs +8 -6
- package/scripts/install.mjs +342 -84
- package/scripts/live-check.mjs +52 -19
- package/scripts/live-mcp-check.mjs +42 -12
- package/scripts/test.mjs +10 -4
- package/src/adapters/antigravity-sandbox.mjs +39 -23
- package/src/adapters/antigravity.mjs +34 -16
- package/src/adapters/claude-code.mjs +73 -21
- package/src/adapters/codex.mjs +69 -27
- package/src/adapters/opencode-sandbox.mjs +40 -21
- package/src/adapters/opencode.mjs +31 -13
- package/src/brief.mjs +65 -41
- package/src/consultants.mjs +99 -30
- package/src/export.mjs +49 -12
- package/src/expose-paths.mjs +162 -44
- package/src/failures.mjs +23 -13
- package/src/jobs.mjs +420 -140
- package/src/parse-result.mjs +85 -30
- package/src/platform.mjs +5 -7
- package/src/policy.mjs +139 -32
- package/src/redact.mjs +38 -14
- package/src/run.mjs +104 -22
- package/src/schema.mjs +76 -22
- package/src/server.mjs +176 -166
- package/src/status.mjs +132 -0
- package/src/store.mjs +164 -19
package/src/parse-result.mjs
CHANGED
|
@@ -60,14 +60,26 @@ export function extractJson(text) {
|
|
|
60
60
|
let esc = false;
|
|
61
61
|
for (let i = start; i < trimmed.length; i++) {
|
|
62
62
|
const ch = trimmed[i];
|
|
63
|
-
if (esc) {
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
if (esc) {
|
|
64
|
+
esc = false;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (ch === '\\') {
|
|
68
|
+
esc = true;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (ch === '"') {
|
|
72
|
+
inStr = !inStr;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
66
75
|
if (inStr) continue;
|
|
67
76
|
if (ch === '{') depth++;
|
|
68
77
|
else if (ch === '}') {
|
|
69
78
|
depth--;
|
|
70
|
-
if (depth === 0) {
|
|
79
|
+
if (depth === 0) {
|
|
80
|
+
candidates.push(trimmed.slice(start, i + 1));
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
71
83
|
}
|
|
72
84
|
}
|
|
73
85
|
}
|
|
@@ -75,7 +87,9 @@ export function extractJson(text) {
|
|
|
75
87
|
try {
|
|
76
88
|
const parsed = JSON.parse(c);
|
|
77
89
|
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed;
|
|
78
|
-
} catch {
|
|
90
|
+
} catch {
|
|
91
|
+
/* try the next candidate */
|
|
92
|
+
}
|
|
79
93
|
}
|
|
80
94
|
return null;
|
|
81
95
|
}
|
|
@@ -101,7 +115,7 @@ function mapList(raw, fields, { requiredField, idPrefix = null }) {
|
|
|
101
115
|
|
|
102
116
|
/**
|
|
103
117
|
* @param {object|string} raw parsed JSON object, or raw text to mine one out of
|
|
104
|
-
* @returns {{result: object, quality: {
|
|
118
|
+
* @returns {{result: object, quality: {missing_sections: string[], findings_without_grounds: number, unsourced: boolean, list_truncated?: Record<string, number>}}}
|
|
105
119
|
*/
|
|
106
120
|
export function normalizeResult(raw) {
|
|
107
121
|
const objIn = typeof raw === 'string' ? extractJson(raw) : raw;
|
|
@@ -120,37 +134,78 @@ export function normalizeResult(raw) {
|
|
|
120
134
|
confidence: level(src.confidence),
|
|
121
135
|
evidence_basis: basis(src.evidence_basis),
|
|
122
136
|
stance: stance(src.stance),
|
|
123
|
-
findings: mapList(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
137
|
+
findings: mapList(
|
|
138
|
+
src.findings,
|
|
139
|
+
{
|
|
140
|
+
point: 'text',
|
|
141
|
+
grounds: 'text',
|
|
142
|
+
impact: 'text',
|
|
143
|
+
severity: 'level',
|
|
144
|
+
confidence: 'level',
|
|
145
|
+
},
|
|
146
|
+
{ requiredField: 'point', idPrefix: 'f' },
|
|
147
|
+
),
|
|
148
|
+
alternatives: mapList(
|
|
149
|
+
src.alternatives,
|
|
150
|
+
{
|
|
151
|
+
option: 'text',
|
|
152
|
+
tradeoffs: 'text',
|
|
153
|
+
when_preferred: 'text',
|
|
154
|
+
},
|
|
155
|
+
{ requiredField: 'option' },
|
|
156
|
+
),
|
|
157
|
+
unknowns: mapList(
|
|
158
|
+
src.unknowns,
|
|
159
|
+
{
|
|
160
|
+
item: 'text',
|
|
161
|
+
why_it_matters: 'text',
|
|
162
|
+
how_to_obtain: 'text',
|
|
163
|
+
},
|
|
164
|
+
{ requiredField: 'item', idPrefix: 'u' },
|
|
165
|
+
),
|
|
166
|
+
decision_changers: mapList(
|
|
167
|
+
src.decision_changers,
|
|
168
|
+
{
|
|
169
|
+
condition: 'text',
|
|
170
|
+
changes_to: 'text',
|
|
171
|
+
},
|
|
172
|
+
{ requiredField: 'condition' },
|
|
173
|
+
),
|
|
174
|
+
next_checks: mapList(
|
|
175
|
+
src.next_checks,
|
|
176
|
+
{
|
|
177
|
+
check: 'text',
|
|
178
|
+
method: 'text',
|
|
179
|
+
expected_signal: 'text',
|
|
180
|
+
},
|
|
181
|
+
{ requiredField: 'check', idPrefix: 'c' },
|
|
182
|
+
),
|
|
183
|
+
remaining_disagreements: mapList(
|
|
184
|
+
src.remaining_disagreements,
|
|
185
|
+
{
|
|
186
|
+
topic: 'text',
|
|
187
|
+
your_position: 'text',
|
|
188
|
+
why_unresolved: 'text',
|
|
189
|
+
},
|
|
190
|
+
{ requiredField: 'topic' },
|
|
191
|
+
),
|
|
141
192
|
references: (Array.isArray(src.references) ? src.references : [])
|
|
142
193
|
.slice(0, O.referencesMax)
|
|
143
|
-
.map((r) =>
|
|
144
|
-
|
|
145
|
-
|
|
194
|
+
.map((r) =>
|
|
195
|
+
r && typeof r === 'object'
|
|
196
|
+
? { title: clampText(r.title, 300), url: clampText(r.url, 1000), relevance: clampText(r.relevance, 600) }
|
|
197
|
+
: { title: clampText(r, 300), url: null, relevance: null },
|
|
198
|
+
)
|
|
146
199
|
.filter((r) => r.title || r.url),
|
|
147
200
|
};
|
|
148
201
|
|
|
149
202
|
// The lists are cut to O.listMax before anything else looks at them; say how
|
|
150
203
|
// long the ones that were cut had been, so the cut is not silent.
|
|
151
|
-
const listTruncated = Object.fromEntries(
|
|
152
|
-
|
|
153
|
-
|
|
204
|
+
const listTruncated = Object.fromEntries(
|
|
205
|
+
['findings', 'alternatives', 'unknowns', 'decision_changers', 'next_checks', 'remaining_disagreements']
|
|
206
|
+
.filter((k) => Array.isArray(src[k]) && src[k].length > O.listMax)
|
|
207
|
+
.map((k) => [k, src[k].length]),
|
|
208
|
+
);
|
|
154
209
|
|
|
155
210
|
const missing = RESULT_KEYS.filter((k) => {
|
|
156
211
|
const v = result[k];
|
package/src/platform.mjs
CHANGED
|
@@ -36,13 +36,9 @@ export function findCommand(command, { cwd = process.cwd(), env = process.env }
|
|
|
36
36
|
if (typeof command !== 'string' || !command) return null;
|
|
37
37
|
cwd = path.resolve(cwd);
|
|
38
38
|
const windows = process.platform === 'win32';
|
|
39
|
-
const value = (name) => env[windows
|
|
40
|
-
? Object.keys(env).find((key) => key.toUpperCase() === name) : name];
|
|
39
|
+
const value = (name) => env[windows ? Object.keys(env).find((key) => key.toUpperCase() === name) : name];
|
|
41
40
|
const explicit = command.includes('/') || (windows && command.includes('\\'));
|
|
42
|
-
const dirs = explicit ? [cwd] : [
|
|
43
|
-
...(windows ? [cwd] : []),
|
|
44
|
-
...(value('PATH') ?? '').split(path.delimiter).filter(Boolean),
|
|
45
|
-
];
|
|
41
|
+
const dirs = explicit ? [cwd] : [...(windows ? [cwd] : []), ...(value('PATH') ?? '').split(path.delimiter).filter(Boolean)];
|
|
46
42
|
for (const dir of dirs) {
|
|
47
43
|
// Absolute candidates prevent which's own cwd lookup from resolving a
|
|
48
44
|
// wrapper in the server directory instead of the child's directory.
|
|
@@ -56,7 +52,9 @@ export function findCommand(command, { cwd = process.cwd(), env = process.env }
|
|
|
56
52
|
if (!fs.statSync(candidate).isFile()) return null;
|
|
57
53
|
fs.accessSync(candidate, fs.constants.X_OK);
|
|
58
54
|
return candidate;
|
|
59
|
-
} catch {
|
|
55
|
+
} catch {
|
|
56
|
+
/* not installed */
|
|
57
|
+
}
|
|
60
58
|
}
|
|
61
59
|
return null;
|
|
62
60
|
}
|
package/src/policy.mjs
CHANGED
|
@@ -7,7 +7,7 @@ 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,
|
|
10
|
+
import { buildConsultants, normalizeName } from './consultants.mjs';
|
|
11
11
|
|
|
12
12
|
// One file, read once at start, so an operator configures a machine in a
|
|
13
13
|
// single place instead of editing each client's MCP registration. Precedence
|
|
@@ -17,10 +17,10 @@ import { buildConsultants, CLI_IDS, normalizeName } from './consultants.mjs';
|
|
|
17
17
|
const CONFIG_HOME = process.env.SEVERALLY_HOME || path.join(os.homedir(), '.severally');
|
|
18
18
|
// .jsonc first, for operators whose editor treats comments in a .json file as
|
|
19
19
|
// an error; both are read the same way, comments and all.
|
|
20
|
-
const CONFIG_PATH =
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
const CONFIG_PATH =
|
|
21
|
+
process.env.SEVERALLY_CONFIG ||
|
|
22
|
+
[path.join(CONFIG_HOME, 'config.jsonc'), path.join(CONFIG_HOME, 'config.json')].find((p) => fs.existsSync(p)) ||
|
|
23
|
+
path.join(CONFIG_HOME, 'config.json');
|
|
24
24
|
|
|
25
25
|
let configError = null;
|
|
26
26
|
const CONFIG = (() => {
|
|
@@ -35,21 +35,53 @@ const CONFIG = (() => {
|
|
|
35
35
|
}
|
|
36
36
|
})();
|
|
37
37
|
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
// read why, but every consultation is
|
|
41
|
-
// quietly re-enable a CLI the
|
|
38
|
+
// A mistake that leaves the whole file untrustworthy (unreadable, an unknown
|
|
39
|
+
// top-level key, a section of the wrong shape) is not run on defaults: the
|
|
40
|
+
// server still starts, so the operator can read why, but every consultation is
|
|
41
|
+
// refused (jobs.mjs) -- falling back would quietly re-enable a CLI the
|
|
42
|
+
// operator turned off. A mistake inside one consultant or one CLI only takes
|
|
43
|
+
// that one out of use, with the reason.
|
|
42
44
|
const BUILT = buildConsultants(CONFIG);
|
|
43
|
-
if (!configError && BUILT.
|
|
45
|
+
if (!configError && BUILT.global.length) configError = `${CONFIG_PATH}: ${BUILT.global.join('; ')}`;
|
|
46
|
+
// A value that is neither, from the environment, cannot be read as either: reading it as "on" would open the web.
|
|
47
|
+
if (!configError && ![undefined, '', 'on', 'off'].includes(process.env.SEVERALLY_NETWORK)) {
|
|
48
|
+
configError = `SEVERALLY_NETWORK must be "on" or "off", not ${JSON.stringify(process.env.SEVERALLY_NETWORK)}`;
|
|
49
|
+
}
|
|
44
50
|
const FROM = configError ? buildConsultants({}) : BUILT;
|
|
51
|
+
const LOCAL = configError ? [] : BUILT.problems.filter((p) => !BUILT.global.includes(p));
|
|
45
52
|
|
|
46
|
-
/** What is wrong with the config file, or null. Reported at startup; while set, nothing can be started. */
|
|
53
|
+
/** What is wrong with the config file as a whole, or null. Reported at startup; while set, nothing can be started. */
|
|
47
54
|
export function configProblem() {
|
|
48
55
|
return configError;
|
|
49
56
|
}
|
|
50
57
|
|
|
58
|
+
/** The mistakes kept to one consultant or one CLI, each of which is out of use until fixed. */
|
|
59
|
+
export function configWarnings() {
|
|
60
|
+
return LOCAL.map((p) => `${CONFIG_PATH}: ${p}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The names of the consultants out of use because of a mistake in their own config section. */
|
|
64
|
+
export function brokenConsultantNames() {
|
|
65
|
+
return configError ? [] : Object.keys(BUILT.broken);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Why the consultant of this name is out of use because of a config mistake, or null. */
|
|
69
|
+
export function brokenConsultant(name) {
|
|
70
|
+
const p = !configError && Object.hasOwn(BUILT.broken, name) ? BUILT.broken[name] : null;
|
|
71
|
+
return p ? `the config file has a mistake in it: ${p.join('; ')}` : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
51
74
|
const usable = configError ? {} : CONFIG;
|
|
52
|
-
|
|
75
|
+
// A CLI whose own section has a mistake is off, whatever else the section says.
|
|
76
|
+
const cfgCli = (id) => {
|
|
77
|
+
if (Object.hasOwn(BUILT.brokenClis, id) && !configError) {
|
|
78
|
+
return {
|
|
79
|
+
enabled: false,
|
|
80
|
+
note: `turned off: the config file has a mistake in clis.${id}: ${BUILT.brokenClis[id].join('; ')}`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return usable.clis && typeof usable.clis === 'object' ? (usable.clis[id] ?? {}) : {};
|
|
84
|
+
};
|
|
53
85
|
const cfgServer = usable.server && typeof usable.server === 'object' ? usable.server : {};
|
|
54
86
|
|
|
55
87
|
/**
|
|
@@ -88,12 +120,23 @@ const str = (name, dflt) => {
|
|
|
88
120
|
// The CLI a lead runs in, by any everyday name. Only `caller` uses this:
|
|
89
121
|
// which consultant a name reaches is consultants.mjs's resolveConsultant.
|
|
90
122
|
// Object.create(null) so 'constructor' is an unknown name, not a function.
|
|
91
|
-
export const CALLER_ALIASES = Object.freeze(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
123
|
+
export const CALLER_ALIASES = Object.freeze(
|
|
124
|
+
Object.assign(Object.create(null), {
|
|
125
|
+
codex: 'codex',
|
|
126
|
+
gpt: 'codex',
|
|
127
|
+
chatgpt: 'codex',
|
|
128
|
+
openai: 'codex',
|
|
129
|
+
'claude-code': 'claude-code',
|
|
130
|
+
claude: 'claude-code',
|
|
131
|
+
anthropic: 'claude-code',
|
|
132
|
+
antigravity: 'antigravity',
|
|
133
|
+
agy: 'antigravity',
|
|
134
|
+
gemini: 'antigravity',
|
|
135
|
+
google: 'antigravity',
|
|
136
|
+
opencode: 'opencode',
|
|
137
|
+
glm: 'opencode',
|
|
138
|
+
}),
|
|
139
|
+
);
|
|
97
140
|
|
|
98
141
|
export const CALLER_INPUTS = Object.freeze(Object.keys(CALLER_ALIASES));
|
|
99
142
|
|
|
@@ -176,18 +219,34 @@ export const POLICY = Object.freeze({
|
|
|
176
219
|
// a consultant's business.
|
|
177
220
|
clis: Object.freeze({
|
|
178
221
|
codex: Object.freeze({
|
|
179
|
-
cli: CODEX_BIN,
|
|
222
|
+
cli: CODEX_BIN,
|
|
223
|
+
available: isEnabled('codex', CODEX_BIN),
|
|
224
|
+
note: disabledNote('codex'),
|
|
225
|
+
label: 'Codex CLI',
|
|
226
|
+
...extras('codex'),
|
|
180
227
|
}),
|
|
181
228
|
'claude-code': Object.freeze({
|
|
182
|
-
cli: CLAUDE_BIN,
|
|
229
|
+
cli: CLAUDE_BIN,
|
|
230
|
+
available: isEnabled('claude-code', CLAUDE_BIN),
|
|
231
|
+
note: disabledNote('claude-code'),
|
|
232
|
+
label: 'Claude Code CLI',
|
|
233
|
+
...extras('claude-code'),
|
|
183
234
|
}),
|
|
184
235
|
// Where the real Antigravity credentials live is credentialsHome() below,
|
|
185
236
|
// not a field here: the sandbox reads it per job (see timeoutMs()).
|
|
186
237
|
antigravity: Object.freeze({
|
|
187
|
-
cli: AGY_BIN,
|
|
238
|
+
cli: AGY_BIN,
|
|
239
|
+
available: isEnabled('antigravity', AGY_BIN),
|
|
240
|
+
note: disabledNote('antigravity'),
|
|
241
|
+
label: 'Antigravity CLI',
|
|
242
|
+
...extras('antigravity'),
|
|
188
243
|
}),
|
|
189
244
|
opencode: Object.freeze({
|
|
190
|
-
cli: OPENCODE_BIN,
|
|
245
|
+
cli: OPENCODE_BIN,
|
|
246
|
+
available: isEnabled('opencode', OPENCODE_BIN),
|
|
247
|
+
note: disabledNote('opencode'),
|
|
248
|
+
label: 'OpenCode CLI',
|
|
249
|
+
...extras('opencode'),
|
|
191
250
|
// opencode.json `provider` definitions copied into the sandbox config:
|
|
192
251
|
// the only way to reach a provider the sandbox's empty HOME would not
|
|
193
252
|
// know, such as a local OpenAI-compatible server.
|
|
@@ -218,6 +277,10 @@ export const POLICY = Object.freeze({
|
|
|
218
277
|
// clients allow a tool call, so a long wait does not blow up as a client-side
|
|
219
278
|
// timeout while the consultation is still healthy; a client registration
|
|
220
279
|
// that is known to wait longer raises it (scripts/install.mjs).
|
|
280
|
+
// "off" takes the web away from every consultation, whatever the request
|
|
281
|
+
// asks; "on" leaves it to each request's own network.
|
|
282
|
+
// Either source can close it; neither can reopen what the other closed.
|
|
283
|
+
network: [process.env.SEVERALLY_NETWORK, cfgServer.network].includes('off') ? 'off' : 'on',
|
|
221
284
|
maxWaitMs: limit('SEVERALLY_MAX_WAIT_MS', cfgServer.max_wait_ms, 45_000, 0, 600_000),
|
|
222
285
|
|
|
223
286
|
input: Object.freeze({
|
|
@@ -305,18 +368,53 @@ export function opencodeCredentialsHome() {
|
|
|
305
368
|
// and never copied by expose_paths; Codex's read-only sandbox cannot express
|
|
306
369
|
// path rules, which the boundary record says.
|
|
307
370
|
export const PROTECTED_HOME_PATHS = Object.freeze([
|
|
308
|
-
'.ssh',
|
|
309
|
-
'.
|
|
310
|
-
'.
|
|
311
|
-
'.
|
|
371
|
+
'.ssh',
|
|
372
|
+
'.gnupg',
|
|
373
|
+
'.aws',
|
|
374
|
+
'.azure',
|
|
375
|
+
'.kube',
|
|
376
|
+
'.docker',
|
|
377
|
+
'.config/gcloud',
|
|
378
|
+
'.config/gh',
|
|
379
|
+
'.config/hub',
|
|
380
|
+
'.netrc',
|
|
381
|
+
'.npmrc',
|
|
382
|
+
'.pypirc',
|
|
383
|
+
'.git-credentials',
|
|
384
|
+
'.cargo/credentials',
|
|
385
|
+
'.cargo/credentials.toml',
|
|
386
|
+
'.terraform.d',
|
|
387
|
+
'.vault-token',
|
|
388
|
+
'Library/Keychains',
|
|
389
|
+
'.codex',
|
|
390
|
+
'.claude',
|
|
391
|
+
'.claude.json',
|
|
392
|
+
'.gemini',
|
|
393
|
+
'.config/opencode',
|
|
394
|
+
'.local/share/opencode',
|
|
312
395
|
]);
|
|
313
396
|
// Under POLICY.home, wherever the operator moved it.
|
|
314
|
-
const PROTECTED_SEVERALLY_PATHS = Object.freeze([
|
|
397
|
+
const PROTECTED_SEVERALLY_PATHS = Object.freeze([
|
|
398
|
+
'history',
|
|
399
|
+
'backups',
|
|
400
|
+
'config.json',
|
|
401
|
+
'config.jsonc',
|
|
402
|
+
'homes',
|
|
315
403
|
// Where homes lived before 1.7: one left by an interrupted run can still hold an auth.json copy.
|
|
316
|
-
'jobs/*/home'
|
|
404
|
+
'jobs/*/home',
|
|
405
|
+
]);
|
|
317
406
|
// File names protected wherever they are.
|
|
318
407
|
export const PROTECTED_FILE_NAMES = Object.freeze([
|
|
319
|
-
'.env',
|
|
408
|
+
'.env',
|
|
409
|
+
'.env.*',
|
|
410
|
+
'.npmrc',
|
|
411
|
+
'.netrc',
|
|
412
|
+
'.pypirc',
|
|
413
|
+
'.git-credentials',
|
|
414
|
+
'id_rsa',
|
|
415
|
+
'id_ed25519',
|
|
416
|
+
'id_ecdsa',
|
|
417
|
+
'id_dsa',
|
|
320
418
|
]);
|
|
321
419
|
|
|
322
420
|
/** Absolute protected roots, with forward slashes; `*` stands for one path segment. */
|
|
@@ -333,8 +431,9 @@ const escapeRe = (s) => s.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
|
|
333
431
|
export const PATHS_CASE_INSENSITIVE = process.platform === 'win32' || process.platform === 'darwin';
|
|
334
432
|
/** Regex sources: one per protected root, matching it and anything under it. */
|
|
335
433
|
export function protectedRootPatterns(extraRoots = []) {
|
|
336
|
-
return [...protectedRoots(), ...extraRoots.map((r) => r.replaceAll('\\', '/'))]
|
|
337
|
-
|
|
434
|
+
return [...protectedRoots(), ...extraRoots.map((r) => r.replaceAll('\\', '/'))].map(
|
|
435
|
+
(r) => `^${escapeRe(r.replace(/\/+$/, '')).replaceAll('*', '[^/]+')}(/|$)`,
|
|
436
|
+
);
|
|
338
437
|
}
|
|
339
438
|
/** Regex sources for PROTECTED_FILE_NAMES, matched against a base name. */
|
|
340
439
|
export function protectedNamePatterns() {
|
|
@@ -347,7 +446,11 @@ export function isProtectedPath(abs) {
|
|
|
347
446
|
const roots = protectedRootPatterns().map((p) => new RegExp(p, flags));
|
|
348
447
|
const names = protectedNamePatterns().map((p) => new RegExp(p, flags));
|
|
349
448
|
let real = abs;
|
|
350
|
-
try {
|
|
449
|
+
try {
|
|
450
|
+
real = fs.realpathSync.native(abs);
|
|
451
|
+
} catch {
|
|
452
|
+
/* missing: check the name as given */
|
|
453
|
+
}
|
|
351
454
|
return [abs, real].some((p) => {
|
|
352
455
|
const fwd = p.replaceAll('\\', '/');
|
|
353
456
|
return roots.some((re) => re.test(fwd)) || names.some((re) => re.test(path.basename(fwd)));
|
|
@@ -389,11 +492,15 @@ export function limitsSummary() {
|
|
|
389
492
|
const by = (f) => Object.fromEntries(list.map((c) => [c.name, f(c)]));
|
|
390
493
|
return {
|
|
391
494
|
available_targets: list.map((c) => c.name),
|
|
495
|
+
// A consultant or CLI out of use because of a mistake in its own section.
|
|
496
|
+
...(LOCAL.length ? { config_mistakes: configWarnings() } : {}),
|
|
392
497
|
clis: by((c) => c.cli),
|
|
393
498
|
models: by((c) => c.model),
|
|
394
499
|
// Per consultant, because they no longer share one budget.
|
|
395
500
|
timeout_ms: by(timeoutMs),
|
|
396
501
|
max_rounds_per_chain: POLICY.maxRounds,
|
|
502
|
+
// "off": the operator has taken the web away from every consultation.
|
|
503
|
+
network: POLICY.network,
|
|
397
504
|
max_concurrent_jobs: POLICY.maxConcurrent,
|
|
398
505
|
max_wait_ms: POLICY.maxWaitMs,
|
|
399
506
|
input_char_budget: POLICY.input.totalCharsMax,
|
package/src/redact.mjs
CHANGED
|
@@ -1,51 +1,75 @@
|
|
|
1
1
|
// Credential scrubbing. Applied on the way out (brief sent to the consultant),
|
|
2
2
|
// on the way in (consultant output), and to everything persisted to disk.
|
|
3
3
|
|
|
4
|
-
const CODE_WORDS = new Set([
|
|
4
|
+
const CODE_WORDS = new Set([
|
|
5
|
+
'true',
|
|
6
|
+
'false',
|
|
7
|
+
'null',
|
|
8
|
+
'none',
|
|
9
|
+
'undefined',
|
|
10
|
+
'string',
|
|
11
|
+
'str',
|
|
12
|
+
'number',
|
|
13
|
+
'boolean',
|
|
14
|
+
'bool',
|
|
15
|
+
'int',
|
|
16
|
+
'bytes',
|
|
17
|
+
]);
|
|
5
18
|
|
|
6
19
|
// A password-type value that is code, not a secret: a flag or a type, a
|
|
7
20
|
// variable reference (${X}, $X, process.env.X, os.environ[...]), and -- unless
|
|
8
21
|
// it is a long value under a full PASSWORD/PASSWD name, which stays masked
|
|
9
22
|
// whatever it looks like -- a dotted name, a call or a borrow (&str).
|
|
10
23
|
function looksLikeCode(name, value) {
|
|
11
|
-
if (
|
|
12
|
-
|
|
24
|
+
if (
|
|
25
|
+
CODE_WORDS.has(value.toLowerCase()) ||
|
|
26
|
+
value.startsWith('${') ||
|
|
27
|
+
/^\$[A-Z_][A-Z0-9_]*$/.test(value) ||
|
|
28
|
+
/^(?:process\.env\.|os\.environ\b)/.test(value)
|
|
29
|
+
)
|
|
30
|
+
return true;
|
|
13
31
|
if (value.length >= 8 && /^PASSW(?:OR)?D$/i.test(name.split('_').at(-1))) return false;
|
|
14
32
|
return /^[A-Za-z_][\w$]*(?:\.[A-Za-z_$][\w$]*)+$/.test(value) || /^[A-Za-z_][\w$.]*\(/.test(value) || value.startsWith('&');
|
|
15
33
|
}
|
|
16
34
|
|
|
35
|
+
// A replacement is a string or a function of the match.
|
|
36
|
+
/** @type {Array<[RegExp, any]>} */
|
|
17
37
|
const PATTERNS = [
|
|
18
38
|
// Provider API keys
|
|
19
|
-
[/\bsk-ant-[A-Za-z0-9_
|
|
20
|
-
[/\bsk-proj-[A-Za-z0-9_
|
|
21
|
-
[/\bsk-[A-Za-z0-9_
|
|
39
|
+
[/\bsk-ant-[A-Za-z0-9_-]{12,}/g, '[REDACTED:anthropic-key]'],
|
|
40
|
+
[/\bsk-proj-[A-Za-z0-9_-]{12,}/g, '[REDACTED:openai-key]'],
|
|
41
|
+
[/\bsk-[A-Za-z0-9_-]{20,}/g, '[REDACTED:api-key]'],
|
|
22
42
|
[/\bgh[pousr]_[A-Za-z0-9]{16,}/g, '[REDACTED:github-token]'],
|
|
23
43
|
[/\bgithub_pat_[A-Za-z0-9_]{20,}/g, '[REDACTED:github-token]'],
|
|
24
44
|
[/\bxox[baprs]-[A-Za-z0-9-]{10,}/g, '[REDACTED:slack-token]'],
|
|
25
45
|
[/https:\/\/hooks\.slack\.com\/services\/T[A-Za-z0-9]+\/B[A-Za-z0-9]+\/[A-Za-z0-9]+/g, '[REDACTED:slack-webhook]'],
|
|
26
46
|
[/\bAKIA[0-9A-Z]{16}\b/g, '[REDACTED:aws-access-key-id]'],
|
|
27
47
|
[/\bASIA[0-9A-Z]{16}\b/g, '[REDACTED:aws-access-key-id]'],
|
|
28
|
-
[/\bAIza[0-9A-Za-z_
|
|
48
|
+
[/\bAIza[0-9A-Za-z_-]{30,}/g, '[REDACTED:google-api-key]'],
|
|
29
49
|
// Stripe secret and restricted keys (pk_ is public, left alone), npm, GitLab, Hugging Face
|
|
30
50
|
[/\b[sr]k_(?:live|test)_[A-Za-z0-9]{16,}/g, '[REDACTED:stripe-key]'],
|
|
31
51
|
[/\bnpm_[A-Za-z0-9]{30,}/g, '[REDACTED:npm-token]'],
|
|
32
|
-
[/\bglpat-[A-Za-z0-9_
|
|
52
|
+
[/\bglpat-[A-Za-z0-9_-]{20,}/g, '[REDACTED:gitlab-token]'],
|
|
33
53
|
[/\bhf_[A-Za-z0-9]{30,}/g, '[REDACTED:huggingface-token]'],
|
|
34
54
|
// JWT / OAuth access tokens (3 base64url segments)
|
|
35
|
-
[/\beyJ[A-Za-z0-9_
|
|
36
|
-
[/\bBearer\s+[A-Za-z0-9._
|
|
55
|
+
[/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g, '[REDACTED:jwt]'],
|
|
56
|
+
[/\bBearer\s+[A-Za-z0-9._-]{20,}/gi, 'Bearer [REDACTED:token]'],
|
|
37
57
|
// PEM private keys
|
|
38
58
|
[/-----BEGIN[^-]{0,40}PRIVATE KEY-----[\s\S]*?-----END[^-]{0,40}PRIVATE KEY-----/g, '[REDACTED:private-key]'],
|
|
39
59
|
// The password in a URL's userinfo, also with an empty user (redis://:pass@host)
|
|
40
60
|
[/(:\/\/[^\s:/@]*:)[^\s@/]+(@)/g, '$1[REDACTED]$2'],
|
|
41
61
|
// KEY=value / "token": "value" style assignments
|
|
42
|
-
[
|
|
43
|
-
(
|
|
62
|
+
[
|
|
63
|
+
/\b((?:[A-Za-z0-9_]*_)?(?:API_KEY|APIKEY|ACCESS_KEY|PRIVATE_KEY|ACCESS_TOKEN|REFRESH_TOKEN|ID_TOKEN|SECRET_KEY|CLIENT_SECRET|AUTH_TOKEN|CREDENTIALS|ACCOUNTKEY|SHAREDACCESSKEY|SECRET|TOKEN))(["']?\s*[:=]\s*["']?)([^\s"',;)]{8,})/gi,
|
|
64
|
+
(_m, k, sep, _v) => `${k}${sep}[REDACTED]`,
|
|
65
|
+
],
|
|
44
66
|
// Password-type names, with values from 4 characters; PASS and PWD too (PWD
|
|
45
67
|
// only after `_` or a letter: bare PWD is the working directory). What reads
|
|
46
68
|
// as code rather than a password is left alone (looksLikeCode).
|
|
47
|
-
[
|
|
48
|
-
(
|
|
69
|
+
[
|
|
70
|
+
/\b((?:[A-Za-z0-9_]*_)?(?:PASSWORD|PASSWD|PASS|(?<=[A-Za-z_])PWD))(["']?\s*[:=]\s*["']?)([^\s"',;)]{4,})/gi,
|
|
71
|
+
(m, k, sep, v) => (looksLikeCode(k, v) ? m : `${k}${sep}[REDACTED]`),
|
|
72
|
+
],
|
|
49
73
|
// Any environment variable named *_KEY. Upper case only: in code, primary_key,
|
|
50
74
|
// sort_key and idempotency_key name things, not secrets.
|
|
51
75
|
[/\b([A-Z0-9_]+_KEY)(\s*=\s*["']?)([^\s"',;)]{8,})/g, (_m, k, sep, _v) => `${k}${sep}[REDACTED]`],
|