@sema-agent/core 5.0.0 → 5.1.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.
Files changed (58) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/dist/agents/repair-loop.js +9 -1
  3. package/dist/agents/roster-store.d.ts +9 -2
  4. package/dist/agents/roster-store.js +26 -5
  5. package/dist/agents/send-message-tool.js +1 -1
  6. package/dist/agents/subagent.js +15 -5
  7. package/dist/bin/sema-tb.d.ts +1 -2
  8. package/dist/bin/sema-tb.js +13 -25
  9. package/dist/brain/anthropic.js +2 -3
  10. package/dist/brain/degrading.d.ts +2 -8
  11. package/dist/brain/degrading.js +3 -3
  12. package/dist/brain/terminal-cause.js +2 -8
  13. package/dist/core/hooks.js +4 -4
  14. package/dist/core/lsp.js +2 -3
  15. package/dist/core/mcp.d.ts +1 -1
  16. package/dist/core/mcp.js +2 -1
  17. package/dist/core/memory-engine/engine.js +2 -2
  18. package/dist/core/memory-engine/file-backend.js +3 -1
  19. package/dist/core/memory-engine/layout.d.ts +3 -0
  20. package/dist/core/memory-engine/layout.js +51 -0
  21. package/dist/core/memory.d.ts +0 -1
  22. package/dist/core/memory.js +1 -1
  23. package/dist/core/permission-rules.d.ts +2 -2
  24. package/dist/core/permission-rules.js +18 -7
  25. package/dist/core/protocol-table.d.ts +14 -0
  26. package/dist/core/protocol-table.js +23 -0
  27. package/dist/core/runner/active-skill-scope.js +7 -7
  28. package/dist/core/runner/prepare-memory.js +4 -1
  29. package/dist/core/runner/prepare-task.js +38 -25
  30. package/dist/core/runner/runtask.js +9 -3
  31. package/dist/core/runner/session-rule-policy.d.ts +2 -2
  32. package/dist/core/runner/session-rule-policy.js +2 -1
  33. package/dist/core/runner/synthetic-tools.js +2 -2
  34. package/dist/core/runner/tool-disclosure.d.ts +0 -1
  35. package/dist/core/runner/tool-disclosure.js +0 -2
  36. package/dist/core/runner/turn-attachments.js +2 -1
  37. package/dist/core/sensitive-path-policy.js +2 -2
  38. package/dist/core/tool-policy.d.ts +0 -3
  39. package/dist/core/tool-policy.js +29 -15
  40. package/dist/index.d.ts +1 -0
  41. package/dist/index.js +1 -0
  42. package/dist/orchestration/run-spec.js +1 -1
  43. package/dist/orchestration/run-workflow-tool.js +2 -2
  44. package/dist/orchestration/workflow-script-store.d.ts +1 -1
  45. package/dist/orchestration/workflow-script-store.js +1 -1
  46. package/dist/prompt-assembly/assemble.js +0 -1
  47. package/dist/prompt-assembly/tool-catalog.d.ts +2 -1
  48. package/dist/prompts/default.d.ts +0 -2
  49. package/dist/prompts/default.js +1 -5
  50. package/dist/stores/cc/mailbox-store.d.ts +4 -0
  51. package/dist/stores/cc/mailbox-store.js +16 -2
  52. package/dist/stores/file/session-policy-store.d.ts +10 -1
  53. package/dist/stores/file/session-policy-store.js +20 -4
  54. package/dist/tools/fs/fs-search-tools.js +1 -2
  55. package/dist/tools/fs/fs-shared.d.ts +0 -1
  56. package/dist/tools/fs/fs-shared.js +0 -1
  57. package/dist/tools/todo.js +1 -1
  58. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import type { ToolPolicy } from "./tool-policy.js";
1
+ import type { NamedToolPolicy } from "./tool-policy.js";
2
2
  export declare const BASH_GENERIC_PARAMS: ReadonlySet<string>;
3
3
  export interface PermissionRule {
4
4
  rule: string;
@@ -32,4 +32,4 @@ export declare function parsePermissionRule(rule: string): ParsedPermissionRule;
32
32
  export declare function wildcardMatch(pattern: string, value: string): boolean;
33
33
  export declare const fileToolParamVocabulary: (canonicalTool: string) => ReadonlySet<string> | undefined;
34
34
  export declare function validatePermissionRules(rules: PermissionRule[], opts?: Pick<PermissionRulePolicyOptions, "primaryFieldGeneric" | "caps">): PermissionRuleIssue[];
35
- export declare function createPermissionRulePolicy(rules: PermissionRule[], opts?: PermissionRulePolicyOptions): ToolPolicy;
35
+ export declare function createPermissionRulePolicy(rules: PermissionRule[], opts?: PermissionRulePolicyOptions): NamedToolPolicy;
@@ -1,3 +1,4 @@
1
+ import { MCP_NAMESPACE } from "./protocol-table.js";
1
2
  export const BASH_GENERIC_PARAMS = new Set(["command", "timeout", "description", "run_in_background"]);
2
3
  const DEFAULT_CAPS = {
3
4
  maxRules: 256,
@@ -95,7 +96,7 @@ const FILE_TOOL_PARAMS = {
95
96
  Write: new Set(["file_path", "path", "content"]),
96
97
  NotebookEdit: new Set(["notebook_path", "path", "cell_id", "cell_type", "edit_mode", "new_source"]),
97
98
  Grep: new Set([
98
- "pattern", "path", "glob", "type", "output_mode", "head_limit", "max_results", "offset",
99
+ "pattern", "path", "glob", "type", "output_mode", "head_limit", "offset",
99
100
  "multiline", "context", "ignore_case", "-i", "-n", "-o", "-A", "-B", "-C",
100
101
  ]),
101
102
  Glob: new Set(["pattern", "path", "max_results"]),
@@ -142,7 +143,7 @@ function compile(rules, caps, primaryFieldGeneric) {
142
143
  }
143
144
  }
144
145
  const toolName = parsed.toolName;
145
- if (toolName.startsWith("mcp__") && (parsed.ruleContent !== undefined || indexOfUnescaped(text, "(") !== -1)) {
146
+ if (toolName.startsWith(MCP_NAMESPACE.prefix) && (parsed.ruleContent !== undefined || indexOfUnescaped(text, "(") !== -1)) {
146
147
  bad(text, "unsupported.mcp_paren", "MCP rules do not support patterns in parentheses (CC parity); use the toolAxes system");
147
148
  continue;
148
149
  }
@@ -259,23 +260,33 @@ export function createPermissionRulePolicy(rules, opts) {
259
260
  }
260
261
  }
261
262
  const defaultAction = opts?.defaultAction ?? "allow";
263
+ const nameSets = { deny: [], ask: [], allow: [] };
264
+ for (const [toolName, entry] of byTool) {
265
+ if (entry.bare.deny !== undefined || entry.param.deny.length > 0)
266
+ nameSets.deny.push(toolName);
267
+ if (entry.bare.ask !== undefined || entry.param.ask.length > 0)
268
+ nameSets.ask.push(toolName);
269
+ if (entry.bare.allow !== undefined)
270
+ nameSets.allow.push(toolName);
271
+ }
262
272
  return {
273
+ nameSets: [nameSets],
263
274
  check(req) {
264
275
  const entry = byTool.get(req.toolName);
265
276
  if (entry) {
266
277
  if (entry.bare.deny) {
267
- return { action: "deny", reason: ruleMessage("denied", entry.bare.deny.ruleText, entry.bare.deny.source) };
278
+ return { action: "deny", message: ruleMessage("denied", entry.bare.deny.ruleText, entry.bare.deny.source) };
268
279
  }
269
280
  const paramDeny = matchParamRules(entry.param.deny, req.args, caps.maxScalarValueChars);
270
281
  if (paramDeny) {
271
- return { action: "deny", reason: ruleMessage("denied", paramDeny.ruleText, paramDeny.source) };
282
+ return { action: "deny", message: ruleMessage("denied", paramDeny.ruleText, paramDeny.source) };
272
283
  }
273
284
  if (entry.bare.ask) {
274
- return { action: "ask", reason: ruleMessage("flagged", entry.bare.ask.ruleText, entry.bare.ask.source) };
285
+ return { action: "ask", message: ruleMessage("flagged", entry.bare.ask.ruleText, entry.bare.ask.source) };
275
286
  }
276
287
  const paramAsk = matchParamRules(entry.param.ask, req.args, caps.maxScalarValueChars);
277
288
  if (paramAsk) {
278
- return { action: "ask", reason: ruleMessage("flagged", paramAsk.ruleText, paramAsk.source) };
289
+ return { action: "ask", message: ruleMessage("flagged", paramAsk.ruleText, paramAsk.source) };
279
290
  }
280
291
  if (entry.bare.allow) {
281
292
  return { action: "allow" };
@@ -283,7 +294,7 @@ export function createPermissionRulePolicy(rules, opts) {
283
294
  }
284
295
  if (defaultAction === "allow")
285
296
  return { action: "allow" };
286
- return { action: defaultAction, reason: `no permission rule matched "${req.toolName}" (default-${defaultAction} rule set)` };
297
+ return { action: defaultAction, message: `no permission rule matched "${req.toolName}" (default-${defaultAction} rule set)` };
287
298
  },
288
299
  };
289
300
  }
@@ -0,0 +1,14 @@
1
+ export type ProtocolId = "mcp";
2
+ export interface ProtocolNamespace {
3
+ readonly id: ProtocolId;
4
+ readonly prefix: string;
5
+ makeName(peer: string, tool: string): string;
6
+ parse(name: string): {
7
+ peer: string;
8
+ tool: string;
9
+ } | undefined;
10
+ displayGroupKey(name: string): string;
11
+ }
12
+ export declare const MCP_NAMESPACE: ProtocolNamespace;
13
+ export declare const PROTOCOL_TABLE: readonly ProtocolNamespace[];
14
+ export declare function protocolOf(name: string): ProtocolNamespace | undefined;
@@ -0,0 +1,23 @@
1
+ const NAME_SEP = "__";
2
+ export const MCP_NAMESPACE = {
3
+ id: "mcp",
4
+ prefix: `mcp${NAME_SEP}`,
5
+ makeName: (peer, tool) => `mcp${NAME_SEP}${peer}${NAME_SEP}${tool}`,
6
+ parse(name) {
7
+ if (!name.startsWith(this.prefix))
8
+ return undefined;
9
+ const rest = name.slice(this.prefix.length);
10
+ const sep = rest.indexOf(NAME_SEP);
11
+ if (sep <= 0)
12
+ return undefined;
13
+ return { peer: rest.slice(0, sep), tool: rest.slice(sep + NAME_SEP.length) };
14
+ },
15
+ displayGroupKey(name) {
16
+ const p = this.parse(name);
17
+ return p === undefined ? name : `${this.prefix}${p.peer}${NAME_SEP}*`;
18
+ },
19
+ };
20
+ export const PROTOCOL_TABLE = [MCP_NAMESPACE];
21
+ export function protocolOf(name) {
22
+ return PROTOCOL_TABLE.find((ns) => name.startsWith(ns.prefix));
23
+ }
@@ -28,7 +28,7 @@ export function createActiveSkillScopePolicy(opts) {
28
28
  if (unresolved && unresolved.kind === "unresolved") {
29
29
  return {
30
30
  action: "deny",
31
- reason: `tool "${req.toolName}" denied: active skill artifact "${unresolved.lineageId}" manifest could not be resolved (${unresolved.reason}) — fail-closed deny-all`,
31
+ message: `tool "${req.toolName}" denied: active skill artifact "${unresolved.lineageId}" manifest could not be resolved (${unresolved.reason}) — fail-closed deny-all`,
32
32
  decisionReason: "safety",
33
33
  };
34
34
  }
@@ -49,7 +49,7 @@ export function createActiveSkillScopePolicy(opts) {
49
49
  const ids = manifests.map((m) => m.lineageId).join(", ");
50
50
  return {
51
51
  action: "deny",
52
- reason: `tool "${req.toolName}" is not in the active skill manifest allowlist (artifact(s): ${ids})`,
52
+ message: `tool "${req.toolName}" is not in the active skill manifest allowlist (artifact(s): ${ids})`,
53
53
  decisionReason: "safety",
54
54
  };
55
55
  }
@@ -68,7 +68,7 @@ export function createActiveSkillScopePolicy(opts) {
68
68
  if (!admitted) {
69
69
  return {
70
70
  action: "deny",
71
- reason: `tool "${req.toolName}" is narrowed by skill manifest "${frame.manifest.lineageId}": ${rejections.join("; ")}`,
71
+ message: `tool "${req.toolName}" is narrowed by skill manifest "${frame.manifest.lineageId}": ${rejections.join("; ")}`,
72
72
  decisionReason: "safety",
73
73
  };
74
74
  }
@@ -80,7 +80,7 @@ export function createActiveSkillScopePolicy(opts) {
80
80
  const ids = manifests.filter((m) => m.allowPaths?.length).map((m) => m.lineageId).join(", ");
81
81
  return {
82
82
  action: "deny",
83
- reason: `write-capable tool "${req.toolName}" denied: active skill manifest constrains writes to allowPaths but this tool's target cannot be path-confined (only ${[...PATH_WRITE_TOOLS].join("/")} are) — fail-closed (artifact(s): ${ids})`,
83
+ message: `write-capable tool "${req.toolName}" denied: active skill manifest constrains writes to allowPaths but this tool's target cannot be path-confined (only ${[...PATH_WRITE_TOOLS].join("/")} are) — fail-closed (artifact(s): ${ids})`,
84
84
  decisionReason: "safety",
85
85
  };
86
86
  }
@@ -92,7 +92,7 @@ export function createActiveSkillScopePolicy(opts) {
92
92
  if (typeof path !== "string" || path.length === 0) {
93
93
  return {
94
94
  action: "deny",
95
- reason: `write tool "${req.toolName}" denied: active skill manifest constrains writes to allowPaths but the call has no resolvable path`,
95
+ message: `write tool "${req.toolName}" denied: active skill manifest constrains writes to allowPaths but the call has no resolvable path`,
96
96
  decisionReason: "safety",
97
97
  };
98
98
  }
@@ -100,7 +100,7 @@ export function createActiveSkillScopePolicy(opts) {
100
100
  if (!canon.ok) {
101
101
  return {
102
102
  action: "deny",
103
- reason: `write to "${path}" denied: its real target could not be resolved against the active skill manifest allowPaths`,
103
+ message: `write to "${path}" denied: its real target could not be resolved against the active skill manifest allowPaths`,
104
104
  decisionReason: "safety",
105
105
  };
106
106
  }
@@ -115,7 +115,7 @@ export function createActiveSkillScopePolicy(opts) {
115
115
  if (!inside) {
116
116
  return {
117
117
  action: "deny",
118
- reason: `write to "${path}" is outside the paths allowed by skill manifest "${m.lineageId}" (allowPaths: ${(m.allowPaths ?? []).join(", ")})`,
118
+ message: `write to "${path}" is outside the paths allowed by skill manifest "${m.lineageId}" (allowPaths: ${(m.allowPaths ?? []).join(", ")})`,
119
119
  decisionReason: "safety",
120
120
  };
121
121
  }
@@ -1,4 +1,4 @@
1
- import { deriveControlPlaneDir, deriveProjectControlDir, deriveProjectMemoryDir, deriveRepoMemoryDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, lookupProjectIdHint, recordProjectIdHint, resolveMemoryEngineRoot } from "../memory-engine/layout.js";
1
+ import { adoptLegacyRepoDirs, deriveControlPlaneDir, deriveProjectControlDir, deriveProjectMemoryDir, deriveRepoMemoryDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, lookupProjectIdHint, recordProjectIdHint, resolveMemoryEngineRoot } from "../memory-engine/layout.js";
2
2
  import { classifyScopePlanes, derivePersonalControlDir, derivePersonalMemoryDir, mergeHarvestReports, mergeInjections, needsDualRoots, parsedProjectPlane } from "../memory-engine/dual-root.js";
3
3
  import { normalizeMemorySpec } from "../memory.js";
4
4
  import { MemoryEngine } from "../memory-engine/engine.js";
@@ -15,6 +15,9 @@ export async function prepareMemory(input) {
15
15
  const pinned = backend.directoryRoot;
16
16
  const engineRoot = resolveMemoryEngineRoot(deps.memoryEngineDir);
17
17
  const repoRoot = deps.rootPath ?? taskRootPath;
18
+ if (repoRoot) {
19
+ adoptLegacyRepoDirs(engineRoot, repoRoot, (err) => deps.onError?.(err, { phase: "memory", sessionId }));
20
+ }
18
21
  let identityKey;
19
22
  if (memorySpec.scopeContract === "v2" && repoRoot) {
20
23
  const consumesProjectIdentity = [...memorySpec.scopes, ...(memorySpec.writeScope !== null ? [memorySpec.writeScope] : [])]
@@ -30,6 +30,7 @@ import { CacheBreakDetector, toolsToFingerprintInputs } from "../cache-break-det
30
30
  import { reservedCollisions, reservedFor } from "../../brain/request-params.js";
31
31
  import { defineTool, isDefineToolProduct } from "../tools.js";
32
32
  import { RETIRED_TOOL_NAMES } from "../tool-name-aliases.js";
33
+ import { protocolOf } from "../protocol-table.js";
33
34
  import { pathToUri } from "../lsp-protocol.js";
34
35
  import { DEFAULT_TOOL_RESULT_THRESHOLD_CHARS, createOffloadPersist, firstPartyOffloadPolicy, InMemoryToolResultStore, RunnerSharedToolResultStore, ScopedToolResultStore, isVolatileOffloadStore, OFFLOAD_TOOL_NAME, createReadToolResultTool, withToolResultOffload, } from "../tool-result-store.js";
35
36
  import { OUTPUT_TOOL_NAME, REPORT_FINDINGS_TOOL_NAME, SKILL_CONTENT_MAX_CHARS, SKILL_TOOL_NAME, createOutputTool, createReportBlockedTool, createReportFindingsTool, createSkillTool, normalizeSkills } from "./synthetic-tools.js";
@@ -1378,6 +1379,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1378
1379
  ...(deps.backgroundAgentStore !== undefined ? { agentStore: deps.backgroundAgentStore } : {}),
1379
1380
  ...(deps.mailboxStore !== undefined ? { mailbox: deps.mailboxStore } : {}),
1380
1381
  ...(reviveSpawn !== undefined ? { reviveSpawn } : {}),
1382
+ onNotifyError: (f) => emitTrace(deps.tracer, () => ({ kind: "observer.notify_failed", version: 1, taskId: hostTaskId, site: f.site, message: f.error.message, ts: Date.now() })),
1381
1383
  })));
1382
1384
  if (!(spec.tools ?? []).some((t) => t.name === AGENT_TRANSCRIPT_TOOL_NAME)) {
1383
1385
  tools.push(firstPartyOffload(createAgentTranscriptTool({
@@ -1834,7 +1836,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1834
1836
  ...(assembled.sections
1835
1837
  ? { sections: assembled.sections.map((s) => ({ id: s.id, slot: s.slot, carrier: s.carrier, cadence: s.cadence, cacheClass: s.cacheClass, chars: s.text.length, hash: saltedHash(s.text), ...(s.declaredContentHash !== undefined ? { contentHash: s.declaredContentHash } : {}) })) }
1836
1838
  : {}),
1837
- tools: projectToolManifest(tools, (t) => (t.name.startsWith("mcp__") ? "mcp" : "caller")),
1839
+ tools: projectToolManifest(tools, (t) => protocolOf(t.name)?.id ?? "caller"),
1838
1840
  };
1839
1841
  const epochDeclaredSections = (assembled.sections ?? [])
1840
1842
  .filter((s) => s.origin === "provider")
@@ -2190,8 +2192,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2190
2192
  }
2191
2193
  }
2192
2194
  const policy = spec.toolPolicy ?? deps.toolPolicy;
2193
- {
2194
- const nameGroups = toolPolicyNameSets(policy);
2195
+ const auditPolicyNames = (auditee) => {
2196
+ const nameGroups = toolPolicyNameSets(auditee);
2195
2197
  if (nameGroups.length > 0) {
2196
2198
  const known = new Set(tools.flatMap((t) => [t.name, ...(t.aliases ?? [])]));
2197
2199
  for (const t of harnessTools)
@@ -2208,6 +2210,11 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2208
2210
  err.code = "config.legacy_tool_name";
2209
2211
  throw err;
2210
2212
  }
2213
+ if (n.includes("__") && protocolOf(n) === undefined) {
2214
+ const err = new Error(`tool policy ${kind}-list entry "${n}" is a pre-prefix MCP tool name and matches nothing in this run's roster — MCP tools are named "mcp__<server>__<tool>" and legacy-name normalization was removed (RB-476-A), so this entry would silently guard nothing. Prefix the entry with "mcp__".`);
2215
+ err.code = "config.legacy_tool_name";
2216
+ throw err;
2217
+ }
2211
2218
  unmatched.add(`"${n}" (${kind})`);
2212
2219
  }
2213
2220
  }
@@ -2231,7 +2238,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2231
2238
  }
2232
2239
  }
2233
2240
  }
2234
- }
2241
+ };
2242
+ auditPolicyNames(policy);
2235
2243
  let sessionRulePolicy;
2236
2244
  if (deps.sessionPolicyStore && spec.sessionId) {
2237
2245
  const rules = await deps.sessionPolicyStore.getRules(spec.sessionId, spec.principal);
@@ -2268,6 +2276,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2268
2276
  })
2269
2277
  : undefined;
2270
2278
  const narrowingLayers = [sessionRulePolicy, ...ancestorRulePolicies, skillScopePolicy].filter((p) => p !== undefined);
2279
+ for (const layer of narrowingLayers)
2280
+ auditPolicyNames(layer);
2271
2281
  const denyNarrowingPolicy = narrowingLayers.length === 0 ? undefined : narrowingLayers.length === 1 ? narrowingLayers[0] : combinePolicies(...narrowingLayers);
2272
2282
  const basePolicyForResumeEdit = spec.toolPolicy ?? deps.toolPolicy;
2273
2283
  const sameInstanceAncestorCount = policy === undefined ? 0 : (inheritedParentConstraints ?? []).reduce((n, pc) => (pc.policy === policy ? n + 1 : n), 0);
@@ -2330,7 +2340,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2330
2340
  if (round >= 3) {
2331
2341
  return {
2332
2342
  action: "deny",
2333
- reason: `the approval-edit chain for "${creq.toolName}" exceeded the re-approval cap (inherited parent policy) — denied fail-closed; re-submit the edited action directly`,
2343
+ message: `the approval-edit chain for "${creq.toolName}" exceeded the re-approval cap (inherited parent policy) — denied fail-closed; re-submit the edited action directly`,
2334
2344
  };
2335
2345
  }
2336
2346
  let re;
@@ -2340,7 +2350,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2340
2350
  catch (err) {
2341
2351
  return {
2342
2352
  action: "deny",
2343
- reason: `inherited parent policy re-check of the approved edit errored for "${creq.toolName}": ${err instanceof Error ? err.message : String(err)}`,
2353
+ message: `inherited parent policy re-check of the approved edit errored for "${creq.toolName}": ${err instanceof Error ? err.message : String(err)}`,
2344
2354
  };
2345
2355
  }
2346
2356
  if (re.action === "deny")
@@ -2354,7 +2364,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2354
2364
  toolName: creq.toolName,
2355
2365
  toolCallId: creq.toolCallId,
2356
2366
  args: editArgs,
2357
- message: re.message ?? re.reason ?? `approval required for "${creq.toolName}" (inherited parent policy)`,
2367
+ message: re.message ?? re.message ?? `approval required for "${creq.toolName}" (inherited parent policy)`,
2358
2368
  ...askSourceIdentity(),
2359
2369
  ...(re.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
2360
2370
  }, onAskOf, csignal ?? abortController.signal);
@@ -2372,7 +2382,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2372
2382
  if (first === undefined) {
2373
2383
  return {
2374
2384
  action: "deny",
2375
- reason: `inherited parent policy could not be arbitrated for "${creq.toolName}" ` +
2385
+ message: `inherited parent policy could not be arbitrated for "${creq.toolName}" ` +
2376
2386
  `(shared-instance first decision unavailable); denied fail-closed`,
2377
2387
  };
2378
2388
  }
@@ -2390,7 +2400,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2390
2400
  }
2391
2401
  return {
2392
2402
  action: "deny",
2393
- reason: `inherited parent policy requires durable approval for "${creq.toolName}" — the parent's durable ` +
2403
+ message: `inherited parent policy requires durable approval for "${creq.toolName}" — the parent's durable ` +
2394
2404
  `ask cannot be reconstructed in a delegated child; denied fail-closed (tighten-only)`,
2395
2405
  };
2396
2406
  }
@@ -2400,7 +2410,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2400
2410
  toolName: creq.toolName,
2401
2411
  toolCallId: creq.toolCallId,
2402
2412
  args: presentedArgs,
2403
- message: first.message ?? first.reason ?? `approval required for "${creq.toolName}" (inherited parent policy)`,
2413
+ message: first.message ?? first.message ?? `approval required for "${creq.toolName}" (inherited parent policy)`,
2404
2414
  ...askSourceIdentity(),
2405
2415
  ...(first.action === "ask" && first.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
2406
2416
  }, pc.onAsk, csignal ?? abortController.signal);
@@ -2410,7 +2420,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2410
2420
  return first;
2411
2421
  return {
2412
2422
  action: "deny",
2413
- reason: `no approver is reachable for the inherited approval of "${creq.toolName}" and the marker channel is at capacity — denied fail-closed`,
2423
+ message: `no approver is reachable for the inherited approval of "${creq.toolName}" and the marker channel is at capacity — denied fail-closed`,
2414
2424
  };
2415
2425
  }
2416
2426
  if (resolved.action !== "allow")
@@ -2431,7 +2441,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2431
2441
  catch (err) {
2432
2442
  return {
2433
2443
  action: "deny",
2434
- reason: `inherited parent policy errored for "${creq.toolName}": ${err instanceof Error ? err.message : String(err)}`,
2444
+ message: `inherited parent policy errored for "${creq.toolName}": ${err instanceof Error ? err.message : String(err)}`,
2435
2445
  };
2436
2446
  }
2437
2447
  if (decision.action !== "ask")
@@ -2446,7 +2456,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2446
2456
  }
2447
2457
  return {
2448
2458
  action: "deny",
2449
- reason: `inherited parent policy requires durable approval for "${creq.toolName}" — the parent's durable ` +
2459
+ message: `inherited parent policy requires durable approval for "${creq.toolName}" — the parent's durable ` +
2450
2460
  `ask cannot be reconstructed in a delegated child; denied fail-closed (tighten-only)`,
2451
2461
  };
2452
2462
  }
@@ -2456,7 +2466,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2456
2466
  toolName: creq.toolName,
2457
2467
  toolCallId: creq.toolCallId,
2458
2468
  args: presentedArgs,
2459
- message: decision.message ?? decision.reason ?? `approval required for "${creq.toolName}" (inherited parent policy)`,
2469
+ message: decision.message ?? decision.message ?? `approval required for "${creq.toolName}" (inherited parent policy)`,
2460
2470
  ...askSourceIdentity(),
2461
2471
  ...(decision.action === "ask" && decision.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
2462
2472
  }, pc.onAsk, csignal ?? abortController.signal);
@@ -2466,7 +2476,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2466
2476
  return decision;
2467
2477
  return {
2468
2478
  action: "deny",
2469
- reason: `no approver is reachable for the inherited approval of "${creq.toolName}" and the marker channel is at capacity — denied fail-closed`,
2479
+ message: `no approver is reachable for the inherited approval of "${creq.toolName}" and the marker channel is at capacity — denied fail-closed`,
2470
2480
  };
2471
2481
  }
2472
2482
  if (resolved.action !== "allow")
@@ -2621,7 +2631,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2621
2631
  const adjudicate = effectivePolicy
2622
2632
  ? (req) => raceAbort(Promise.resolve(effectivePolicy.check({ ...req, budget: budgetSnapshot }, abortController.signal)), abortController.signal, () => ({
2623
2633
  action: "deny",
2624
- reason: "policy check aborted (task timed out or cancelled)",
2634
+ message: "policy check aborted (task timed out or cancelled)",
2625
2635
  }))
2626
2636
  : undefined;
2627
2637
  const sanitizePreview = (node, depth = 0) => {
@@ -2663,7 +2673,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2663
2673
  if (inheritedUnavailableAsks.delete(req.toolCallId)) {
2664
2674
  return {
2665
2675
  action: "deny",
2666
- reason: `approval for "${req.toolName}" requires an ancestor task's approval that cannot be resolved here ` +
2676
+ message: `approval for "${req.toolName}" requires an ancestor task's approval that cannot be resolved here ` +
2667
2677
  `(no live approver reachable, or a durable-park mandate applies), and no durable approval park is ` +
2668
2678
  `available — denied fail-closed (the inherited constraint stands).`,
2669
2679
  decisionReason: "mode",
@@ -2696,7 +2706,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2696
2706
  const preview = approvalPreviewOf(req.toolName, req.args);
2697
2707
  return preview !== undefined ? { preview } : {};
2698
2708
  })(),
2699
- message: decision.message ?? decision.reason ?? `approval required for "${req.toolName}"`,
2709
+ message: decision.message ?? decision.message ?? `approval required for "${req.toolName}"`,
2700
2710
  ...askSourceIdentity(),
2701
2711
  ...(decision.action === "ask" && decision.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
2702
2712
  }, onAsk, abortController.signal);
@@ -2878,6 +2888,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2878
2888
  const token = mintCheckpointToken();
2879
2889
  const gate = { kind: "resource_limit", reason };
2880
2890
  const resourceLedgerOut = debitLedger(priorLedger, sliceSpend, resourceTotal);
2891
+ const mintedAt = Date.now();
2881
2892
  const cp = {
2882
2893
  token,
2883
2894
  version: resourceLedgerOut.totalWalltimeSec !== undefined ? WALLTIME_CHECKPOINT_VERSION : RESOURCE_CHECKPOINT_VERSION,
@@ -2888,8 +2899,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2888
2899
  pendingAction: { kind: "resource_limit", reason },
2889
2900
  state: serializeCheckpointState(remoteHandle),
2890
2901
  status: "pending",
2891
- createdAt: Date.now(),
2892
- deadline: Date.now() + (sanitizedTtlMs(rs.ttlMs) ?? DEFAULT_RESOURCE_TTL_MS),
2902
+ createdAt: mintedAt,
2903
+ deadline: mintedAt + (sanitizedTtlMs(rs.ttlMs) ?? DEFAULT_RESOURCE_TTL_MS),
2893
2904
  suspendCount: priorSuspendCount,
2894
2905
  humanReview: humanReviewRef.count > 0
2895
2906
  ? { count: humanReviewRef.count, totalWaitMs: humanReviewRef.totalWaitMs, gates: [...humanReviewRef.gates] }
@@ -2958,6 +2969,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2958
2969
  const scope = checkpointScopeOf(spec);
2959
2970
  const gate = { kind: "plan_review", reason: reason ?? `plan review requested` };
2960
2971
  const reviewLedger = debitLedger(priorLedger, liveSpendRef.get?.() ?? { costMicroUsd: 0, tokens: 0, turns: 0, walltimeMs: 0 }, resourceTotal, { countSlice: false });
2972
+ const mintedAt = Date.now();
2961
2973
  const cp = {
2962
2974
  token,
2963
2975
  version: reviewLedger.totalWalltimeSec !== undefined ? WALLTIME_CHECKPOINT_VERSION : BINDING_CHECKPOINT_VERSION,
@@ -2968,9 +2980,9 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2968
2980
  pendingAction: { kind: "plan_review" },
2969
2981
  state: serializeCheckpointState(remoteHandle),
2970
2982
  status: "pending",
2971
- createdAt: Date.now(),
2983
+ createdAt: mintedAt,
2972
2984
  suspendedAt: now(),
2973
- deadline: Date.now() + (sanitizedTtlMs(spec.durableApproval?.ttlMs) ?? DEFAULT_RESOURCE_TTL_MS),
2985
+ deadline: mintedAt + (sanitizedTtlMs(spec.durableApproval?.ttlMs) ?? DEFAULT_RESOURCE_TTL_MS),
2974
2986
  suspendCount: priorSuspendCount + 1,
2975
2987
  humanReview: humanReviewRef.count > 0
2976
2988
  ? { count: humanReviewRef.count, totalWaitMs: humanReviewRef.totalWaitMs, gates: [...humanReviewRef.gates] }
@@ -3082,6 +3094,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3082
3094
  const ttlMs = sanitizedTtlMs(durableApproval?.ttlMs);
3083
3095
  const checkpointState = serializeCheckpointState(remoteHandle);
3084
3096
  const approvalLedger = debitLedger(priorLedger, liveSpendRef.get?.() ?? { costMicroUsd: 0, tokens: 0, turns: 0, walltimeMs: 0 }, resourceTotal, { countSlice: false });
3097
+ const mintedAt = Date.now();
3085
3098
  cp = {
3086
3099
  token,
3087
3100
  version: approvalLedger.totalWalltimeSec !== undefined ? WALLTIME_CHECKPOINT_VERSION : BINDING_CHECKPOINT_VERSION,
@@ -3104,12 +3117,12 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3104
3117
  },
3105
3118
  state: checkpointState,
3106
3119
  deadline: ttlMs !== undefined
3107
- ? Date.now() + ttlMs
3120
+ ? mintedAt + ttlMs
3108
3121
  : durableApproval !== undefined
3109
3122
  ? undefined
3110
- : Date.now() + DEFAULT_UNATTENDED_APPROVAL_TTL_MS,
3123
+ : mintedAt + DEFAULT_UNATTENDED_APPROVAL_TTL_MS,
3111
3124
  status: "pending",
3112
- createdAt: Date.now(),
3125
+ createdAt: mintedAt,
3113
3126
  suspendedAt: now(),
3114
3127
  humanReview: humanReviewRef.count > 0
3115
3128
  ? { count: humanReviewRef.count, totalWaitMs: humanReviewRef.totalWaitMs, gates: [...humanReviewRef.gates] }
@@ -1674,7 +1674,13 @@ export class Runner {
1674
1674
  }
1675
1675
  rs.telemetry.taskStart = Date.now();
1676
1676
  rs.telemetry.taskStartMonotonic = performance.now();
1677
- void Promise.resolve(this.sessions.noteTaskRun?.(prepared.sessionId, rs.telemetry.taskId)).catch(() => { });
1677
+ void Promise.resolve(this.sessions.noteTaskRun?.(prepared.sessionId, rs.telemetry.taskId)).catch((err) => {
1678
+ try {
1679
+ this.deps.onError?.(err instanceof Error ? err : new Error(String(err)), { phase: "config", sessionId: prepared.sessionId });
1680
+ }
1681
+ catch {
1682
+ }
1683
+ });
1678
1684
  rs.degrade.recordDegraded = (info, toModel) => {
1679
1685
  if (rs.degrade.degraded !== undefined)
1680
1686
  return;
@@ -3344,7 +3350,7 @@ export class Runner {
3344
3350
  const rechecked = await prepared.basePolicyForResumeEdit.check({ toolName: pendingAction.toolName, args: resolvedArgs, toolCallId: pendingAction.toolCallId }, prepared.abortController.signal);
3345
3351
  if (rechecked.action === "deny") {
3346
3352
  emitEnd(true);
3347
- const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, formatHookFeedback(`The approver EDITED this call's input; the edited call is denied by the deployment's tool policy and was not executed${rechecked.reason ? `: ${rechecked.reason}` : ""}.`), true));
3353
+ const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, formatHookFeedback(`The approver EDITED this call's input; the edited call is denied by the deployment's tool policy and was not executed${rechecked.message ? `: ${rechecked.message}` : ""}.`), true));
3348
3354
  emitCommitted(eid, "toolResult", pendingAction.toolCallId);
3349
3355
  return;
3350
3356
  }
@@ -3353,7 +3359,7 @@ export class Runner {
3353
3359
  const narrowed = await prepared.denyNarrowingPolicy.check({ toolName: pendingAction.toolName, args: resolvedArgs, toolCallId: pendingAction.toolCallId }, prepared.abortController.signal);
3354
3360
  if (narrowed.action === "deny") {
3355
3361
  emitEnd(true);
3356
- const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, formatHookFeedback(`The approved tool call "${pendingAction.toolName}" is now denied by a session rule and was not executed${narrowed.reason ? `: ${narrowed.reason}` : ""}.`), true));
3362
+ const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, formatHookFeedback(`The approved tool call "${pendingAction.toolName}" is now denied by a session rule and was not executed${narrowed.message ? `: ${narrowed.message}` : ""}.`), true));
3357
3363
  emitCommitted(eid, "toolResult", pendingAction.toolCallId);
3358
3364
  return;
3359
3365
  }
@@ -1,6 +1,6 @@
1
1
  import type { ExecutionEnv } from "../../internal/harness-types.js";
2
2
  import type { ToolEffect } from "../types.js";
3
- import { type ToolPolicy } from "../tool-policy.js";
3
+ import { type NamedToolPolicy } from "../tool-policy.js";
4
4
  import type { SessionPermissionRules } from "../session-policy-store.js";
5
5
  export declare const PATH_WRITE_TOOLS: ReadonlySet<string>;
6
6
  export declare function isWithin(root: string, p: string): boolean;
@@ -8,4 +8,4 @@ export declare function createSessionRulePolicy(rules: SessionPermissionRules, o
8
8
  env: ExecutionEnv;
9
9
  rootPath?: string;
10
10
  toolEffects?: ReadonlyMap<string, ToolEffect>;
11
- }): ToolPolicy;
11
+ }): NamedToolPolicy;
@@ -18,7 +18,7 @@ export function isWithin(root, p) {
18
18
  const base = r.endsWith("/") ? r : r + "/";
19
19
  return c.startsWith(base);
20
20
  }
21
- const deny = (reason) => ({ action: "deny", reason, decisionReason: "rule" });
21
+ const deny = (message) => ({ action: "deny", message, decisionReason: "rule" });
22
22
  export function createSessionRulePolicy(rules, opts) {
23
23
  const { env, rootPath, toolEffects } = opts;
24
24
  const toolDeny = new Set(rules.toolDeny ?? []);
@@ -32,6 +32,7 @@ export function createSessionRulePolicy(rules, opts) {
32
32
  : undefined;
33
33
  const allowDirs = rules.allowDirs && rules.allowDirs.length > 0 ? rules.allowDirs : undefined;
34
34
  return {
35
+ nameSets: [{ ...(rules.toolDeny?.length ? { deny: [...rules.toolDeny] } : {}), ...(rules.toolAllow?.length ? { allow: [...rules.toolAllow] } : {}) }],
35
36
  async check(req, signal) {
36
37
  const toolName = req.toolName;
37
38
  if (toolDeny.has(toolName))
@@ -187,13 +187,13 @@ export function createSkillTool(skills, scope) {
187
187
  `Do not invoke a skill that is already running. ` +
188
188
  `If a \`<command-name>\` block is already present this turn, the skill is loaded — follow it directly rather than calling again.`,
189
189
  parameters: Type.Object({
190
- skill: Type.Optional(Type.String({ description: 'The skill name. E.g., "commit", "review-pr", or "pdf"' })),
190
+ skill: Type.String({ description: 'The skill name. E.g., "commit", "review-pr", or "pdf"' }),
191
191
  args: Type.Optional(Type.String({ description: "Optional arguments for the skill" })),
192
192
  }),
193
193
  effect: "read",
194
194
  execute: (rawArgs) => {
195
195
  const a = (rawArgs ?? {});
196
- const name = String(a.skill ?? a.name ?? "");
196
+ const name = String(a.skill ?? "");
197
197
  const skill = byName.get(name);
198
198
  if (!skill) {
199
199
  throw new Error(`Unknown skill "${name}". Available skills: ${names}.`);
@@ -39,7 +39,6 @@ export declare function scoreToolMatch(query: string, info: DeferredToolInfo): n
39
39
  export interface ToolSearchArgs {
40
40
  query?: string;
41
41
  max_results?: number;
42
- select?: string[];
43
42
  }
44
43
  export declare function resolveToolSearchDetailed(args: ToolSearchArgs, registry: ReadonlyMap<string, DeferredToolInfo>): {
45
44
  matched: string[];
@@ -130,8 +130,6 @@ export function resolveToolSearchDetailed(args, registry) {
130
130
  out.push(name);
131
131
  }
132
132
  };
133
- for (const name of args.select ?? [])
134
- add(name);
135
133
  const q = typeof args.query === "string" ? args.query.trim() : "";
136
134
  if (q !== "") {
137
135
  const selectMatch = /^select:(.+)$/i.exec(q);
@@ -1,5 +1,6 @@
1
1
  import { PRESENT_PLAN_TOOL_NAME } from "../present-plan-tool.js";
2
2
  import { defuseFenceMarkers, delimitUntrusted, sanitizeUntrustedText } from "../untrusted-text.js";
3
+ import { protocolOf } from "../protocol-table.js";
3
4
  import { buildSkillsBlock, skillListingLine } from "./synthetic-tools.js";
4
5
  import { TOOL_SEARCH_NAME as TOOL_SEARCH_TOOL_NAME } from "./tool-disclosure.js";
5
6
  export const TODO_REMINDER_CONFIG = {
@@ -384,7 +385,7 @@ export const TOOLS_DELTA_LIST_MAX = 30;
384
385
  function groupByMcpServer(names) {
385
386
  const counts = new Map();
386
387
  for (const n of names) {
387
- const key = n.startsWith("mcp__") ? `${n.split("__", 2).join("__")}__*` : n;
388
+ const key = protocolOf(n)?.displayGroupKey(n) ?? n;
388
389
  counts.set(key, (counts.get(key) ?? 0) + 1);
389
390
  }
390
391
  return [...counts.entries()]
@@ -69,7 +69,7 @@ export function createSensitivePathPolicy(opts) {
69
69
  if (canon.unresolvedSymlink) {
70
70
  return {
71
71
  action: "deny",
72
- reason: `write to "${path}" is blocked: it is a symlink whose real target could not be resolved (it could point onto a guarded sensitive path)`,
72
+ message: `write to "${path}" is blocked: it is a symlink whose real target could not be resolved (it could point onto a guarded sensitive path)`,
73
73
  decisionReason: "safety",
74
74
  };
75
75
  }
@@ -79,7 +79,7 @@ export function createSensitivePathPolicy(opts) {
79
79
  if (hit) {
80
80
  return {
81
81
  action: "deny",
82
- reason: `write to "${path}" is blocked: its real target resolves onto the guarded sensitive path "${hit}"`,
82
+ message: `write to "${path}" is blocked: its real target resolves onto the guarded sensitive path "${hit}"`,
83
83
  decisionReason: "safety",
84
84
  };
85
85
  }
@@ -13,20 +13,17 @@ export type PermissionResult = {
13
13
  action: "allow";
14
14
  updatedInput?: unknown;
15
15
  message?: string;
16
- reason?: string;
17
16
  decisionReason?: DecisionReason;
18
17
  } | {
19
18
  action: "ask";
20
19
  updatedInput?: unknown;
21
20
  message?: string;
22
- reason?: string;
23
21
  decisionReason?: DecisionReason;
24
22
  requiresRealApproval?: boolean;
25
23
  } | {
26
24
  action: "deny";
27
25
  updatedInput?: unknown;
28
26
  message?: string;
29
- reason?: string;
30
27
  decisionReason?: DecisionReason;
31
28
  };
32
29
  export declare function decisionText(d: PermissionResult): string | undefined;