@heretyc/subagent-mcp 2.12.8 → 2.12.10
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/dist/concurrency.js +226 -30
- package/dist/config-scaffold.js +1 -1
- package/dist/drivers.js +52 -13
- package/dist/global-concurrency.jsonc +5 -1
- package/dist/global-subagent-mcp-config.jsonc +5 -1
- package/dist/hooks/orchestration-claude.js +6 -1
- package/dist/hooks/orchestration-codex.js +8 -16
- package/dist/index.js +65 -10
- package/dist/launch-prompt.js +13 -7
- package/dist/orchestration/atomic-write.js +31 -0
- package/dist/orchestration/hook-core.js +48 -16
- package/dist/orchestration/liveness.js +3 -2
- package/dist/orchestration/marker.js +67 -38
- package/dist/orchestration/model-mode.js +3 -2
- package/dist/orchestration/pretool.js +2 -2
- package/dist/orchestration/reminder.js +3 -2
- package/dist/orchestration/update-check.js +6 -7
- package/dist/output-helpers.js +5 -1
- package/dist/permission-engine.js +31 -3
- package/dist/zombie.js +61 -4
- package/package.json +2 -2
package/dist/concurrency.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { homedir, platform } from "node:os";
|
|
3
|
+
import { homedir, platform, userInfo } from "node:os";
|
|
4
4
|
import { dirname, join, parse as parsePath, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { CONCURRENCY_SCAFFOLD } from "./config-scaffold.js";
|
|
@@ -11,6 +11,7 @@ export const DEFAULT_CHECK_FOR_UPDATES = true;
|
|
|
11
11
|
export const DEFAULT_PERMISSIONS_CEILING = "auto";
|
|
12
12
|
export const DEFAULT_ESCALATION = "irreversible-only";
|
|
13
13
|
export const DEFAULT_STRICT_READ_PARITY = "warn";
|
|
14
|
+
export const DEFAULT_SANDBOX_NETWORK = false;
|
|
14
15
|
export const CONFIG_FILENAME = "global-subagent-mcp-config.jsonc";
|
|
15
16
|
export const LEGACY_CONFIG_FILENAME = "global-concurrency.jsonc";
|
|
16
17
|
export { cullStaleSlots, ZOMBIE_FORCE_GRACE_MS, ZOMBIE_LIVE_IDLE_MS, ZOMBIE_TERMINAL_IDLE_MS, buildProcessTreeKillCommands, drainZombieIntents, drainZombieReports, parseSlotMetadata, readSlotMetadata, slotPathForAgent, writeSlotMetadata, };
|
|
@@ -83,6 +84,15 @@ export function parseStrictReadParityConfig(text) {
|
|
|
83
84
|
return DEFAULT_STRICT_READ_PARITY;
|
|
84
85
|
}
|
|
85
86
|
}
|
|
87
|
+
export function parseSandboxNetworkConfig(text) {
|
|
88
|
+
try {
|
|
89
|
+
const raw = parseJsonObject(text).sandboxNetwork;
|
|
90
|
+
return typeof raw === "boolean" ? raw : DEFAULT_SANDBOX_NETWORK;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return DEFAULT_SANDBOX_NETWORK;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
86
96
|
export function defaultConfigPath() {
|
|
87
97
|
return fileURLToPath(new URL("./" + CONFIG_FILENAME, import.meta.url));
|
|
88
98
|
}
|
|
@@ -118,6 +128,7 @@ export function readGlobalConfig(path = defaultConfigPath()) {
|
|
|
118
128
|
permissionsCeiling: parsePermissionsCeilingConfig(text),
|
|
119
129
|
escalation: parseEscalationConfig(text),
|
|
120
130
|
strictReadParity: parseStrictReadParityConfig(text),
|
|
131
|
+
sandboxNetwork: parseSandboxNetworkConfig(text),
|
|
121
132
|
path: resolved.path,
|
|
122
133
|
usedLegacyPath: resolved.usedLegacyPath,
|
|
123
134
|
parseFailure: null,
|
|
@@ -130,6 +141,7 @@ export function readGlobalConfig(path = defaultConfigPath()) {
|
|
|
130
141
|
permissionsCeiling: "manual",
|
|
131
142
|
escalation: DEFAULT_ESCALATION,
|
|
132
143
|
strictReadParity: DEFAULT_STRICT_READ_PARITY,
|
|
144
|
+
sandboxNetwork: DEFAULT_SANDBOX_NETWORK,
|
|
133
145
|
path,
|
|
134
146
|
usedLegacyPath: false,
|
|
135
147
|
parseFailure: e instanceof Error ? e.message : String(e),
|
|
@@ -151,6 +163,9 @@ export function readEscalation(path = defaultConfigPath()) {
|
|
|
151
163
|
export function readStrictReadParity(path = defaultConfigPath()) {
|
|
152
164
|
return readGlobalConfig(path).strictReadParity;
|
|
153
165
|
}
|
|
166
|
+
export function readSandboxNetwork(path = defaultConfigPath()) {
|
|
167
|
+
return readGlobalConfig(path).sandboxNetwork;
|
|
168
|
+
}
|
|
154
169
|
let legacyConfigDeprecationPending = false;
|
|
155
170
|
export function noteLegacyConfigIfUsed(path = defaultConfigPath()) {
|
|
156
171
|
if (resolveGlobalConfigPath(path).usedLegacyPath)
|
|
@@ -208,7 +223,7 @@ function readClaudePermissions(source, path, userScoped) {
|
|
|
208
223
|
}
|
|
209
224
|
const disableBypass = userScoped && parsed.disableBypassPermissionsMode === "disable";
|
|
210
225
|
return {
|
|
211
|
-
rules: { allow, deny, ask, additionalDirectories },
|
|
226
|
+
rules: { allow, deny, ask, additionalDirectories, sandboxNetwork: false },
|
|
212
227
|
disableBypass,
|
|
213
228
|
failure: null,
|
|
214
229
|
};
|
|
@@ -227,6 +242,7 @@ function blanketMutatingAskRules() {
|
|
|
227
242
|
deny: [],
|
|
228
243
|
ask: ["Bash", "Edit", "Write", "NotebookEdit", "MultiEdit"],
|
|
229
244
|
additionalDirectories: [],
|
|
245
|
+
sandboxNetwork: false,
|
|
230
246
|
};
|
|
231
247
|
}
|
|
232
248
|
function mergeRules(target, next) {
|
|
@@ -234,57 +250,204 @@ function mergeRules(target, next) {
|
|
|
234
250
|
target.deny.push(...next.deny);
|
|
235
251
|
target.ask.push(...next.ask);
|
|
236
252
|
target.additionalDirectories.push(...next.additionalDirectories);
|
|
253
|
+
target.sandboxNetwork ||= next.sandboxNetwork;
|
|
237
254
|
}
|
|
238
255
|
function unique(items) {
|
|
239
256
|
return [...new Set(items)];
|
|
240
257
|
}
|
|
258
|
+
function codexKeyPattern(key) {
|
|
259
|
+
return new RegExp(`^\\s*${key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*=\\s*(.*)$`);
|
|
260
|
+
}
|
|
261
|
+
function scanBasicString(line, start) {
|
|
262
|
+
for (let i = start + 1; i < line.length; i++) {
|
|
263
|
+
if (line[i] === "\\" && i + 1 < line.length) {
|
|
264
|
+
i++;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (line[i] === "\"")
|
|
268
|
+
return i;
|
|
269
|
+
}
|
|
270
|
+
throw new Error(`unbalanced TOML quotes: ${line.trim()}`);
|
|
271
|
+
}
|
|
272
|
+
function scanLiteralString(line, start) {
|
|
273
|
+
const end = line.indexOf("'", start + 1);
|
|
274
|
+
if (end === -1)
|
|
275
|
+
throw new Error(`unbalanced TOML quotes: ${line.trim()}`);
|
|
276
|
+
return end;
|
|
277
|
+
}
|
|
278
|
+
function stripTomlCommentsAndMultilineStrings(text) {
|
|
279
|
+
let multiline = null;
|
|
280
|
+
const out = [];
|
|
281
|
+
for (const line of text.split(/\r?\n/)) {
|
|
282
|
+
let sanitized = "";
|
|
283
|
+
let i = 0;
|
|
284
|
+
if (multiline) {
|
|
285
|
+
const end = line.indexOf(multiline);
|
|
286
|
+
if (end === -1) {
|
|
287
|
+
out.push("");
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
i = end + 3;
|
|
291
|
+
multiline = null;
|
|
292
|
+
}
|
|
293
|
+
while (i < line.length) {
|
|
294
|
+
if (line.startsWith(`"""`, i) || line.startsWith("'''", i)) {
|
|
295
|
+
const delimiter = line.startsWith(`"""`, i) ? `"""` : "'''";
|
|
296
|
+
sanitized += delimiter === `"""` ? `""` : "''";
|
|
297
|
+
i += 3;
|
|
298
|
+
const end = line.indexOf(delimiter, i);
|
|
299
|
+
if (end === -1) {
|
|
300
|
+
multiline = delimiter;
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
i = end + 3;
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (line[i] === "#")
|
|
307
|
+
break;
|
|
308
|
+
if (line[i] === "\"") {
|
|
309
|
+
const end = scanBasicString(line, i);
|
|
310
|
+
sanitized += line.slice(i, end + 1);
|
|
311
|
+
i = end + 1;
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (line[i] === "'") {
|
|
315
|
+
const end = scanLiteralString(line, i);
|
|
316
|
+
sanitized += line.slice(i, end + 1);
|
|
317
|
+
i = end + 1;
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
sanitized += line[i];
|
|
321
|
+
i++;
|
|
322
|
+
}
|
|
323
|
+
out.push(sanitized);
|
|
324
|
+
}
|
|
325
|
+
if (multiline)
|
|
326
|
+
throw new Error("unterminated TOML multiline string");
|
|
327
|
+
return out.join("\n");
|
|
328
|
+
}
|
|
329
|
+
function countTomlBrackets(line) {
|
|
330
|
+
let opens = 0;
|
|
331
|
+
let closes = 0;
|
|
332
|
+
for (let i = 0; i < line.length; i++) {
|
|
333
|
+
if (line[i] === "\"") {
|
|
334
|
+
i = scanBasicString(line, i);
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
if (line[i] === "'") {
|
|
338
|
+
i = scanLiteralString(line, i);
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
if (line[i] === "[")
|
|
342
|
+
opens++;
|
|
343
|
+
if (line[i] === "]")
|
|
344
|
+
closes++;
|
|
345
|
+
}
|
|
346
|
+
return { opens, closes };
|
|
347
|
+
}
|
|
348
|
+
function tomlAssignmentValue(text, key) {
|
|
349
|
+
const keyPattern = codexKeyPattern(key);
|
|
350
|
+
const lines = text.split(/\r?\n/);
|
|
351
|
+
for (let i = 0; i < lines.length; i++) {
|
|
352
|
+
const m = lines[i].match(keyPattern);
|
|
353
|
+
if (!m)
|
|
354
|
+
continue;
|
|
355
|
+
let value = m[1].trim();
|
|
356
|
+
if (!value.startsWith("["))
|
|
357
|
+
return value;
|
|
358
|
+
let balance = 0;
|
|
359
|
+
for (let j = i; j < lines.length; j++) {
|
|
360
|
+
const fragment = j === i ? value : lines[j];
|
|
361
|
+
const { opens, closes } = countTomlBrackets(fragment);
|
|
362
|
+
balance += opens - closes;
|
|
363
|
+
if (j > i)
|
|
364
|
+
value += `\n${fragment.trim()}`;
|
|
365
|
+
if (balance <= 0)
|
|
366
|
+
return value;
|
|
367
|
+
}
|
|
368
|
+
return value;
|
|
369
|
+
}
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
function parseTomlScalar(value) {
|
|
373
|
+
const trimmed = value.trim();
|
|
374
|
+
if (trimmed.startsWith("\"")) {
|
|
375
|
+
const end = scanBasicString(trimmed, 0);
|
|
376
|
+
return trimmed.slice(1, end);
|
|
377
|
+
}
|
|
378
|
+
if (trimmed.startsWith("'")) {
|
|
379
|
+
const end = scanLiteralString(trimmed, 0);
|
|
380
|
+
return trimmed.slice(1, end);
|
|
381
|
+
}
|
|
382
|
+
const bare = trimmed.match(/^[^\s,]+/);
|
|
383
|
+
return bare ? bare[0] : null;
|
|
384
|
+
}
|
|
241
385
|
function codexTomlValue(text, key) {
|
|
242
|
-
const
|
|
243
|
-
return
|
|
386
|
+
const value = tomlAssignmentValue(text, key);
|
|
387
|
+
return value === null ? null : parseTomlScalar(value);
|
|
244
388
|
}
|
|
245
389
|
function codexTomlArray(text, key) {
|
|
246
|
-
const
|
|
247
|
-
if (!
|
|
390
|
+
const value = tomlAssignmentValue(text, key);
|
|
391
|
+
if (!value || !value.trim().startsWith("["))
|
|
248
392
|
return [];
|
|
249
|
-
|
|
393
|
+
const out = [];
|
|
394
|
+
for (let i = 0; i < value.length; i++) {
|
|
395
|
+
if (value[i] === "\"") {
|
|
396
|
+
const end = scanBasicString(value, i);
|
|
397
|
+
out.push(value.slice(i + 1, end));
|
|
398
|
+
i = end;
|
|
399
|
+
}
|
|
400
|
+
else if (value[i] === "'") {
|
|
401
|
+
const end = scanLiteralString(value, i);
|
|
402
|
+
out.push(value.slice(i + 1, end));
|
|
403
|
+
i = end;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return out;
|
|
250
407
|
}
|
|
251
408
|
function assertBasicTomlParsable(text) {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
409
|
+
const sanitized = stripTomlCommentsAndMultilineStrings(text);
|
|
410
|
+
let arrayDepth = 0;
|
|
411
|
+
for (const line of sanitized.split(/\r?\n/)) {
|
|
412
|
+
const trimmed = line.trim();
|
|
255
413
|
if (!trimmed)
|
|
256
414
|
continue;
|
|
257
415
|
if (/^\[[^\]]+\]$/.test(trimmed))
|
|
258
416
|
continue;
|
|
259
|
-
if (!trimmed.includes("=") &&
|
|
417
|
+
if (!trimmed.includes("=") && arrayDepth === 0)
|
|
418
|
+
throw new Error(`malformed TOML line: ${trimmed}`);
|
|
419
|
+
const { opens, closes } = countTomlBrackets(trimmed);
|
|
420
|
+
arrayDepth += opens - closes;
|
|
421
|
+
if (arrayDepth < 0)
|
|
260
422
|
throw new Error(`malformed TOML line: ${trimmed}`);
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
throw new Error(`unbalanced TOML quotes: ${trimmed}`);
|
|
264
|
-
const opens = (trimmed.match(/\[/g) ?? []).length;
|
|
265
|
-
const closes = (trimmed.match(/\]/g) ?? []).length;
|
|
266
|
-
inArray = inArray || opens > closes;
|
|
267
|
-
if (closes > opens)
|
|
268
|
-
inArray = false;
|
|
269
|
-
}
|
|
270
|
-
if (inArray)
|
|
423
|
+
}
|
|
424
|
+
if (arrayDepth > 0)
|
|
271
425
|
throw new Error("unterminated TOML array");
|
|
426
|
+
return sanitized;
|
|
272
427
|
}
|
|
273
428
|
function readCodexConfig(path) {
|
|
274
429
|
try {
|
|
275
430
|
const text = readFileSync(path, "utf8");
|
|
276
|
-
assertBasicTomlParsable(text);
|
|
277
|
-
const rules = { allow: [], deny: [], ask: [], additionalDirectories: [] };
|
|
278
|
-
if (codexTomlValue(
|
|
431
|
+
const toml = assertBasicTomlParsable(text);
|
|
432
|
+
const rules = { allow: [], deny: [], ask: [], additionalDirectories: [], sandboxNetwork: false };
|
|
433
|
+
if (codexTomlValue(toml, "sandbox_mode") === "read-only") {
|
|
279
434
|
rules.deny.push("Edit", "Write", "NotebookEdit");
|
|
280
435
|
}
|
|
281
|
-
|
|
282
|
-
|
|
436
|
+
if (/\bsandbox_workspace_write\.network_access\s*=\s*true\b/i.test(toml) ||
|
|
437
|
+
/^\s*\[sandbox_workspace_write\][\s\S]*?^\s*network_access\s*=\s*true\b/im.test(toml)) {
|
|
438
|
+
rules.sandboxNetwork = true;
|
|
439
|
+
}
|
|
440
|
+
rules.additionalDirectories.push(...codexTomlArray(toml, "writable_roots"));
|
|
441
|
+
for (const m of toml.matchAll(/^\s*path\s*=\s*["']([^"']+)["']\s*\r?\n\s*access\s*=\s*["']deny["']/gm)) {
|
|
283
442
|
rules.deny.push(`Edit(${m[1]})`, `Write(${m[1]})`);
|
|
284
443
|
}
|
|
285
|
-
for (const m of
|
|
444
|
+
for (const m of toml.matchAll(/^\s*domain\s*=\s*["']([^"']+)["']\s*\r?\n\s*access\s*=\s*["']deny["']/gm)) {
|
|
286
445
|
rules.deny.push(`WebFetch(domain:${m[1]})`);
|
|
287
446
|
}
|
|
447
|
+
for (const m of toml.matchAll(/^\s*domain\s*=\s*["']([^"']+)["']\s*\r?\n\s*access\s*=\s*["']allow["']/gm)) {
|
|
448
|
+
rules.allow.push(`WebFetch(domain:${m[1]})`);
|
|
449
|
+
rules.sandboxNetwork = true;
|
|
450
|
+
}
|
|
288
451
|
return { rules, failure: null };
|
|
289
452
|
}
|
|
290
453
|
catch (e) {
|
|
@@ -327,7 +490,13 @@ const firstRepoDigests = new Map();
|
|
|
327
490
|
export function readMergedPermissionConfig(cwd, path = defaultConfigPath()) {
|
|
328
491
|
const global = readGlobalConfig(path);
|
|
329
492
|
noteLegacyConfigIfUsed(path);
|
|
330
|
-
const merged = {
|
|
493
|
+
const merged = {
|
|
494
|
+
allow: [],
|
|
495
|
+
deny: [],
|
|
496
|
+
ask: [],
|
|
497
|
+
additionalDirectories: [],
|
|
498
|
+
sandboxNetwork: global.sandboxNetwork,
|
|
499
|
+
};
|
|
331
500
|
const failures = [];
|
|
332
501
|
let ceiling = global.permissionsCeiling;
|
|
333
502
|
if (global.parseFailure) {
|
|
@@ -377,6 +546,7 @@ export function readMergedPermissionConfig(cwd, path = defaultConfigPath()) {
|
|
|
377
546
|
deny: unique(merged.deny),
|
|
378
547
|
ask: unique(merged.ask),
|
|
379
548
|
additionalDirectories: unique(merged.additionalDirectories).filter((p) => !protectedDirs.has(resolve(p))),
|
|
549
|
+
sandboxNetwork: merged.sandboxNetwork,
|
|
380
550
|
permissionsCeiling: ceiling,
|
|
381
551
|
escalation: global.escalation,
|
|
382
552
|
strictReadParity: global.strictReadParity,
|
|
@@ -385,7 +555,24 @@ export function readMergedPermissionConfig(cwd, path = defaultConfigPath()) {
|
|
|
385
555
|
selfProtectionDeny,
|
|
386
556
|
};
|
|
387
557
|
}
|
|
388
|
-
|
|
558
|
+
function safeSlotNamespace(raw) {
|
|
559
|
+
const cleaned = raw.replace(/[^A-Za-z0-9_.-]/g, "_");
|
|
560
|
+
return cleaned || createHash("sha256").update(raw || "unknown").digest("hex").slice(0, 16);
|
|
561
|
+
}
|
|
562
|
+
export function currentUserSlotNamespace() {
|
|
563
|
+
try {
|
|
564
|
+
const info = userInfo();
|
|
565
|
+
if (platform() !== "win32" && Number.isInteger(info.uid))
|
|
566
|
+
return `uid-${info.uid}`;
|
|
567
|
+
return safeSlotNamespace(info.username);
|
|
568
|
+
}
|
|
569
|
+
catch { }
|
|
570
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : null;
|
|
571
|
+
if (typeof uid === "number")
|
|
572
|
+
return `uid-${uid}`;
|
|
573
|
+
return safeSlotNamespace(process.env.USERNAME || process.env.USER || "unknown");
|
|
574
|
+
}
|
|
575
|
+
export function slotBaseDir() {
|
|
389
576
|
if (process.env.SUBAGENT_SLOT_DIR)
|
|
390
577
|
return process.env.SUBAGENT_SLOT_DIR;
|
|
391
578
|
if (platform() === "win32") {
|
|
@@ -393,6 +580,9 @@ export function slotDir() {
|
|
|
393
580
|
}
|
|
394
581
|
return "/tmp/subagent-mcp/slots";
|
|
395
582
|
}
|
|
583
|
+
export function slotDir() {
|
|
584
|
+
return join(slotBaseDir(), currentUserSlotNamespace());
|
|
585
|
+
}
|
|
396
586
|
export function countSlots(dir = slotDir()) {
|
|
397
587
|
try {
|
|
398
588
|
return readdirSync(dir).filter((f) => f.startsWith("slot-")).length;
|
|
@@ -410,7 +600,13 @@ export const NONBLOCKING_CULL_DEPS = {
|
|
|
410
600
|
};
|
|
411
601
|
export function reserveSlot(agentId, max, dir = slotDir(), cullDeps) {
|
|
412
602
|
try {
|
|
413
|
-
|
|
603
|
+
if (dir === slotDir()) {
|
|
604
|
+
mkdirSync(slotBaseDir(), { recursive: true, mode: 0o1777 });
|
|
605
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
606
|
+
}
|
|
607
|
+
else {
|
|
608
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
609
|
+
}
|
|
414
610
|
cullStaleSlots(dir, cullDeps);
|
|
415
611
|
const slotPath = slotPathForAgent(dir, agentId);
|
|
416
612
|
const before = countSlots(dir);
|
package/dist/config-scaffold.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// GENERATED by scripts/gen-ruleset-scaffold.mjs from src/global-subagent-mcp-config.jsonc — DO NOT EDIT.
|
|
2
|
-
export const CONCURRENCY_SCAFFOLD = "// subagent-mcp - Global Subagent MCP Config\n// ------------------------------------------------------------------\n// SOLE source of truth for machine-wide subagent-mcp defaults that must be\n// available beside the compiled server.\n//\n// globalConcurrentSubagents controls how many subagents may be ALIVE AT ONCE\n// across EVERY session, process, and user on this machine. There is NO\n// environment-variable override for the cap.\n//\n// RE-READ on every launch_agent call - edits take effect immediately, no\n// server restart required.\n//\n// Cap value rules:\n// - missing / unset / non-integer / 0 / negative -> reset to default 20\n// - 1 through 9 -> forced UP to minimum 10\n// - 10 or greater -> used as-is\n//\n// checkForUpdates controls the silent npmjs update check started when the MCP\n// server connects. Default true. Set to false to skip the registry fetch and\n// suppress hook notices. SUBAGENT_UPDATE_CHECK=0 or false also disables it.\n//\n// permissionsCeiling controls launch-time permissions posture:\n// - auto (default): shared engine gates unsafe/residue actions\n// - manual: human approval for residue while still auto-denying DANGER\n// - yolo: preserve the historical bypass/danger-full-access path, except\n// for non-bypassable config-file self-protection\n//\n// escalation applies only in auto mode. irreversible-only routes irreversible\n// NEUTRAL residue to a human; off leaves residue to orchestrator judgment.\n//\n// strictReadParity controls logging only. Unparseable Codex approval payloads\n// always fail closed to ask; warn logs the construct, off silences that log.\n{\n \"globalConcurrentSubagents\": 20,\n \"checkForUpdates\": true,\n \"permissionsCeiling\": \"auto\",\n \"escalation\": \"irreversible-only\",\n \"strictReadParity\": \"warn\"\n}\n";
|
|
2
|
+
export const CONCURRENCY_SCAFFOLD = "// subagent-mcp - Global Subagent MCP Config\n// ------------------------------------------------------------------\n// SOLE source of truth for machine-wide subagent-mcp defaults that must be\n// available beside the compiled server.\n//\n// globalConcurrentSubagents controls how many subagents may be ALIVE AT ONCE\n// across EVERY session, process, and user on this machine. There is NO\n// environment-variable override for the cap.\n//\n// RE-READ on every launch_agent call - edits take effect immediately, no\n// server restart required.\n//\n// Cap value rules:\n// - missing / unset / non-integer / 0 / negative -> reset to default 20\n// - 1 through 9 -> forced UP to minimum 10\n// - 10 or greater -> used as-is\n//\n// checkForUpdates controls the silent npmjs update check started when the MCP\n// server connects. Default true. Set to false to skip the registry fetch and\n// suppress hook notices. SUBAGENT_UPDATE_CHECK=0 or false also disables it.\n//\n// permissionsCeiling controls launch-time permissions posture:\n// - auto (default): shared engine gates unsafe/residue actions\n// - manual: human approval for residue while still auto-denying DANGER\n// - yolo: preserve the historical bypass/danger-full-access path, except\n// for non-bypassable config-file self-protection\n//\n// escalation applies only in auto mode. irreversible-only routes irreversible\n// NEUTRAL residue to a human; off leaves residue to orchestrator judgment.\n//\n// strictReadParity controls logging only. Unparseable Codex approval payloads\n// always fail closed to ask; warn logs the construct, off silences that log.\n//\n// sandboxNetwork controls Codex workspace-write network access. Default false.\n// Set true only when sub-agents need network after permission approval.\n{\n \"globalConcurrentSubagents\": 20,\n \"checkForUpdates\": true,\n \"permissionsCeiling\": \"auto\",\n \"escalation\": \"irreversible-only\",\n \"strictReadParity\": \"warn\",\n \"sandboxNetwork\": false\n}\n";
|
package/dist/drivers.js
CHANGED
|
@@ -217,9 +217,29 @@ function permissionSnapshotForLaunch(options) {
|
|
|
217
217
|
additionalDirectories: merged.additionalDirectories,
|
|
218
218
|
repoConfigChangedSinceFirstSeen: merged.repoConfigChangedSinceFirstSeen,
|
|
219
219
|
strictReadParity: merged.strictReadParity,
|
|
220
|
+
sandboxNetwork: merged.sandboxNetwork,
|
|
220
221
|
};
|
|
221
222
|
return snapshot;
|
|
222
223
|
}
|
|
224
|
+
export function providerChildSpawnOptions(options, p = process.platform) {
|
|
225
|
+
return {
|
|
226
|
+
cwd: options.cwd,
|
|
227
|
+
env: options.env,
|
|
228
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
229
|
+
windowsHide: true,
|
|
230
|
+
detached: p !== "win32",
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
export function killProviderChildProcess(child, signal, p = process.platform, killGroup = process.kill) {
|
|
234
|
+
if (p !== "win32" && typeof child.pid === "number") {
|
|
235
|
+
try {
|
|
236
|
+
killGroup(-child.pid, signal);
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
catch { }
|
|
240
|
+
}
|
|
241
|
+
return child.kill(signal);
|
|
242
|
+
}
|
|
223
243
|
function claudeToolName(request) {
|
|
224
244
|
for (const key of ["toolName", "tool_name", "name"]) {
|
|
225
245
|
const value = request[key];
|
|
@@ -273,7 +293,7 @@ function resolveOpPaths(paths, cwd) {
|
|
|
273
293
|
}
|
|
274
294
|
});
|
|
275
295
|
}
|
|
276
|
-
function permissionOpFromClaudeRequest(request, cwd) {
|
|
296
|
+
function permissionOpFromClaudeRequest(request, cwd, additionalDirectories) {
|
|
277
297
|
const input = claudeToolInput(request);
|
|
278
298
|
const command = stringField(input, ["command", "cmd"]);
|
|
279
299
|
const url = stringField(input, ["url"]);
|
|
@@ -292,6 +312,7 @@ function permissionOpFromClaudeRequest(request, cwd) {
|
|
|
292
312
|
...(paths.length > 0 ? { paths, resolvedPaths: resolveOpPaths(paths, cwd) } : {}),
|
|
293
313
|
...(url || host ? { network: [{ ...(url ? { url } : {}), ...(host ? { host } : {}) }] } : {}),
|
|
294
314
|
cwd,
|
|
315
|
+
...(additionalDirectories ? { additionalDirectories } : {}),
|
|
295
316
|
irreversible: Boolean(input.irreversible),
|
|
296
317
|
};
|
|
297
318
|
}
|
|
@@ -316,21 +337,38 @@ function isBypassImmuneClaudeAsk(request, op) {
|
|
|
316
337
|
function permissionDecisionToClaude(decision, reason) {
|
|
317
338
|
return decision === "allow" ? allowClaudePermission() : denyClaudePermission(reason);
|
|
318
339
|
}
|
|
319
|
-
function
|
|
340
|
+
function networkAllowRuleTriggersSandbox(rule) {
|
|
341
|
+
const trimmed = rule.trim();
|
|
342
|
+
if (/^WebFetch(?:\b|\()/i.test(trimmed))
|
|
343
|
+
return true;
|
|
344
|
+
if (/^Bash(?:\(\s*(?:\*|[\w./\\-]*\s*)?\)|$)/i.test(trimmed))
|
|
345
|
+
return true;
|
|
346
|
+
return /^Bash\([^)]*\b(?:git\s+(?:push|fetch|pull|clone|ls-remote)|gh|npm|pnpm|yarn|curl|wget)\b/i.test(trimmed);
|
|
347
|
+
}
|
|
348
|
+
export function resolveCodexLaunchValues(snapshot, logTranslation = true) {
|
|
320
349
|
if (snapshot.ceiling === "yolo") {
|
|
321
350
|
return {
|
|
322
351
|
approvalPolicy: "never",
|
|
323
352
|
threadSandbox: "danger-full-access",
|
|
324
353
|
turnSandboxPolicy: { type: "dangerFullAccess" },
|
|
354
|
+
cliConfigArgs: [],
|
|
325
355
|
};
|
|
326
356
|
}
|
|
357
|
+
const driverSnapshot = snapshot;
|
|
358
|
+
const translatedNetworkAllow = (snapshot.rules.allow ?? []).some(networkAllowRuleTriggersSandbox);
|
|
359
|
+
const networkAccess = Boolean(driverSnapshot.sandboxNetwork || translatedNetworkAllow);
|
|
360
|
+
if (logTranslation && translatedNetworkAllow && !driverSnapshot.sandboxNetwork) {
|
|
361
|
+
console.error("[permissions] translated network allow rule(s) to Codex workspace-write network access");
|
|
362
|
+
}
|
|
327
363
|
return {
|
|
328
364
|
approvalPolicy: "untrusted",
|
|
329
365
|
threadSandbox: "workspace-write",
|
|
330
366
|
turnSandboxPolicy: {
|
|
331
367
|
type: "workspaceWrite",
|
|
332
368
|
writableRoots: snapshot.additionalDirectories ?? [],
|
|
369
|
+
...(networkAccess ? { networkAccess: true } : {}),
|
|
333
370
|
},
|
|
371
|
+
cliConfigArgs: networkAccess ? ["-c", "sandbox_workspace_write.network_access=true"] : [],
|
|
334
372
|
};
|
|
335
373
|
}
|
|
336
374
|
function isCodexApprovalMethod(method) {
|
|
@@ -380,7 +418,7 @@ function codexCommandFromParams(params) {
|
|
|
380
418
|
return { command: argv.join(" "), argv };
|
|
381
419
|
return {};
|
|
382
420
|
}
|
|
383
|
-
export function codexApprovalOp(method, params, cwd) {
|
|
421
|
+
export function codexApprovalOp(method, params, cwd, additionalDirectories) {
|
|
384
422
|
if (method === "item/commandExecution/requestApproval" || method === "execCommandApproval") {
|
|
385
423
|
const command = codexCommandFromParams(params);
|
|
386
424
|
const opCwd = typeof params.cwd === "string" && params.cwd.length > 0 ? params.cwd : cwd;
|
|
@@ -389,6 +427,7 @@ export function codexApprovalOp(method, params, cwd) {
|
|
|
389
427
|
tool: "Bash",
|
|
390
428
|
...command,
|
|
391
429
|
cwd: opCwd,
|
|
430
|
+
...(additionalDirectories ? { additionalDirectories } : {}),
|
|
392
431
|
irreversible: Boolean(params.irreversible),
|
|
393
432
|
},
|
|
394
433
|
confidence: Boolean(command.command),
|
|
@@ -404,6 +443,7 @@ export function codexApprovalOp(method, params, cwd) {
|
|
|
404
443
|
tool: codexFileChangeTool(params.fileChanges),
|
|
405
444
|
...(paths.length > 0 ? { paths, resolvedPaths: resolveOpPaths(paths, cwd) } : {}),
|
|
406
445
|
cwd,
|
|
446
|
+
...(additionalDirectories ? { additionalDirectories } : {}),
|
|
407
447
|
irreversible: false,
|
|
408
448
|
},
|
|
409
449
|
confidence: paths.length > 0 || method === "item/fileChange/requestApproval",
|
|
@@ -413,6 +453,7 @@ export function codexApprovalOp(method, params, cwd) {
|
|
|
413
453
|
op: {
|
|
414
454
|
tool: "mcpServer/elicitation",
|
|
415
455
|
cwd,
|
|
456
|
+
...(additionalDirectories ? { additionalDirectories } : {}),
|
|
416
457
|
irreversible: false,
|
|
417
458
|
},
|
|
418
459
|
confidence: false,
|
|
@@ -598,7 +639,7 @@ export class CodexAppServerDriver {
|
|
|
598
639
|
kill() {
|
|
599
640
|
this.process.killed = true;
|
|
600
641
|
this.child.stdin?.destroy();
|
|
601
|
-
this.child
|
|
642
|
+
killProviderChildProcess(this.child, "SIGKILL");
|
|
602
643
|
this.process.kill("SIGKILL");
|
|
603
644
|
this.rejectPending(new Error("codex app-server driver was killed"));
|
|
604
645
|
this.queuedTurns.length = 0;
|
|
@@ -732,7 +773,7 @@ export class CodexAppServerDriver {
|
|
|
732
773
|
return;
|
|
733
774
|
}
|
|
734
775
|
this.pendingApprovals.set(key, record);
|
|
735
|
-
const { op, confidence } = codexApprovalOp(method, params, this.options.cwd);
|
|
776
|
+
const { op, confidence } = codexApprovalOp(method, params, this.options.cwd, this.permissionSnapshot.additionalDirectories);
|
|
736
777
|
const strictReadParity = this.permissionSnapshot.strictReadParity ?? "warn";
|
|
737
778
|
if (!confidence && this.permissionSnapshot.ceiling !== "yolo" && strictReadParity === "warn") {
|
|
738
779
|
console.error(`[permissions] strictReadParity warn: Codex approval payload for ${method} could not be matched with full confidence; routing to ask`);
|
|
@@ -890,7 +931,7 @@ export class ClaudeSdkDriver {
|
|
|
890
931
|
// allow/deny; parks (early-returns to the parent, resolves on respond/timeout)
|
|
891
932
|
// when the engine verdict is "ask".
|
|
892
933
|
async gateRequest(request, options, permissionSnapshot, isYolo, harnessChannel) {
|
|
893
|
-
const op = permissionOpFromClaudeRequest(request, options.cwd);
|
|
934
|
+
const op = permissionOpFromClaudeRequest(request, options.cwd, permissionSnapshot.additionalDirectories);
|
|
894
935
|
const engineResult = verdict(op, permissionSnapshot.rules);
|
|
895
936
|
if (isYolo && isBypassImmuneClaudeAsk(request, op)) {
|
|
896
937
|
return denyClaudePermission("bypass-immune Claude safety prompt auto-denied under yolo");
|
|
@@ -1030,11 +1071,9 @@ export async function createProviderDriver(options) {
|
|
|
1030
1071
|
driver.open(options);
|
|
1031
1072
|
return driver;
|
|
1032
1073
|
}
|
|
1033
|
-
const
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
});
|
|
1039
|
-
return new CodexAppServerDriver(child, options);
|
|
1074
|
+
const permissionSnapshot = permissionSnapshotForLaunch(options);
|
|
1075
|
+
const launchValues = resolveCodexLaunchValues(permissionSnapshot, false);
|
|
1076
|
+
const driverOptions = options.permissionSnapshot ? options : { ...options, permissionSnapshot };
|
|
1077
|
+
const child = spawn(options.command, [...launchValues.cliConfigArgs, ...options.args], providerChildSpawnOptions(options));
|
|
1078
|
+
return new CodexAppServerDriver(child, driverOptions);
|
|
1040
1079
|
}
|
|
@@ -30,10 +30,14 @@
|
|
|
30
30
|
//
|
|
31
31
|
// strictReadParity controls logging only. Unparseable Codex approval payloads
|
|
32
32
|
// always fail closed to ask; warn logs the construct, off silences that log.
|
|
33
|
+
//
|
|
34
|
+
// sandboxNetwork controls Codex workspace-write network access. Default false.
|
|
35
|
+
// Set true only when sub-agents need network after permission approval.
|
|
33
36
|
{
|
|
34
37
|
"globalConcurrentSubagents": 20,
|
|
35
38
|
"checkForUpdates": true,
|
|
36
39
|
"permissionsCeiling": "auto",
|
|
37
40
|
"escalation": "irreversible-only",
|
|
38
|
-
"strictReadParity": "warn"
|
|
41
|
+
"strictReadParity": "warn",
|
|
42
|
+
"sandboxNetwork": false
|
|
39
43
|
}
|
|
@@ -30,10 +30,14 @@
|
|
|
30
30
|
//
|
|
31
31
|
// strictReadParity controls logging only. Unparseable Codex approval payloads
|
|
32
32
|
// always fail closed to ask; warn logs the construct, off silences that log.
|
|
33
|
+
//
|
|
34
|
+
// sandboxNetwork controls Codex workspace-write network access. Default false.
|
|
35
|
+
// Set true only when sub-agents need network after permission approval.
|
|
33
36
|
{
|
|
34
37
|
"globalConcurrentSubagents": 20,
|
|
35
38
|
"checkForUpdates": true,
|
|
36
39
|
"permissionsCeiling": "auto",
|
|
37
40
|
"escalation": "irreversible-only",
|
|
38
|
-
"strictReadParity": "warn"
|
|
41
|
+
"strictReadParity": "warn",
|
|
42
|
+
"sandboxNetwork": false
|
|
39
43
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { realpathSync } from "node:fs";
|
|
2
2
|
import { pathToFileURL } from "node:url";
|
|
3
3
|
import { countJsonlType, runHook, } from "../orchestration/hook-core.js";
|
|
4
|
+
import { hasParentMarker } from "../launch-prompt.js";
|
|
4
5
|
/**
|
|
5
6
|
* Claude Code UserPromptSubmit hook entry. Reads the JSON payload from stdin,
|
|
6
7
|
* runs the provider-agnostic core with the Claude adapter, and writes the
|
|
@@ -31,7 +32,10 @@ export const claudeAdapter = {
|
|
|
31
32
|
return true;
|
|
32
33
|
}
|
|
33
34
|
const entrypoint = env.CLAUDE_CODE_ENTRYPOINT;
|
|
34
|
-
|
|
35
|
+
if (typeof entrypoint === "string" && SUBAGENT_ENTRYPOINTS.has(entrypoint)) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
return hasParentMarker(payload.prompt);
|
|
35
39
|
},
|
|
36
40
|
// Count JSONL lines in the transcript whose parsed object.type === 'user'.
|
|
37
41
|
// Delegates to the bounded counter (reads at most the trailing window so a
|
|
@@ -41,6 +45,7 @@ export const claudeAdapter = {
|
|
|
41
45
|
currentTurn(transcriptPath) {
|
|
42
46
|
return countJsonlType(transcriptPath, "user");
|
|
43
47
|
},
|
|
48
|
+
anonScope: "claude",
|
|
44
49
|
fullDirectiveFile: "orchestration-claude.md",
|
|
45
50
|
shortOnFile: "short-on.md",
|
|
46
51
|
shortOffFile: "short-off.md",
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { realpathSync } from "node:fs";
|
|
2
2
|
import { pathToFileURL } from "node:url";
|
|
3
|
-
import { claimAndEmit,
|
|
3
|
+
import { claimAndEmit, classifyOwnerClaim, countJsonlType, cullHookZombies, ownerKey, runHook, } from "../orchestration/hook-core.js";
|
|
4
4
|
import * as marker from "../orchestration/marker.js";
|
|
5
|
+
import { hasParentMarker } from "../launch-prompt.js";
|
|
5
6
|
/**
|
|
6
7
|
* Codex CLI hook entry. Branches on payload.hook_event_name:
|
|
7
8
|
* - 'SessionStart' -> if active and not a subagent, emit FULL + the ON
|
|
@@ -19,7 +20,6 @@ const SUBAGENT_SOURCE_STRINGS = new Set([
|
|
|
19
20
|
"subAgentThreadSpawn",
|
|
20
21
|
"subAgentOther",
|
|
21
22
|
]);
|
|
22
|
-
const PARENT_PROCESS_MARKER = "this is a request from a parent process";
|
|
23
23
|
export const codexAdapter = {
|
|
24
24
|
isSubagent(payload, env) {
|
|
25
25
|
// subagent-mcp-spawned children inherit this guard and must not claim/nag.
|
|
@@ -37,17 +37,7 @@ export const codexAdapter = {
|
|
|
37
37
|
if (typeof source === "string" && SUBAGENT_SOURCE_STRINGS.has(source)) {
|
|
38
38
|
return true;
|
|
39
39
|
}
|
|
40
|
-
|
|
41
|
-
// non-empty line (our own subagent contract starts with this sentinel).
|
|
42
|
-
const prompt = typeof payload.prompt === "string" ? payload.prompt : "";
|
|
43
|
-
const head = prompt.slice(0, 200);
|
|
44
|
-
for (const line of head.split("\n")) {
|
|
45
|
-
const trimmed = line.trim().toLowerCase();
|
|
46
|
-
if (!trimmed)
|
|
47
|
-
continue;
|
|
48
|
-
return trimmed.includes(PARENT_PROCESS_MARKER);
|
|
49
|
-
}
|
|
50
|
-
return false;
|
|
40
|
+
return hasParentMarker(payload.prompt);
|
|
51
41
|
},
|
|
52
42
|
// Count JSONL lines whose parsed object.type === 'turn_context'. Delegates to
|
|
53
43
|
// the bounded counter (reads at most the trailing window so a huge/
|
|
@@ -57,6 +47,7 @@ export const codexAdapter = {
|
|
|
57
47
|
currentTurn(transcriptPath) {
|
|
58
48
|
return countJsonlType(transcriptPath, "turn_context");
|
|
59
49
|
},
|
|
50
|
+
anonScope: "codex",
|
|
60
51
|
fullDirectiveFile: "orchestration-codex.md",
|
|
61
52
|
shortOnFile: "short-on.md",
|
|
62
53
|
shortOffFile: "short-off.md",
|
|
@@ -85,13 +76,14 @@ export function runCodexHook(payload, env, adapter = codexAdapter) {
|
|
|
85
76
|
return "";
|
|
86
77
|
}
|
|
87
78
|
const cwd = payload.cwd || process.cwd();
|
|
88
|
-
|
|
79
|
+
const current = ownerKey(payload, cwd, adapter);
|
|
80
|
+
marker.writeCurrentSession(cwd, current);
|
|
81
|
+
if (!marker.isActive(cwd, current)) {
|
|
89
82
|
return "";
|
|
90
83
|
}
|
|
91
|
-
const current = sessionKey(payload);
|
|
92
84
|
const turn = adapter.currentTurn(payload.transcript_path);
|
|
93
85
|
const m = marker.readMarker(cwd);
|
|
94
|
-
const kind =
|
|
86
|
+
const kind = classifyOwnerClaim(m, current);
|
|
95
87
|
// Claim/re-claim + emit via the SHARED claim path (one copy of the
|
|
96
88
|
// semantics — FULL + ON reminder, ack-latched CARRYOVER prepend, counter
|
|
97
89
|
// re-baseline). SessionStart claims even on SAME-SESSION (resume) so
|