@heretyc/subagent-mcp 2.12.4 → 2.12.5-beta.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/dist/drivers.js CHANGED
@@ -1,7 +1,12 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { EventEmitter } from "node:events";
3
+ import { realpathSync } from "node:fs";
4
+ import { resolve as resolvePath } from "node:path";
3
5
  import { PassThrough } from "node:stream";
6
+ import { readMergedPermissionConfig } from "./concurrency.js";
4
7
  import { mapModel } from "./effort.js";
8
+ import { applyPermissionCeiling, verdict, } from "./permission-engine.js";
9
+ import { requestPendingPermission } from "./pending-permissions.js";
5
10
  export class ProviderTransientError extends Error {
6
11
  isTransient = true;
7
12
  constructor(message) {
@@ -195,6 +200,233 @@ function optionId(opt) {
195
200
  function isJsonRpcId(id) {
196
201
  return typeof id === "string" || typeof id === "number";
197
202
  }
203
+ function denyClaudePermission(message) {
204
+ return { behavior: "deny", message };
205
+ }
206
+ function allowClaudePermission() {
207
+ return { behavior: "allow" };
208
+ }
209
+ function permissionSnapshotForLaunch(options) {
210
+ if (options.permissionSnapshot)
211
+ return options.permissionSnapshot;
212
+ const merged = readMergedPermissionConfig(options.cwd);
213
+ const snapshot = {
214
+ ceiling: merged.permissionsCeiling,
215
+ escalation: merged.escalation,
216
+ rules: { allow: merged.allow, deny: merged.deny, ask: merged.ask },
217
+ additionalDirectories: merged.additionalDirectories,
218
+ repoConfigChangedSinceFirstSeen: merged.repoConfigChangedSinceFirstSeen,
219
+ strictReadParity: merged.strictReadParity,
220
+ };
221
+ return snapshot;
222
+ }
223
+ function claudeToolName(request) {
224
+ for (const key of ["toolName", "tool_name", "name"]) {
225
+ const value = request[key];
226
+ if (typeof value === "string" && value.length > 0)
227
+ return value;
228
+ }
229
+ return "unknown";
230
+ }
231
+ function claudeToolInput(request) {
232
+ for (const key of ["input", "toolInput", "tool_input"]) {
233
+ const value = request[key];
234
+ if (value && typeof value === "object" && !Array.isArray(value))
235
+ return value;
236
+ }
237
+ return {};
238
+ }
239
+ function stringField(input, keys) {
240
+ for (const key of keys) {
241
+ const value = input[key];
242
+ if (typeof value === "string" && value.length > 0)
243
+ return value;
244
+ }
245
+ return undefined;
246
+ }
247
+ function stringArrayField(input, keys) {
248
+ const out = [];
249
+ for (const key of keys) {
250
+ const value = input[key];
251
+ if (typeof value === "string" && value.length > 0)
252
+ out.push(value);
253
+ else if (Array.isArray(value)) {
254
+ out.push(...value.filter((v) => typeof v === "string" && v.length > 0));
255
+ }
256
+ }
257
+ return out;
258
+ }
259
+ /**
260
+ * Adapter-layer symlink parity: the shared engine denies against the RESOLVED
261
+ * target (see PermissionOp.resolvedPaths), so every adapter must populate it.
262
+ * Resolve each path via realpath where it exists; pass the literal through
263
+ * otherwise. Fail-closed: a resolution error keeps the literal in the match set
264
+ * (never drops the path), it never turns a deny into a grant.
265
+ */
266
+ function resolveOpPaths(paths, cwd) {
267
+ return paths.map((p) => {
268
+ try {
269
+ return realpathSync(resolvePath(cwd, p));
270
+ }
271
+ catch {
272
+ return p;
273
+ }
274
+ });
275
+ }
276
+ function permissionOpFromClaudeRequest(request, cwd) {
277
+ const input = claudeToolInput(request);
278
+ const command = stringField(input, ["command", "cmd"]);
279
+ const url = stringField(input, ["url"]);
280
+ const host = stringField(input, ["host", "domain"]);
281
+ const paths = stringArrayField(input, [
282
+ "path",
283
+ "file_path",
284
+ "filePath",
285
+ "notebook_path",
286
+ "notebookPath",
287
+ "paths",
288
+ ]);
289
+ return {
290
+ tool: claudeToolName(request),
291
+ ...(command ? { command } : {}),
292
+ ...(paths.length > 0 ? { paths, resolvedPaths: resolveOpPaths(paths, cwd) } : {}),
293
+ ...(url || host ? { network: [{ ...(url ? { url } : {}), ...(host ? { host } : {}) }] } : {}),
294
+ cwd,
295
+ irreversible: Boolean(input.irreversible),
296
+ };
297
+ }
298
+ function claudeCorrelationId(request) {
299
+ for (const key of ["tool_use_id", "toolUseId", "id"]) {
300
+ const value = request[key];
301
+ if (typeof value === "string" || typeof value === "number")
302
+ return value;
303
+ }
304
+ return null;
305
+ }
306
+ function isBypassImmuneClaudeAsk(request, op) {
307
+ const input = claudeToolInput(request);
308
+ if (input.requiresUserInteraction === true || input.requires_user_interaction === true)
309
+ return true;
310
+ const command = op.command?.toLowerCase() ?? "";
311
+ if (/\b(shell|bash|zsh|fish|powershell|profile|rc)\b/.test(command) && /\b(edit|write|append|set)\b/.test(command)) {
312
+ return true;
313
+ }
314
+ return (op.paths ?? []).some((path) => /(^|[\\/])\.(git|claude|vscode)([\\/]|$)/i.test(path));
315
+ }
316
+ function permissionDecisionToClaude(decision, reason) {
317
+ return decision === "allow" ? allowClaudePermission() : denyClaudePermission(reason);
318
+ }
319
+ function resolveCodexLaunchValues(snapshot) {
320
+ if (snapshot.ceiling === "yolo") {
321
+ return {
322
+ approvalPolicy: "never",
323
+ threadSandbox: "danger-full-access",
324
+ turnSandboxPolicy: { type: "dangerFullAccess" },
325
+ };
326
+ }
327
+ return {
328
+ approvalPolicy: "untrusted",
329
+ threadSandbox: "workspace-write",
330
+ turnSandboxPolicy: {
331
+ type: "workspaceWrite",
332
+ writableRoots: snapshot.additionalDirectories ?? [],
333
+ },
334
+ };
335
+ }
336
+ function isCodexApprovalMethod(method) {
337
+ return (method === "item/commandExecution/requestApproval" ||
338
+ method === "item/fileChange/requestApproval" ||
339
+ method === "execCommandApproval" ||
340
+ method === "applyPatchApproval" ||
341
+ method === "mcpServer/elicitation/request");
342
+ }
343
+ function codexApprovalKey(id) {
344
+ return `${typeof id}:${id}`;
345
+ }
346
+ function codexParams(message) {
347
+ return message.params && typeof message.params === "object" && !Array.isArray(message.params)
348
+ ? message.params
349
+ : {};
350
+ }
351
+ function codexStringArray(value) {
352
+ return Array.isArray(value) ? value.filter((v) => typeof v === "string" && v.length > 0) : [];
353
+ }
354
+ function codexFileChangePaths(fileChanges) {
355
+ if (!fileChanges || typeof fileChanges !== "object" || Array.isArray(fileChanges))
356
+ return [];
357
+ return Object.keys(fileChanges).filter((p) => p.length > 0);
358
+ }
359
+ // Codex apply-patch op taxonomy: add/create -> Write, modify -> Edit. Mirrors
360
+ // how the Claude adapter and golden vectors distinguish creation from mutation
361
+ // so Write(...) rules round-trip identically through both adapters. Any create
362
+ // in the batch makes it a Write; a pure-modify batch is an Edit.
363
+ function codexFileChangeTool(fileChanges) {
364
+ if (!fileChanges || typeof fileChanges !== "object" || Array.isArray(fileChanges))
365
+ return "Edit";
366
+ const creates = /^(add|create)/i;
367
+ for (const change of Object.values(fileChanges)) {
368
+ const type = change && typeof change === "object" ? change.type : change;
369
+ if (typeof type === "string" && creates.test(type))
370
+ return "Write";
371
+ }
372
+ return "Edit";
373
+ }
374
+ function codexCommandFromParams(params) {
375
+ const command = params.command;
376
+ if (typeof command === "string" && command.length > 0)
377
+ return { command };
378
+ const argv = codexStringArray(command);
379
+ if (argv.length > 0)
380
+ return { command: argv.join(" "), argv };
381
+ return {};
382
+ }
383
+ export function codexApprovalOp(method, params, cwd) {
384
+ if (method === "item/commandExecution/requestApproval" || method === "execCommandApproval") {
385
+ const command = codexCommandFromParams(params);
386
+ const opCwd = typeof params.cwd === "string" && params.cwd.length > 0 ? params.cwd : cwd;
387
+ return {
388
+ op: {
389
+ tool: "Bash",
390
+ ...command,
391
+ cwd: opCwd,
392
+ irreversible: Boolean(params.irreversible),
393
+ },
394
+ confidence: Boolean(command.command),
395
+ };
396
+ }
397
+ if (method === "item/fileChange/requestApproval" || method === "applyPatchApproval") {
398
+ const paths = [
399
+ ...(typeof params.grantRoot === "string" && params.grantRoot.length > 0 ? [params.grantRoot] : []),
400
+ ...codexFileChangePaths(params.fileChanges),
401
+ ];
402
+ return {
403
+ op: {
404
+ tool: codexFileChangeTool(params.fileChanges),
405
+ ...(paths.length > 0 ? { paths, resolvedPaths: resolveOpPaths(paths, cwd) } : {}),
406
+ cwd,
407
+ irreversible: false,
408
+ },
409
+ confidence: paths.length > 0 || method === "item/fileChange/requestApproval",
410
+ };
411
+ }
412
+ return {
413
+ op: {
414
+ tool: "mcpServer/elicitation",
415
+ cwd,
416
+ irreversible: false,
417
+ },
418
+ confidence: false,
419
+ };
420
+ }
421
+ export function codexApprovalResult(method, decision) {
422
+ const allowed = decision === "allow";
423
+ if (method === "mcpServer/elicitation/request")
424
+ return { action: allowed ? "accept" : "decline" };
425
+ if (method === "execCommandApproval" || method === "applyPatchApproval") {
426
+ return { decision: allowed ? "approved" : "denied" };
427
+ }
428
+ return { decision: allowed ? "accept" : "decline" };
429
+ }
198
430
  export class MockJsonlDriver {
199
431
  child;
200
432
  provider;
@@ -293,7 +525,9 @@ export class CodexAppServerDriver {
293
525
  this._definitelyStartedReject = rej;
294
526
  });
295
527
  pending = new Map();
528
+ pendingApprovals = new Map();
296
529
  queuedTurns = [];
530
+ pendingDenyReasons = [];
297
531
  nextId = 1;
298
532
  stdoutBuf = "";
299
533
  threadId = null;
@@ -302,13 +536,14 @@ export class CodexAppServerDriver {
302
536
  initialized = false;
303
537
  turnInFlight = false;
304
538
  maxQueueDepth = 32;
305
- // An inbound server->client RPC (elicitation, e.g. requestUserInput / approval)
306
- // awaiting a reply. While set, the next send() answers this request instead of
307
- // enqueuing a new turn — otherwise the in-flight question turn wedges the queue.
308
- pendingServerRequest = null;
539
+ maxPendingApprovals = 16;
540
+ permissionSnapshot;
541
+ codexLaunchValues;
309
542
  constructor(child, options) {
310
543
  this.child = child;
311
544
  this.options = options;
545
+ this.permissionSnapshot = permissionSnapshotForLaunch(options);
546
+ this.codexLaunchValues = resolveCodexLaunchValues(this.permissionSnapshot);
312
547
  this.process = new LogicalProcess(child.pid);
313
548
  child.once("spawn", () => this.process.emit("spawn"));
314
549
  child.once("error", (err) => this.fail(err));
@@ -336,8 +571,8 @@ export class CodexAppServerDriver {
336
571
  const thread = await this.request("thread/start", {
337
572
  model: this.options.model,
338
573
  cwd: this.options.cwd,
339
- approvalPolicy: "never",
340
- sandbox: "danger-full-access",
574
+ approvalPolicy: this.codexLaunchValues.approvalPolicy,
575
+ sandbox: this.codexLaunchValues.threadSandbox,
341
576
  ephemeral: true,
342
577
  threadSource: "subagent",
343
578
  });
@@ -353,18 +588,6 @@ export class CodexAppServerDriver {
353
588
  if (!this.initialized || !this.threadId) {
354
589
  return Promise.reject(new Error("codex app-server thread is not initialized"));
355
590
  }
356
- if (this.pendingServerRequest) {
357
- // The message is the user's answer to a parked server-side elicitation.
358
- // Reply on the same JSON-RPC id (mirroring the { id, result } envelope our
359
- // own request() responses arrive in) instead of enqueuing a fresh turn.
360
- const req = this.pendingServerRequest;
361
- this.pendingServerRequest = null;
362
- return writeLine(this.child.stdin, {
363
- jsonrpc: "2.0",
364
- id: req.id,
365
- result: buildElicitationResult(req.options, message),
366
- });
367
- }
368
591
  if (this.queuedTurns.length >= this.maxQueueDepth) {
369
592
  return Promise.reject(new Error(`provider input queue is full (${this.maxQueueDepth})`));
370
593
  }
@@ -379,7 +602,7 @@ export class CodexAppServerDriver {
379
602
  this.process.kill("SIGKILL");
380
603
  this.rejectPending(new Error("codex app-server driver was killed"));
381
604
  this.queuedTurns.length = 0;
382
- this.pendingServerRequest = null;
605
+ this.pendingApprovals.clear();
383
606
  }
384
607
  async drainQueuedTurns() {
385
608
  if (this.drainActive)
@@ -407,15 +630,16 @@ export class CodexAppServerDriver {
407
630
  if (!this.threadId)
408
631
  throw new Error("codex app-server thread is not initialized");
409
632
  this.turnInFlight = true;
633
+ const inputText = this.consumeDenyNotice(message);
410
634
  try {
411
635
  const response = await this.request("turn/start", {
412
636
  threadId: this.threadId,
413
- input: [textInput(message)],
637
+ input: [textInput(inputText)],
414
638
  cwd: this.options.cwd,
415
639
  model: this.options.model,
416
640
  effort: this.options.effort,
417
- approvalPolicy: "never",
418
- sandboxPolicy: { type: "dangerFullAccess" },
641
+ approvalPolicy: this.codexLaunchValues.approvalPolicy,
642
+ sandboxPolicy: this.codexLaunchValues.turnSandboxPolicy,
419
643
  });
420
644
  const turn = response.result?.turn;
421
645
  this.activeTurnId = typeof turn?.id === "string" ? turn.id : this.activeTurnId;
@@ -440,6 +664,13 @@ export class CodexAppServerDriver {
440
664
  notify(method, params) {
441
665
  return writeLine(this.child.stdin, params === undefined ? { method } : { method, params });
442
666
  }
667
+ consumeDenyNotice(message) {
668
+ if (this.pendingDenyReasons.length === 0)
669
+ return message;
670
+ const reasons = this.pendingDenyReasons.splice(0);
671
+ const shown = [...new Set(reasons)].slice(0, 8).join("; ");
672
+ return `Permission DENIED for ${reasons.length} action(s) since your last message: ${shown}. Do not retry; adjust approach.\n\n${message}`;
673
+ }
443
674
  onStdout(chunk) {
444
675
  this.stdoutBuf += chunk;
445
676
  const lines = this.stdoutBuf.split("\n");
@@ -471,15 +702,15 @@ export class CodexAppServerDriver {
471
702
  pending.resolve(message);
472
703
  }
473
704
  }
705
+ else if (isJsonRpcId(message.id) &&
706
+ typeof message.method === "string" &&
707
+ isCodexApprovalMethod(message.method)) {
708
+ void this.handleCodexApproval(message.id, message.method, codexParams(message));
709
+ }
474
710
  else if (isJsonRpcId(message.id) &&
475
711
  typeof message.method === "string" &&
476
712
  isElicitationMethod(message.method)) {
477
- // Inbound server->client RPC (elicitation) that is NOT a reply to one of
478
- // our requests. Park it so the next send() answers it instead of queuing a
479
- // new turn; leaving it unanswered wedges the in-flight question turn.
480
- const params = (message.params ?? {});
481
- const options = Array.isArray(params.options) ? params.options : undefined;
482
- this.pendingServerRequest = { id: message.id, method: message.method, options };
713
+ void this.replyJsonRpc(message.id, buildElicitationResult(undefined, ""));
483
714
  }
484
715
  if (message.method === "turn/started" && message.params && typeof message.params === "object") {
485
716
  const turn = (message.params.turn ?? {});
@@ -493,6 +724,58 @@ export class CodexAppServerDriver {
493
724
  void this.drainQueuedTurns();
494
725
  }
495
726
  }
727
+ async handleCodexApproval(jsonRpcId, method, params) {
728
+ const key = codexApprovalKey(jsonRpcId);
729
+ const record = { jsonRpcId, method, params };
730
+ if (this.pendingApprovals.size >= this.maxPendingApprovals) {
731
+ await this.replyCodexApproval(record, "deny", "pending approval cap reached; auto-denied fail-closed");
732
+ return;
733
+ }
734
+ this.pendingApprovals.set(key, record);
735
+ const { op, confidence } = codexApprovalOp(method, params, this.options.cwd);
736
+ const strictReadParity = this.permissionSnapshot.strictReadParity ?? "warn";
737
+ if (!confidence && this.permissionSnapshot.ceiling !== "yolo" && strictReadParity === "warn") {
738
+ console.error(`[permissions] strictReadParity warn: Codex approval payload for ${method} could not be matched with full confidence; routing to ask`);
739
+ }
740
+ const engineResult = confidence
741
+ ? verdict(op, this.permissionSnapshot.rules)
742
+ : {
743
+ verdict: "ask",
744
+ classification: "neutral",
745
+ irreversible: Boolean(op.irreversible),
746
+ reason: "Codex approval payload could not be matched with full confidence",
747
+ };
748
+ const decision = applyPermissionCeiling(engineResult.verdict, this.permissionSnapshot.ceiling);
749
+ if (decision !== "ask") {
750
+ await this.replyCodexApproval(record, decision, engineResult.reason);
751
+ return;
752
+ }
753
+ const pendingDecision = await requestPendingPermission({
754
+ agentId: this.options.agentId,
755
+ harnessChannel: "codex-app-server",
756
+ toolNameOrMethod: method,
757
+ action: params,
758
+ permissionCeiling: this.permissionSnapshot.ceiling,
759
+ escalation: this.permissionSnapshot.escalation,
760
+ irreversible: engineResult.irreversible,
761
+ reason: engineResult.reason,
762
+ suggestions: [],
763
+ correlationId: jsonRpcId,
764
+ });
765
+ await this.replyCodexApproval(record, pendingDecision.verdict, pendingDecision.reason);
766
+ }
767
+ async replyCodexApproval(record, decision, reason) {
768
+ const key = codexApprovalKey(record.jsonRpcId);
769
+ if (!this.pendingApprovals.has(key))
770
+ return;
771
+ this.pendingApprovals.delete(key);
772
+ if (decision !== "allow")
773
+ this.pendingDenyReasons.push(`${record.method}: ${reason}`);
774
+ await this.replyJsonRpc(record.jsonRpcId, codexApprovalResult(record.method, decision));
775
+ }
776
+ replyJsonRpc(id, result) {
777
+ return writeLine(this.child.stdin, { jsonrpc: "2.0", id, result });
778
+ }
496
779
  fail(error) {
497
780
  const msg = error.message;
498
781
  const transient = /\b429\b|\b5\d{2}\b|quota|rate.?limit|timeout|ECONNRESET|ETIMEDOUT|ECONNREFUSED|too many requests|service unavailable|server error|overloaded/i.test(msg);
@@ -534,6 +817,8 @@ export class ClaudeSdkDriver {
534
817
  return this.closedFlag || this.process.killed;
535
818
  }
536
819
  open(options) {
820
+ const permissionSnapshot = permissionSnapshotForLaunch(options);
821
+ const isYolo = permissionSnapshot.ceiling === "yolo";
537
822
  const sdkOptions = {
538
823
  abortController: this.abortController,
539
824
  cwd: options.cwd,
@@ -542,9 +827,34 @@ export class ClaudeSdkDriver {
542
827
  // model_not_found (404); normalize to the full id the SDK accepts.
543
828
  model: mapModel(options.provider, options.model),
544
829
  pathToClaudeCodeExecutable: options.command,
545
- permissionMode: "bypassPermissions",
546
- allowDangerouslySkipPermissions: true,
830
+ permissionMode: isYolo ? "bypassPermissions" : "default",
831
+ allowDangerouslySkipPermissions: isYolo,
832
+ settingSources: [],
547
833
  tools: { type: "preset", preset: "claude_code" },
834
+ canUseTool: async (request) => {
835
+ const op = permissionOpFromClaudeRequest(request, options.cwd);
836
+ const engineResult = verdict(op, permissionSnapshot.rules);
837
+ if (isYolo && isBypassImmuneClaudeAsk(request, op)) {
838
+ return denyClaudePermission("bypass-immune Claude safety prompt auto-denied under yolo");
839
+ }
840
+ const decision = applyPermissionCeiling(engineResult.verdict, permissionSnapshot.ceiling);
841
+ if (decision !== "ask") {
842
+ return permissionDecisionToClaude(decision, engineResult.reason);
843
+ }
844
+ const pendingDecision = await requestPendingPermission({
845
+ agentId: options.agentId,
846
+ harnessChannel: "claude-canUseTool",
847
+ toolNameOrMethod: op.tool,
848
+ action: claudeToolInput(request),
849
+ permissionCeiling: permissionSnapshot.ceiling,
850
+ escalation: permissionSnapshot.escalation,
851
+ irreversible: engineResult.irreversible,
852
+ reason: engineResult.reason,
853
+ suggestions: [],
854
+ correlationId: claudeCorrelationId(request),
855
+ });
856
+ return permissionDecisionToClaude(pendingDecision.verdict, pendingDecision.reason);
857
+ },
548
858
  maxTurns: 50,
549
859
  includePartialMessages: true,
550
860
  };
package/dist/effort.js CHANGED
@@ -1,4 +1,4 @@
1
- import { writeFileSync } from "fs";
1
+ import { mkdirSync, writeFileSync } from "fs";
2
2
  import { join } from "path";
3
3
  import { tmpdir } from "os";
4
4
  import { randomUUID } from "crypto";
@@ -43,7 +43,7 @@ export function resolveEffort(provider, model, effort) {
43
43
  }
44
44
  return { kind: "flag", value: "high" };
45
45
  }
46
- export function buildCommand(provider, model, effort, cwd) {
46
+ export function buildCommand(provider, model, effort, cwd, agentId) {
47
47
  const mapped = mapModel(provider, model);
48
48
  const er = resolveEffort(provider, model, effort);
49
49
  if (provider === "claude") {
@@ -52,11 +52,16 @@ export function buildCommand(provider, model, effort, cwd) {
52
52
  args.push("--effort", er.value);
53
53
  }
54
54
  else if (er.kind === "settings") {
55
- const ucSettingsPath = join(tmpdir(), `subagent-uc-${randomUUID()}.json`);
56
- writeFileSync(ucSettingsPath, '{"ultracode":true}');
55
+ const safeAgentId = (agentId ?? randomUUID()).replace(/[^a-zA-Z0-9._-]/g, "_");
56
+ // J1-5: keep per-agent settings under the user's temp profile; POSIX modes
57
+ // restrict the scratch dir/file, while Windows applies fs defaults.
58
+ const ucSettingsDir = join(tmpdir(), "subagent-mcp", `perm-${safeAgentId}`);
59
+ mkdirSync(ucSettingsDir, { recursive: true, mode: 0o700 });
60
+ const ucSettingsPath = join(ucSettingsDir, "settings.json");
61
+ writeFileSync(ucSettingsPath, '{"ultracode":true}', { mode: 0o600 });
57
62
  args.push("--settings", ucSettingsPath);
58
63
  args.push("--permission-mode", "bypassPermissions", "--tools", "default", "--max-turns", "50");
59
- return { args, ucSettingsPath };
64
+ return { args, ucSettingsPath, ucSettingsDir };
60
65
  }
61
66
  args.push("--permission-mode", "bypassPermissions", "--tools", "default", "--max-turns", "50");
62
67
  return { args };
@@ -1,34 +1,39 @@
1
- // subagent-mcp Global Concurrent Subagent Cap
2
- // ------------------------------------------------------------------
3
- // SOLE source of truth for the machine-wide limit on how many subagents
4
- // may be ALIVE AT ONCE across EVERY session, process, and user on this
5
- // machine. There is NO environment-variable override for the cap.
6
- //
7
- // The whole recursive descendant tree counts toward this ONE number: a
8
- // subagent that itself launches subagents adds to the same machine-wide
9
- // total, and OTHER active agentic sessions count too.
10
- //
11
- // RE-READ on every launch_agent call — edits take effect immediately, no
12
- // server restart required.
13
- //
14
- // Value rules (forcibly applied to the number below):
15
- // - missing / unset / non-integer / 0 / negative -> reset to default 20
16
- // - 1 through 9 -> forced UP to minimum 10
17
- // - 10 or greater -> used as-is
18
- //
19
- // Zombie culling is always enabled. There is no config knob. Before cap
20
- // rejection, launch/tool/hook paths refresh live owned slots, preserve stale
21
- // slots whose owner server is still alive, and cull stale slots whose owner is
22
- // gone. Managed stale slots terminate the child process tree, then force-kill
23
- // after 20s when needed; unmanaged stale slots are only unlinked.
24
- //
25
- // When the cap is reached after culling, launch_agent is REJECTED (never
26
- // queued). Free a slot with list_agents + kill_agent, then retry.
27
- //
28
- // checkForUpdates controls the silent npmjs update check started when the MCP
29
- // server connects. Default true. Set to false to skip the registry fetch and
30
- // suppress hook notices. SUBAGENT_UPDATE_CHECK=0 or false also disables it.
31
- {
32
- "globalConcurrentSubagents": 20,
33
- "checkForUpdates": true
34
- }
1
+ // subagent-mcp - Global Subagent MCP Config
2
+ // ------------------------------------------------------------------
3
+ // SOLE source of truth for machine-wide subagent-mcp defaults that must be
4
+ // available beside the compiled server.
5
+ //
6
+ // globalConcurrentSubagents controls how many subagents may be ALIVE AT ONCE
7
+ // across EVERY session, process, and user on this machine. There is NO
8
+ // environment-variable override for the cap.
9
+ //
10
+ // RE-READ on every launch_agent call - edits take effect immediately, no
11
+ // server restart required.
12
+ //
13
+ // Cap value rules:
14
+ // - missing / unset / non-integer / 0 / negative -> reset to default 20
15
+ // - 1 through 9 -> forced UP to minimum 10
16
+ // - 10 or greater -> used as-is
17
+ //
18
+ // checkForUpdates controls the silent npmjs update check started when the MCP
19
+ // server connects. Default true. Set to false to skip the registry fetch and
20
+ // suppress hook notices. SUBAGENT_UPDATE_CHECK=0 or false also disables it.
21
+ //
22
+ // permissionsCeiling controls launch-time permissions posture:
23
+ // - auto (default): shared engine gates unsafe/residue actions
24
+ // - manual: human approval for residue while still auto-denying DANGER
25
+ // - yolo: preserve the historical bypass/danger-full-access path, except
26
+ // for non-bypassable config-file self-protection
27
+ //
28
+ // escalation applies only in auto mode. irreversible-only routes irreversible
29
+ // NEUTRAL residue to a human; off leaves residue to orchestrator judgment.
30
+ //
31
+ // strictReadParity controls logging only. Unparseable Codex approval payloads
32
+ // always fail closed to ask; warn logs the construct, off silences that log.
33
+ {
34
+ "globalConcurrentSubagents": 20,
35
+ "checkForUpdates": true,
36
+ "permissionsCeiling": "auto",
37
+ "escalation": "irreversible-only",
38
+ "strictReadParity": "warn"
39
+ }
@@ -0,0 +1,39 @@
1
+ // subagent-mcp - Global Subagent MCP Config
2
+ // ------------------------------------------------------------------
3
+ // SOLE source of truth for machine-wide subagent-mcp defaults that must be
4
+ // available beside the compiled server.
5
+ //
6
+ // globalConcurrentSubagents controls how many subagents may be ALIVE AT ONCE
7
+ // across EVERY session, process, and user on this machine. There is NO
8
+ // environment-variable override for the cap.
9
+ //
10
+ // RE-READ on every launch_agent call - edits take effect immediately, no
11
+ // server restart required.
12
+ //
13
+ // Cap value rules:
14
+ // - missing / unset / non-integer / 0 / negative -> reset to default 20
15
+ // - 1 through 9 -> forced UP to minimum 10
16
+ // - 10 or greater -> used as-is
17
+ //
18
+ // checkForUpdates controls the silent npmjs update check started when the MCP
19
+ // server connects. Default true. Set to false to skip the registry fetch and
20
+ // suppress hook notices. SUBAGENT_UPDATE_CHECK=0 or false also disables it.
21
+ //
22
+ // permissionsCeiling controls launch-time permissions posture:
23
+ // - auto (default): shared engine gates unsafe/residue actions
24
+ // - manual: human approval for residue while still auto-denying DANGER
25
+ // - yolo: preserve the historical bypass/danger-full-access path, except
26
+ // for non-bypassable config-file self-protection
27
+ //
28
+ // escalation applies only in auto mode. irreversible-only routes irreversible
29
+ // NEUTRAL residue to a human; off leaves residue to orchestrator judgment.
30
+ //
31
+ // strictReadParity controls logging only. Unparseable Codex approval payloads
32
+ // always fail closed to ask; warn logs the construct, off silences that log.
33
+ {
34
+ "globalConcurrentSubagents": 20,
35
+ "checkForUpdates": true,
36
+ "permissionsCeiling": "auto",
37
+ "escalation": "irreversible-only",
38
+ "strictReadParity": "warn"
39
+ }