pi-subagents 0.36.0 → 0.37.1
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/CHANGELOG.md +38 -0
- package/README.md +111 -16
- package/agents/delegate.md +2 -0
- package/agents/planner.md +2 -1
- package/agents/worker.md +2 -0
- package/package.json +6 -4
- package/skills/pi-subagents/SKILL.md +18 -973
- package/skills/pi-subagents/references/constraints-and-recipes.md +256 -0
- package/skills/pi-subagents/references/execution-controls.md +411 -0
- package/skills/pi-subagents/references/management-authoring-rpc.md +140 -0
- package/skills/pi-subagents/references/prompting-and-roles.md +268 -0
- package/src/agents/agent-management.ts +7 -1
- package/src/agents/agents.ts +99 -12
- package/src/api/capability-ceiling.ts +17 -0
- package/src/api/delegation.ts +6 -1
- package/src/api/preflight.ts +399 -0
- package/src/extension/index.ts +16 -7
- package/src/extension/rpc.ts +29 -2
- package/src/extension/schemas.ts +10 -4
- package/src/extension/tool-description.ts +4 -2
- package/src/intercom/intercom-bridge.ts +5 -2
- package/src/runs/background/async-execution.ts +125 -14
- package/src/runs/background/async-job-tracker.ts +19 -12
- package/src/runs/background/async-resume.ts +27 -9
- package/src/runs/background/async-status.ts +18 -0
- package/src/runs/background/notify.ts +3 -0
- package/src/runs/background/process-terminal.ts +280 -0
- package/src/runs/background/result-watcher.ts +9 -3
- package/src/runs/background/run-status.ts +7 -1
- package/src/runs/background/scheduled-runs.ts +6 -1
- package/src/runs/background/stale-run-reconciler.ts +6 -0
- package/src/runs/background/subagent-runner.ts +182 -12
- package/src/runs/foreground/chain-execution.ts +5 -0
- package/src/runs/foreground/execution.ts +59 -13
- package/src/runs/foreground/subagent-executor.ts +142 -47
- package/src/runs/shared/acceptance.ts +41 -30
- package/src/runs/shared/capability-ceiling.ts +177 -0
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +56 -17
- package/src/runs/shared/model-fallback.ts +8 -0
- package/src/runs/shared/nested-events.ts +8 -1
- package/src/runs/shared/parallel-utils.ts +5 -0
- package/src/runs/shared/pi-args.ts +141 -58
- package/src/runs/shared/session-lease.ts +25 -5
- package/src/runs/shared/subagent-prompt-runtime.ts +14 -1
- package/src/runs/shared/task-intent.ts +1 -1
- package/src/runs/shared/tool-availability.ts +18 -2
- package/src/shared/launch-contract.ts +123 -0
- package/src/shared/types.ts +118 -8
- package/src/shared/utils.ts +17 -42
- package/src/slash/delegation-adapters.ts +6 -1
- package/src/slash/slash-commands.ts +1 -1
- package/src/tui/fleet-status.ts +62 -16
- package/src/tui/fleet.ts +1 -1
|
@@ -29,10 +29,9 @@ const LEVEL_RANK: Record<Exclude<AcceptanceLevel, "auto">, number> = {
|
|
|
29
29
|
attested: 1,
|
|
30
30
|
checked: 2,
|
|
31
31
|
verified: 3,
|
|
32
|
-
reviewed: 4,
|
|
33
32
|
};
|
|
34
33
|
|
|
35
|
-
const VALID_LEVELS = new Set<AcceptanceLevel>(["auto", "none", "attested", "checked", "verified"
|
|
34
|
+
const VALID_LEVELS = new Set<AcceptanceLevel>(["auto", "none", "attested", "checked", "verified"]);
|
|
36
35
|
const VALID_EVIDENCE = new Set<AcceptanceEvidenceKind>([
|
|
37
36
|
"changed-files",
|
|
38
37
|
"tests-added",
|
|
@@ -48,7 +47,7 @@ const ACCEPTANCE_CONFIG_KEYS = new Set(["level", "criteria", "evidence", "verify
|
|
|
48
47
|
const ACCEPTANCE_GATE_KEYS = new Set(["id", "must", "evidence", "severity"]);
|
|
49
48
|
const ACCEPTANCE_VERIFY_KEYS = new Set(["id", "command", "timeoutMs", "cwd", "env", "allowFailure"]);
|
|
50
49
|
const ACCEPTANCE_REVIEW_KEYS = new Set(["agent", "focus", "required"]);
|
|
51
|
-
const EXPLICIT_REVIEWED_UNAVAILABLE = "
|
|
50
|
+
const EXPLICIT_REVIEWED_UNAVAILABLE = "is an achieved status, not a requestable acceptance level. For a read-only reviewer call, omit acceptance. To require independent review of a writer result, use acceptance.review.required and orchestrate the reviewer separately.";
|
|
52
51
|
|
|
53
52
|
function normalizeLevel(level: AcceptanceLevel | undefined): Exclude<AcceptanceLevel, "auto"> | "auto" {
|
|
54
53
|
return level ?? "auto";
|
|
@@ -67,7 +66,6 @@ function requiredEvidenceForLevel(level: Exclude<AcceptanceLevel, "auto">): Acce
|
|
|
67
66
|
case "checked":
|
|
68
67
|
return ["changed-files", "tests-added", "commands-run", "residual-risks", "no-staged-files"];
|
|
69
68
|
case "verified":
|
|
70
|
-
case "reviewed":
|
|
71
69
|
return ["changed-files", "tests-added", "commands-run", "validation-output", "residual-risks", "no-staged-files"];
|
|
72
70
|
}
|
|
73
71
|
}
|
|
@@ -110,10 +108,10 @@ function inferLevel(input: {
|
|
|
110
108
|
reasons.push(input.async ? "async write-capable or risky run" : "risky write-capable run");
|
|
111
109
|
if (input.dynamic || input.dynamicGroup) reasons.push("dynamic fanout context");
|
|
112
110
|
return {
|
|
113
|
-
level: "
|
|
111
|
+
level: "checked",
|
|
114
112
|
reasons,
|
|
115
113
|
criteria: ["Implement the requested change without widening scope", "Return evidence sufficient for an independent acceptance review"],
|
|
116
|
-
evidence: requiredEvidenceForLevel("
|
|
114
|
+
evidence: requiredEvidenceForLevel("checked"),
|
|
117
115
|
review: { agent: "reviewer", required: true },
|
|
118
116
|
};
|
|
119
117
|
}
|
|
@@ -160,9 +158,9 @@ export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"
|
|
|
160
158
|
if (input === undefined) return errors;
|
|
161
159
|
if (input === false) return errors;
|
|
162
160
|
if (typeof input === "string") {
|
|
163
|
-
if (
|
|
161
|
+
if (input === "reviewed") errors.push(`${pathLabel} ${EXPLICIT_REVIEWED_UNAVAILABLE}`);
|
|
162
|
+
else if (!VALID_LEVELS.has(input as AcceptanceLevel)) errors.push(`${pathLabel} has invalid level '${input}'.`);
|
|
164
163
|
else if (input === "none") errors.push(`${pathLabel} level "none" requires a reason; use { level: "none", reason: "..." }.`);
|
|
165
|
-
else if (input === "reviewed") errors.push(`${pathLabel} ${EXPLICIT_REVIEWED_UNAVAILABLE}`);
|
|
166
164
|
return errors;
|
|
167
165
|
}
|
|
168
166
|
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
@@ -173,13 +171,14 @@ export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"
|
|
|
173
171
|
for (const key of Object.keys(value)) {
|
|
174
172
|
if (!ACCEPTANCE_CONFIG_KEYS.has(key)) errors.push(`${pathLabel}.${key} is not supported.`);
|
|
175
173
|
}
|
|
176
|
-
if (value.level
|
|
177
|
-
errors.push(`${pathLabel}.level
|
|
174
|
+
if (value.level === "reviewed") {
|
|
175
|
+
errors.push(`${pathLabel}.level ${EXPLICIT_REVIEWED_UNAVAILABLE}`);
|
|
176
|
+
} else if (value.level !== undefined && (typeof value.level !== "string" || !VALID_LEVELS.has(value.level as AcceptanceLevel))) {
|
|
177
|
+
errors.push(`${pathLabel}.level must be one of auto, none, attested, checked, verified.`);
|
|
178
178
|
}
|
|
179
179
|
if (value.level === "none" && (typeof value.reason !== "string" || !value.reason.trim())) {
|
|
180
180
|
errors.push(`${pathLabel}.reason is required when level is none.`);
|
|
181
181
|
}
|
|
182
|
-
if (value.level === "reviewed") errors.push(`${pathLabel}.level ${EXPLICIT_REVIEWED_UNAVAILABLE}`);
|
|
183
182
|
if (value.reason !== undefined && typeof value.reason !== "string") errors.push(`${pathLabel}.reason must be a string.`);
|
|
184
183
|
if (value.criteria !== undefined && !Array.isArray(value.criteria)) errors.push(`${pathLabel}.criteria must be an array.`);
|
|
185
184
|
if (Array.isArray(value.criteria)) {
|
|
@@ -362,10 +361,7 @@ export function resolveEffectiveAcceptance(input: {
|
|
|
362
361
|
(explicit.criteria?.length ? explicit.criteria : inferred.criteria) as Array<string | { id?: string; must?: string; evidence?: AcceptanceEvidenceKind[]; severity?: "required" | "recommended" }>,
|
|
363
362
|
evidence,
|
|
364
363
|
);
|
|
365
|
-
|
|
366
|
-
if (level === "reviewed" && input.explicit !== undefined && explicitLevel !== "reviewed" && explicit.review === undefined && review && review !== false) {
|
|
367
|
-
review = { ...review, required: false };
|
|
368
|
-
}
|
|
364
|
+
const review = explicit.review !== undefined ? explicit.review : inferred.review;
|
|
369
365
|
return {
|
|
370
366
|
level,
|
|
371
367
|
explicit: input.explicit !== undefined,
|
|
@@ -1061,8 +1057,10 @@ export async function evaluateAcceptance(input: {
|
|
|
1061
1057
|
reportOptional?: boolean;
|
|
1062
1058
|
}): Promise<AcceptanceLedger> {
|
|
1063
1059
|
const acceptance = input.acceptance;
|
|
1060
|
+
const initialStatus = acceptance.level === "none" ? "not-required" : "claimed";
|
|
1064
1061
|
const ledger: AcceptanceLedger = {
|
|
1065
|
-
status:
|
|
1062
|
+
status: initialStatus,
|
|
1063
|
+
evidenceStatus: initialStatus,
|
|
1066
1064
|
explicit: acceptance.explicit,
|
|
1067
1065
|
effectiveAcceptance: acceptance,
|
|
1068
1066
|
inferredReason: acceptance.inferredReason,
|
|
@@ -1084,11 +1082,13 @@ export async function evaluateAcceptance(input: {
|
|
|
1084
1082
|
if (parsed.report) {
|
|
1085
1083
|
ledger.childReport = parsed.report;
|
|
1086
1084
|
ledger.status = "attested";
|
|
1085
|
+
ledger.evidenceStatus = "attested";
|
|
1087
1086
|
} else if (!input.reportOptional || needsReport || parsed.error !== ACCEPTANCE_REPORT_NOT_FOUND) {
|
|
1088
1087
|
ledger.childReportParseError = parsed.error;
|
|
1089
1088
|
ledger.runtimeChecks.push({ id: "attestation", status: "failed", message: parsed.error ?? "Structured acceptance report missing." });
|
|
1090
1089
|
if (!input.reportOptional) {
|
|
1091
1090
|
ledger.status = "rejected";
|
|
1091
|
+
ledger.evidenceStatus = "rejected";
|
|
1092
1092
|
return ledger;
|
|
1093
1093
|
}
|
|
1094
1094
|
} else {
|
|
@@ -1103,6 +1103,7 @@ export async function evaluateAcceptance(input: {
|
|
|
1103
1103
|
];
|
|
1104
1104
|
if (!ledger.runtimeChecks.some((check) => check.status === "failed")) {
|
|
1105
1105
|
ledger.status = "checked";
|
|
1106
|
+
ledger.evidenceStatus = "checked";
|
|
1106
1107
|
}
|
|
1107
1108
|
}
|
|
1108
1109
|
|
|
@@ -1110,6 +1111,7 @@ export async function evaluateAcceptance(input: {
|
|
|
1110
1111
|
if (acceptance.level === "verified" && acceptance.verify.length === 0) {
|
|
1111
1112
|
ledger.runtimeChecks.push({ id: "verification-config", status: "failed", message: "verified acceptance requires runtime verify commands." });
|
|
1112
1113
|
ledger.status = "rejected";
|
|
1114
|
+
ledger.evidenceStatus = "rejected";
|
|
1113
1115
|
return ledger;
|
|
1114
1116
|
}
|
|
1115
1117
|
ledger.verifyRuns = [];
|
|
@@ -1119,34 +1121,42 @@ export async function evaluateAcceptance(input: {
|
|
|
1119
1121
|
}
|
|
1120
1122
|
if (ledger.verifyRuns.some((run) => run.status === "failed" || run.status === "timed-out")) {
|
|
1121
1123
|
ledger.status = "rejected";
|
|
1124
|
+
ledger.evidenceStatus = "rejected";
|
|
1122
1125
|
return ledger;
|
|
1123
1126
|
}
|
|
1124
|
-
if (!ledger.runtimeChecks.some((check) => check.status === "failed"))
|
|
1127
|
+
if (!ledger.runtimeChecks.some((check) => check.status === "failed")) {
|
|
1128
|
+
ledger.status = "verified";
|
|
1129
|
+
ledger.evidenceStatus = "verified";
|
|
1130
|
+
}
|
|
1125
1131
|
}
|
|
1126
1132
|
|
|
1127
1133
|
if (ledger.runtimeChecks.some((check) => check.status === "failed")) {
|
|
1128
1134
|
ledger.status = "rejected";
|
|
1135
|
+
ledger.evidenceStatus = "rejected";
|
|
1129
1136
|
return ledger;
|
|
1130
1137
|
}
|
|
1131
|
-
if (ledger.status === "claimed"
|
|
1138
|
+
if (ledger.status === "claimed") {
|
|
1132
1139
|
ledger.status = acceptance.level === "verified" ? "verified" : acceptance.level;
|
|
1140
|
+
ledger.evidenceStatus = ledger.status;
|
|
1133
1141
|
}
|
|
1134
1142
|
|
|
1135
|
-
if (acceptance.
|
|
1136
|
-
if (input.reviewResult) {
|
|
1143
|
+
if (acceptance.review && acceptance.review !== false) {
|
|
1144
|
+
if (input.reviewResult?.status === "reviewed") {
|
|
1137
1145
|
ledger.reviewResult = input.reviewResult;
|
|
1138
|
-
ledger.status =
|
|
1139
|
-
} else {
|
|
1140
|
-
|
|
1141
|
-
ledger.
|
|
1142
|
-
|
|
1146
|
+
ledger.status = "reviewed";
|
|
1147
|
+
} else if (input.reviewResult?.status === "blockers") {
|
|
1148
|
+
ledger.reviewResult = input.reviewResult;
|
|
1149
|
+
ledger.status = "rejected";
|
|
1150
|
+
} else if (acceptance.review.required !== false) {
|
|
1151
|
+
ledger.reviewResult = input.reviewResult ?? {
|
|
1152
|
+
status: "review-required",
|
|
1143
1153
|
findings: [{
|
|
1144
|
-
severity:
|
|
1145
|
-
issue: "
|
|
1154
|
+
severity: "non-blocking",
|
|
1155
|
+
issue: "Independent review has not been supplied.",
|
|
1146
1156
|
rationale: "The run cannot be marked reviewed from child evidence alone.",
|
|
1147
1157
|
}],
|
|
1148
1158
|
};
|
|
1149
|
-
|
|
1159
|
+
ledger.status = "review-required";
|
|
1150
1160
|
}
|
|
1151
1161
|
}
|
|
1152
1162
|
|
|
@@ -1154,8 +1164,10 @@ export async function evaluateAcceptance(input: {
|
|
|
1154
1164
|
}
|
|
1155
1165
|
|
|
1156
1166
|
export function buildSkippedAcceptanceLedger(acceptance: ResolvedAcceptanceConfig, input: { id: string; message: string }): AcceptanceLedger {
|
|
1167
|
+
const status = acceptance.level === "none" ? "not-required" : "rejected";
|
|
1157
1168
|
return {
|
|
1158
|
-
status
|
|
1169
|
+
status,
|
|
1170
|
+
evidenceStatus: status,
|
|
1159
1171
|
explicit: acceptance.explicit,
|
|
1160
1172
|
effectiveAcceptance: acceptance,
|
|
1161
1173
|
inferredReason: acceptance.inferredReason,
|
|
@@ -1173,7 +1185,6 @@ export function acceptanceFailureMessage(ledger: AcceptanceLedger): string | und
|
|
|
1173
1185
|
if (failedCheck) return `Acceptance rejected: ${failedCheck.message}`;
|
|
1174
1186
|
const failedVerify = ledger.verifyRuns.find((run) => run.status === "failed" || run.status === "timed-out");
|
|
1175
1187
|
if (failedVerify) return `Acceptance verification '${failedVerify.id}' ${failedVerify.status}.`;
|
|
1176
|
-
if (ledger.reviewResult?.status === "needs-parent-decision") return "Acceptance review required but no automatic reviewer result is available.";
|
|
1177
1188
|
if (ledger.reviewResult?.status === "blockers") return "Acceptance review found blockers.";
|
|
1178
1189
|
return "Acceptance rejected.";
|
|
1179
1190
|
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
|
|
3
|
+
export const SUBAGENT_CAPABILITY_CEILING_VERSION = 1 as const;
|
|
4
|
+
export const SUBAGENT_CAPABILITY_CEILING_REGISTRY_KEY = "pi-subagents.capability-ceiling.v1";
|
|
5
|
+
export const SUBAGENT_CAPABILITY_CEILING_ENV = "PI_SUBAGENT_CAPABILITY_CEILING_V1";
|
|
6
|
+
|
|
7
|
+
export type SubagentCapabilityCeiling =
|
|
8
|
+
| { allowedTools: readonly string[]; denyExtensions?: boolean }
|
|
9
|
+
| { allowedTools?: readonly string[]; denyExtensions: boolean };
|
|
10
|
+
|
|
11
|
+
export interface ResolvedSubagentCapabilityCeiling {
|
|
12
|
+
version: typeof SUBAGENT_CAPABILITY_CEILING_VERSION;
|
|
13
|
+
allowedTools?: string[];
|
|
14
|
+
denyExtensions: boolean;
|
|
15
|
+
sources: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface SubagentCapabilityAudit {
|
|
19
|
+
ceiling: ResolvedSubagentCapabilityCeiling;
|
|
20
|
+
requestedTools?: string[];
|
|
21
|
+
effectiveTools: string[];
|
|
22
|
+
removedTools: string[];
|
|
23
|
+
internalTools: string[];
|
|
24
|
+
extensionsDenied: boolean;
|
|
25
|
+
removedExtensionCount: number;
|
|
26
|
+
requestedMcpToolCount: number;
|
|
27
|
+
effectiveMcpTools: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface RegisterSubagentCapabilityCeilingOptions {
|
|
31
|
+
sessionId: string;
|
|
32
|
+
source: string;
|
|
33
|
+
ceiling: SubagentCapabilityCeiling;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SubagentCapabilityCeilingHandle {
|
|
37
|
+
update(ceiling: SubagentCapabilityCeiling): void;
|
|
38
|
+
dispose(): void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type Registration = { source: string; ceiling: ResolvedSubagentCapabilityCeiling };
|
|
42
|
+
type Registry = Map<string, Map<symbol, Registration>>;
|
|
43
|
+
|
|
44
|
+
function registry(): Registry {
|
|
45
|
+
const key = Symbol.for(SUBAGENT_CAPABILITY_CEILING_REGISTRY_KEY);
|
|
46
|
+
const store = globalThis as typeof globalThis & { [key: symbol]: unknown };
|
|
47
|
+
const existing = store[key];
|
|
48
|
+
if (existing instanceof Map) return existing as Registry;
|
|
49
|
+
const created: Registry = new Map();
|
|
50
|
+
store[key] = created;
|
|
51
|
+
return created;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function validateText(value: unknown, field: string): string {
|
|
55
|
+
if (typeof value !== "string" || !value.trim() || /[\u0000-\u001f\u007f]/u.test(value) || Buffer.byteLength(value.trim(), "utf8") > 256) {
|
|
56
|
+
throw new Error(`Invalid capability ceiling ${field}; expected a non-empty string without control characters (max 256 UTF-8 bytes).`);
|
|
57
|
+
}
|
|
58
|
+
return value.trim();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizeCeiling(ceiling: SubagentCapabilityCeiling): ResolvedSubagentCapabilityCeiling {
|
|
62
|
+
if (!ceiling || typeof ceiling !== "object" || Array.isArray(ceiling)) throw new Error("Invalid capability ceiling; expected an object.");
|
|
63
|
+
const hasAllowedTools = Object.hasOwn(ceiling, "allowedTools");
|
|
64
|
+
const hasDenyExtensions = Object.hasOwn(ceiling, "denyExtensions");
|
|
65
|
+
if (!hasAllowedTools && !hasDenyExtensions) throw new Error("Invalid capability ceiling; expected allowedTools or denyExtensions.");
|
|
66
|
+
if (hasDenyExtensions && typeof ceiling.denyExtensions !== "boolean") throw new Error("Invalid capability ceiling denyExtensions; expected a boolean.");
|
|
67
|
+
let allowedTools: string[] | undefined;
|
|
68
|
+
if (hasAllowedTools) {
|
|
69
|
+
if (!Array.isArray(ceiling.allowedTools)) throw new Error("Invalid capability ceiling allowedTools; expected an array.");
|
|
70
|
+
if (ceiling.allowedTools.length > 256) throw new Error("Invalid capability ceiling allowedTools; expected at most 256 names.");
|
|
71
|
+
allowedTools = [...new Set(ceiling.allowedTools.map((tool) => {
|
|
72
|
+
const name = validateText(tool, "allowedTools entry");
|
|
73
|
+
if (!/^[A-Za-z0-9_.:-]+$/u.test(name)) throw new Error(`Invalid capability ceiling allowedTools entry '${name}'.`);
|
|
74
|
+
if (Buffer.byteLength(name, "utf8") > 128) throw new Error(`Invalid capability ceiling allowedTools entry '${name}'; max 128 UTF-8 bytes.`);
|
|
75
|
+
return name;
|
|
76
|
+
}))].sort();
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
version: SUBAGENT_CAPABILITY_CEILING_VERSION,
|
|
80
|
+
...(allowedTools ? { allowedTools } : {}),
|
|
81
|
+
denyExtensions: ceiling.denyExtensions === true,
|
|
82
|
+
sources: [],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function parseSubagentCapabilityCeiling(value: unknown, field = "capability ceiling"): ResolvedSubagentCapabilityCeiling {
|
|
87
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Invalid ${field}; expected an object.`);
|
|
88
|
+
const record = value as Record<string, unknown>;
|
|
89
|
+
if (record.version !== SUBAGENT_CAPABILITY_CEILING_VERSION) throw new Error(`Invalid ${field} version.`);
|
|
90
|
+
const normalized = normalizeCeiling(record as SubagentCapabilityCeiling);
|
|
91
|
+
const sources = record.sources;
|
|
92
|
+
if (!Array.isArray(sources) || sources.some((source) => typeof source !== "string")) throw new Error(`Invalid ${field} sources; expected an array of strings.`);
|
|
93
|
+
normalized.sources = [...new Set(sources.map((source) => validateText(source, `${field} source`)))].sort();
|
|
94
|
+
return normalized;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function registerSubagentCapabilityCeiling(options: RegisterSubagentCapabilityCeilingOptions): SubagentCapabilityCeilingHandle {
|
|
98
|
+
const sessionId = validateText(options.sessionId, "sessionId");
|
|
99
|
+
const source = validateText(options.source, "source");
|
|
100
|
+
let normalized = normalizeCeiling(options.ceiling);
|
|
101
|
+
const token = Symbol(source);
|
|
102
|
+
const store = registry();
|
|
103
|
+
let session = store.get(sessionId);
|
|
104
|
+
if (!session) {
|
|
105
|
+
session = new Map();
|
|
106
|
+
store.set(sessionId, session);
|
|
107
|
+
}
|
|
108
|
+
const setRegistration = () => {
|
|
109
|
+
normalized = normalizeCeiling(normalized);
|
|
110
|
+
normalized.sources = [source];
|
|
111
|
+
session!.set(token, { source, ceiling: normalized });
|
|
112
|
+
};
|
|
113
|
+
setRegistration();
|
|
114
|
+
let disposed = false;
|
|
115
|
+
return {
|
|
116
|
+
update(ceiling) {
|
|
117
|
+
if (disposed) throw new Error("Cannot update a disposed capability ceiling handle.");
|
|
118
|
+
normalized = normalizeCeiling(ceiling);
|
|
119
|
+
normalized.sources = [source];
|
|
120
|
+
session!.set(token, { source, ceiling: normalized });
|
|
121
|
+
},
|
|
122
|
+
dispose() {
|
|
123
|
+
if (disposed) return;
|
|
124
|
+
disposed = true;
|
|
125
|
+
session!.delete(token);
|
|
126
|
+
if (session!.size === 0) store.delete(sessionId);
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function intersectSubagentCapabilityCeilings(...ceilings: Array<ResolvedSubagentCapabilityCeiling | undefined>): ResolvedSubagentCapabilityCeiling | undefined {
|
|
132
|
+
const active = ceilings.filter((ceiling): ceiling is ResolvedSubagentCapabilityCeiling => ceiling !== undefined);
|
|
133
|
+
if (active.length === 0) return undefined;
|
|
134
|
+
const definedLists = active.filter((ceiling) => ceiling.allowedTools !== undefined).map((ceiling) => new Set(ceiling.allowedTools));
|
|
135
|
+
let allowedTools: string[] | undefined;
|
|
136
|
+
if (definedLists.length > 0) {
|
|
137
|
+
allowedTools = [...definedLists[0]!].filter((tool) => definedLists.every((list) => list.has(tool))).sort();
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
version: SUBAGENT_CAPABILITY_CEILING_VERSION,
|
|
141
|
+
...(allowedTools ? { allowedTools } : {}),
|
|
142
|
+
denyExtensions: active.some((ceiling) => ceiling.denyExtensions),
|
|
143
|
+
sources: [...new Set(active.flatMap((ceiling) => ceiling.sources))].sort(),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function resolveSubagentCapabilityCeiling(sessionId: string | undefined, inherited?: ResolvedSubagentCapabilityCeiling): ResolvedSubagentCapabilityCeiling | undefined {
|
|
148
|
+
const active: ResolvedSubagentCapabilityCeiling[] = [];
|
|
149
|
+
if (sessionId) {
|
|
150
|
+
const registrations = registry().get(sessionId);
|
|
151
|
+
if (registrations) active.push(...Array.from(registrations.values(), ({ ceiling }) => ceiling));
|
|
152
|
+
}
|
|
153
|
+
return intersectSubagentCapabilityCeilings(inherited, ...active);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function resolveCurrentSubagentCapabilityCeiling(sessionId: string | undefined): ResolvedSubagentCapabilityCeiling | undefined {
|
|
157
|
+
return resolveSubagentCapabilityCeiling(sessionId, decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function encodeSubagentCapabilityCeiling(ceiling: ResolvedSubagentCapabilityCeiling | undefined): string | undefined {
|
|
161
|
+
if (!ceiling) return undefined;
|
|
162
|
+
return Buffer.from(JSON.stringify(ceiling), "utf8").toString("base64url");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function decodeSubagentCapabilityCeiling(value: string | undefined): ResolvedSubagentCapabilityCeiling | undefined {
|
|
166
|
+
if (value === undefined || value === "") return undefined;
|
|
167
|
+
let parsed: unknown;
|
|
168
|
+
try {
|
|
169
|
+
parsed = JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
|
|
170
|
+
} catch (error) {
|
|
171
|
+
throw new Error(`Invalid inherited capability ceiling: ${error instanceof Error ? error.message : String(error)}`);
|
|
172
|
+
}
|
|
173
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || (parsed as { version?: unknown }).version !== SUBAGENT_CAPABILITY_CEILING_VERSION) {
|
|
174
|
+
throw new Error("Invalid inherited capability ceiling version.");
|
|
175
|
+
}
|
|
176
|
+
return parseSubagentCapabilityCeiling(parsed, "inherited capability ceiling");
|
|
177
|
+
}
|
|
@@ -50,7 +50,7 @@ const DYNAMIC_PARALLEL_KEYS = new Set(["agent", "task", "phase", "label", "outpu
|
|
|
50
50
|
const RUNNER_DYNAMIC_PARALLEL_KEYS = new Set([
|
|
51
51
|
...DYNAMIC_PARALLEL_KEYS,
|
|
52
52
|
"outputName", "structured", "inheritProjectContext", "inheritSkills", "skills", "outputPath", "namespaceOutputPath", "maxSubagentDepth", "waitToolEnabled",
|
|
53
|
-
"structuredOutput", "structuredOutputSchema", "tools", "extensions", "subagentOnlyExtensions", "mcpDirectTools", "completionGuard", "systemPrompt",
|
|
53
|
+
"structuredOutput", "structuredOutputSchema", "tools", "extensions", "subagentOnlyExtensions", "mcpDirectTools", "capabilityCeiling", "completionGuard", "systemPrompt",
|
|
54
54
|
"systemPromptMode", "thinking", "modelCandidates", "sessionFile", "effectiveAcceptance", "acceptanceInput", "acceptanceRole", "parentSessionId",
|
|
55
55
|
]);
|
|
56
56
|
const DYNAMIC_COLLECT_KEYS = new Set(["as", "outputSchema"]);
|
|
@@ -27,6 +27,7 @@ type ImportKind = keyof typeof IMPORT_PATHS;
|
|
|
27
27
|
interface ServerEntry {
|
|
28
28
|
command?: string;
|
|
29
29
|
args?: string[];
|
|
30
|
+
socket?: string;
|
|
30
31
|
env?: Record<string, string>;
|
|
31
32
|
cwd?: string;
|
|
32
33
|
url?: string;
|
|
@@ -35,6 +36,7 @@ interface ServerEntry {
|
|
|
35
36
|
bearerToken?: string;
|
|
36
37
|
bearerTokenEnv?: string;
|
|
37
38
|
exposeResources?: boolean;
|
|
39
|
+
includeTools?: string[];
|
|
38
40
|
excludeTools?: string[];
|
|
39
41
|
directTools?: boolean | string[];
|
|
40
42
|
}
|
|
@@ -69,14 +71,16 @@ interface MetadataCache {
|
|
|
69
71
|
servers: Record<string, ServerCacheEntry>;
|
|
70
72
|
}
|
|
71
73
|
|
|
72
|
-
export
|
|
74
|
+
export interface ResolvedMcpDirectToolSelection { name: string; selector: string }
|
|
75
|
+
|
|
76
|
+
export function resolveMcpDirectToolSelections(mcpDirectTools: string[] | undefined, cwd = process.cwd()): ResolvedMcpDirectToolSelection[] {
|
|
73
77
|
if (!mcpDirectTools?.length) return [];
|
|
74
78
|
|
|
75
79
|
try {
|
|
76
80
|
const config = loadMcpConfig(cwd);
|
|
77
81
|
const cache = loadMetadataCache();
|
|
78
82
|
if (!cache) return [];
|
|
79
|
-
return
|
|
83
|
+
return resolveDirectToolSelections(config, cache, getToolPrefix(config.settings?.toolPrefix), mcpDirectTools);
|
|
80
84
|
} catch {
|
|
81
85
|
return [];
|
|
82
86
|
}
|
|
@@ -195,8 +199,8 @@ function extractServers(config: unknown, kind: ImportKind): Record<string, Serve
|
|
|
195
199
|
return servers && typeof servers === "object" && !Array.isArray(servers) ? servers as Record<string, ServerEntry> : {};
|
|
196
200
|
}
|
|
197
201
|
|
|
198
|
-
function
|
|
199
|
-
const names:
|
|
202
|
+
function resolveDirectToolSelections(config: McpConfig, cache: MetadataCache, prefix: ToolPrefix, envOverride: string[]): ResolvedMcpDirectToolSelection[] {
|
|
203
|
+
const names: ResolvedMcpDirectToolSelection[] = [];
|
|
200
204
|
const seenNames = new Set<string>();
|
|
201
205
|
const { servers: selectedServers, tools: selectedTools } = parseSelections(envOverride);
|
|
202
206
|
|
|
@@ -216,7 +220,7 @@ function resolveDirectToolNames(config: McpConfig, cache: MetadataCache, prefix:
|
|
|
216
220
|
const prefixedName = formatToolName(tool.name, serverName, prefix);
|
|
217
221
|
if (BUILTIN_TOOL_NAMES.has(prefixedName) || seenNames.has(prefixedName)) continue;
|
|
218
222
|
seenNames.add(prefixedName);
|
|
219
|
-
names.push(prefixedName);
|
|
223
|
+
names.push({ name: prefixedName, selector: `${serverName}/${tool.name}` });
|
|
220
224
|
}
|
|
221
225
|
|
|
222
226
|
if (definition.exposeResources === false) continue;
|
|
@@ -228,13 +232,17 @@ function resolveDirectToolNames(config: McpConfig, cache: MetadataCache, prefix:
|
|
|
228
232
|
const prefixedName = formatToolName(baseName, serverName, prefix);
|
|
229
233
|
if (BUILTIN_TOOL_NAMES.has(prefixedName) || seenNames.has(prefixedName)) continue;
|
|
230
234
|
seenNames.add(prefixedName);
|
|
231
|
-
names.push(prefixedName);
|
|
235
|
+
names.push({ name: prefixedName, selector: `${serverName}/${baseName}` });
|
|
232
236
|
}
|
|
233
237
|
}
|
|
234
238
|
|
|
235
239
|
return names;
|
|
236
240
|
}
|
|
237
241
|
|
|
242
|
+
export function resolveMcpDirectToolNames(mcpDirectTools: string[] | undefined, cwd = process.cwd()): string[] {
|
|
243
|
+
return resolveMcpDirectToolSelections(mcpDirectTools, cwd).map((selection) => selection.name);
|
|
244
|
+
}
|
|
245
|
+
|
|
238
246
|
function parseSelections(selections: string[]): { servers: Set<string>; tools: Map<string, Set<string>> } {
|
|
239
247
|
const servers = new Set<string>();
|
|
240
248
|
const tools = new Map<string, Set<string>>();
|
|
@@ -265,14 +273,16 @@ export function computeMcpServerHash(definition: ServerEntry): string {
|
|
|
265
273
|
const identity: Record<string, unknown> = {
|
|
266
274
|
command: definition.command,
|
|
267
275
|
args: definition.args,
|
|
276
|
+
socket: resolveConfigPath(definition.socket),
|
|
268
277
|
env: interpolateEnvRecord(definition.env),
|
|
269
278
|
cwd: resolveConfigPath(definition.cwd),
|
|
270
|
-
url: definition
|
|
279
|
+
url: resolveServerUrl(definition),
|
|
271
280
|
headers: interpolateEnvRecord(definition.headers),
|
|
272
281
|
auth: definition.auth,
|
|
273
282
|
bearerToken: resolveBearerToken(definition),
|
|
274
283
|
bearerTokenEnv: definition.bearerTokenEnv,
|
|
275
284
|
exposeResources: definition.exposeResources,
|
|
285
|
+
includeTools: definition.includeTools,
|
|
276
286
|
excludeTools: definition.excludeTools,
|
|
277
287
|
};
|
|
278
288
|
return createHash("sha256").update(stableStringify(identity)).digest("hex");
|
|
@@ -327,22 +337,51 @@ function resourceNameToToolName(name: string): string {
|
|
|
327
337
|
}
|
|
328
338
|
|
|
329
339
|
function interpolateEnvRecord(values: Record<string, string> | undefined): Record<string, string> | undefined {
|
|
330
|
-
if (!values
|
|
331
|
-
|
|
332
|
-
for (const [key, value] of Object.entries(values)) {
|
|
333
|
-
if (typeof value === "string") resolved[key] = interpolateEnvVars(value);
|
|
334
|
-
}
|
|
335
|
-
return resolved;
|
|
340
|
+
if (!values) return undefined;
|
|
341
|
+
return Object.fromEntries(Object.entries(values).map(([key, value]) => [key, interpolateSecretExpression(value)]));
|
|
336
342
|
}
|
|
337
343
|
|
|
338
344
|
function interpolateEnvVars(value: string): string {
|
|
339
345
|
return value
|
|
340
346
|
.replace(/\$\{(\w+)\}/g, (_, name: string) => process.env[name] ?? "")
|
|
341
|
-
.replace(/\$env:(\w+)/g, (_, name: string) => process.env[name] ?? "")
|
|
347
|
+
.replace(/\$env:(\w+)/g, (_, name: string) => process.env[name] ?? "")
|
|
348
|
+
.replace(/\{env:(\w+)\}/g, (_, name: string) => process.env[name] ?? "");
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function interpolateSecretExpression(value: string): string {
|
|
352
|
+
if (value.startsWith("!!")) return interpolateEnvVars(value.slice(1));
|
|
353
|
+
return value.startsWith("!") ? value : interpolateEnvVars(value);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function getMissingEnvVars(value: string): string[] {
|
|
357
|
+
const missing = new Set<string>();
|
|
358
|
+
for (const match of value.matchAll(/\$\{(\w+)\}|\$env:(\w+)|\{env:(\w+)\}/g)) {
|
|
359
|
+
const name = match[1] ?? match[2] ?? match[3];
|
|
360
|
+
if (name && process.env[name] === undefined) missing.add(name);
|
|
361
|
+
}
|
|
362
|
+
return [...missing];
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function resolveServerUrl(definition: Pick<ServerEntry, "url">): string | undefined {
|
|
366
|
+
if (definition.url == null) return undefined;
|
|
367
|
+
if (typeof definition.url !== "string") throw new Error("MCP server URL must be a string");
|
|
368
|
+
|
|
369
|
+
const missing = getMissingEnvVars(definition.url);
|
|
370
|
+
if (missing.length > 0) {
|
|
371
|
+
throw new Error(`Missing environment variable${missing.length === 1 ? "" : "s"} in MCP server URL: ${missing.join(", ")}`);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const resolved = interpolateEnvVars(definition.url);
|
|
375
|
+
try {
|
|
376
|
+
new URL(resolved);
|
|
377
|
+
} catch (error) {
|
|
378
|
+
throw new Error(`Invalid MCP server URL after environment interpolation: ${resolved}`, { cause: error });
|
|
379
|
+
}
|
|
380
|
+
return resolved;
|
|
342
381
|
}
|
|
343
382
|
|
|
344
383
|
function resolveConfigPath(value: string | undefined): string | undefined {
|
|
345
|
-
if (
|
|
384
|
+
if (value === undefined) return undefined;
|
|
346
385
|
const resolved = interpolateEnvVars(value);
|
|
347
386
|
if (resolved === "~") return os.homedir();
|
|
348
387
|
if (resolved.startsWith("~/") || resolved.startsWith("~\\")) return path.join(os.homedir(), resolved.slice(2));
|
|
@@ -350,8 +389,8 @@ function resolveConfigPath(value: string | undefined): string | undefined {
|
|
|
350
389
|
}
|
|
351
390
|
|
|
352
391
|
function resolveBearerToken(definition: Pick<ServerEntry, "bearerToken" | "bearerTokenEnv">): string | undefined {
|
|
353
|
-
if (
|
|
354
|
-
return
|
|
392
|
+
if (definition.bearerToken !== undefined) return interpolateSecretExpression(definition.bearerToken);
|
|
393
|
+
return definition.bearerTokenEnv ? process.env[definition.bearerTokenEnv] : undefined;
|
|
355
394
|
}
|
|
356
395
|
|
|
357
396
|
function stableStringify(value: unknown): string {
|
|
@@ -30,6 +30,14 @@ export interface ParentModel {
|
|
|
30
30
|
id: string;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
export function normalizeParentModel(model: unknown): ParentModel | undefined {
|
|
34
|
+
if (!model || typeof model !== "object") return undefined;
|
|
35
|
+
const candidate = model as { provider?: unknown; id?: unknown };
|
|
36
|
+
if (typeof candidate.provider !== "string" || typeof candidate.id !== "string") return undefined;
|
|
37
|
+
if (!candidate.provider || !candidate.id) return undefined;
|
|
38
|
+
return { provider: candidate.provider, id: candidate.id };
|
|
39
|
+
}
|
|
40
|
+
|
|
33
41
|
/**
|
|
34
42
|
* Normalize a model id or provider segment for fuzzy comparison: case-fold,
|
|
35
43
|
* treat dots/underscores as dashes (so `4.5` matches `4-5`), and collapse
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
SUBAGENT_PARENT_RUN_ID_ENV,
|
|
28
28
|
} from "./pi-args.ts";
|
|
29
29
|
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
30
|
+
import { sanitizeProcessTerminal } from "../background/process-terminal.ts";
|
|
30
31
|
|
|
31
32
|
export const NESTED_EVENTS_DIR = path.join(TEMP_ROOT_DIR, "nested-subagent-events");
|
|
32
33
|
const ROUTE_FILE = "route.json";
|
|
@@ -873,6 +874,9 @@ export function nestedSummaryFromAsyncStatus(status: AsyncStatus, asyncDir: stri
|
|
|
873
874
|
...(status.pid ? { pid: status.pid } : {}),
|
|
874
875
|
...(status.sessionId ? { sessionId: status.sessionId } : {}),
|
|
875
876
|
mode: status.mode ?? fallback.mode,
|
|
877
|
+
...(status.processTerminal ? { processTerminal: sanitizeProcessTerminal(status.processTerminal, { runId: status.runId || fallback.id, runnerProcessInstanceId: status.processTerminal.runnerProcessInstanceId }, `${asyncDir}/status.json`) } : {}),
|
|
878
|
+
...(status.capabilityCeiling ? { capabilityCeiling: status.capabilityCeiling } : {}),
|
|
879
|
+
...(status.capabilityAudit ? { capabilityAudit: status.capabilityAudit } : {}),
|
|
876
880
|
state: status.state,
|
|
877
881
|
...(status.currentStep !== undefined ? { currentStep: status.currentStep } : {}),
|
|
878
882
|
...(status.chainStepCount !== undefined ? { chainStepCount: status.chainStepCount } : {}),
|
|
@@ -896,7 +900,7 @@ export function nestedSummaryFromAsyncStatus(status: AsyncStatus, asyncDir: stri
|
|
|
896
900
|
...(status.endedAt !== undefined ? { endedAt: status.endedAt } : {}),
|
|
897
901
|
lastUpdate: status.lastUpdate ?? fallback.ts,
|
|
898
902
|
...(status.sessionFile ? { sessionFile: status.sessionFile } : {}),
|
|
899
|
-
...(status.steps?.length ? { steps: status.steps.map((step) => ({
|
|
903
|
+
...(status.steps?.length ? { steps: status.steps.map((step, index) => ({
|
|
900
904
|
agent: step.agent,
|
|
901
905
|
status: step.status,
|
|
902
906
|
...(step.sessionFile ? { sessionFile: step.sessionFile } : {}),
|
|
@@ -915,6 +919,9 @@ export function nestedSummaryFromAsyncStatus(status: AsyncStatus, asyncDir: stri
|
|
|
915
919
|
...(step.turnBudget ? { turnBudget: step.turnBudget } : {}),
|
|
916
920
|
...(step.turnBudgetExceeded !== undefined ? { turnBudgetExceeded: step.turnBudgetExceeded } : {}),
|
|
917
921
|
...(step.wrapUpRequested !== undefined ? { wrapUpRequested: step.wrapUpRequested } : {}),
|
|
922
|
+
...(step.processTerminal ? { processTerminal: sanitizeProcessTerminal(step.processTerminal, { runId: status.runId || fallback.id, runnerProcessInstanceId: step.processTerminal.runnerProcessInstanceId }, `${asyncDir}/status.json step ${index}`) } : {}),
|
|
923
|
+
...(step.capabilityCeiling ? { capabilityCeiling: step.capabilityCeiling } : {}),
|
|
924
|
+
...(step.capabilityAudit ? { capabilityAudit: step.capabilityAudit } : {}),
|
|
918
925
|
})).slice(0, MAX_STEPS) } : {}),
|
|
919
926
|
};
|
|
920
927
|
}
|
|
@@ -43,11 +43,16 @@ export interface RunnerSubagentStep {
|
|
|
43
43
|
};
|
|
44
44
|
structuredOutputSchema?: import("../../shared/types.ts").JsonSchemaObject;
|
|
45
45
|
agentContract?: import("../../shared/types.ts").AgentContract;
|
|
46
|
+
definitionDigest?: string;
|
|
47
|
+
launchBindingTask?: string;
|
|
48
|
+
launchContractDigest?: string;
|
|
46
49
|
effectiveAcceptance?: import("../../shared/types.ts").ResolvedAcceptanceConfig;
|
|
47
50
|
acceptanceInput?: import("../../shared/types.ts").AcceptanceInput;
|
|
48
51
|
acceptanceRole?: import("../../shared/types.ts").AcceptanceRole;
|
|
49
52
|
gateOn?: import("../../shared/types.ts").ChainGateLayer;
|
|
50
53
|
toolBudget?: import("../../shared/types.ts").ResolvedToolBudget;
|
|
54
|
+
capabilityCeiling?: import("./capability-ceiling.ts").ResolvedSubagentCapabilityCeiling;
|
|
55
|
+
capabilityAudit?: import("./capability-ceiling.ts").SubagentCapabilityAudit;
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
export interface ParallelStepGroup {
|