@heretyc/subagent-mcp 2.12.3 → 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/README.md +193 -163
- package/dist/advanced-ruleset.py +67 -67
- package/dist/concurrency.js +325 -11
- package/dist/config-scaffold.js +2 -2
- package/dist/drivers.js +340 -30
- package/dist/effort.js +10 -5
- package/dist/global-concurrency.jsonc +39 -34
- package/dist/global-subagent-mcp-config.jsonc +39 -0
- package/dist/index.js +242 -32
- package/dist/pending-permissions.js +193 -0
- package/dist/permission-classes.json +36 -0
- package/dist/permission-engine.js +253 -0
- package/dist/routing-table.json +3561 -3561
- package/dist/ruleset-scaffold.js +1 -1
- package/dist/setup.js +1 -1
- package/dist/status-helpers.js +10 -2
- package/dist/wait-helpers.js +3 -0
- package/dist/zombie.js +16 -3
- package/package.json +69 -69
- package/scripts/postinstall.mjs +102 -102
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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:
|
|
340
|
-
sandbox:
|
|
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.
|
|
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(
|
|
637
|
+
input: [textInput(inputText)],
|
|
414
638
|
cwd: this.options.cwd,
|
|
415
639
|
model: this.options.model,
|
|
416
640
|
effort: this.options.effort,
|
|
417
|
-
approvalPolicy:
|
|
418
|
-
sandboxPolicy:
|
|
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
|
-
|
|
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:
|
|
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
|
|
56
|
-
|
|
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
|
|
2
|
-
// ------------------------------------------------------------------
|
|
3
|
-
// SOLE source of truth for
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// -
|
|
16
|
-
// -
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
+
}
|