@sema-agent/core 5.0.1 → 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 (54) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/agents/roster-store.d.ts +9 -2
  3. package/dist/agents/roster-store.js +26 -5
  4. package/dist/agents/subagent.js +8 -3
  5. package/dist/bin/sema-tb.d.ts +1 -2
  6. package/dist/bin/sema-tb.js +13 -25
  7. package/dist/brain/anthropic.js +2 -3
  8. package/dist/brain/degrading.d.ts +2 -8
  9. package/dist/brain/degrading.js +3 -3
  10. package/dist/brain/terminal-cause.js +2 -8
  11. package/dist/core/hooks.js +4 -4
  12. package/dist/core/lsp.js +2 -3
  13. package/dist/core/mcp.d.ts +1 -1
  14. package/dist/core/mcp.js +2 -1
  15. package/dist/core/memory-engine/engine.js +2 -2
  16. package/dist/core/memory-engine/file-backend.js +3 -1
  17. package/dist/core/memory-engine/layout.d.ts +3 -0
  18. package/dist/core/memory-engine/layout.js +51 -0
  19. package/dist/core/memory.d.ts +0 -1
  20. package/dist/core/memory.js +1 -1
  21. package/dist/core/permission-rules.js +8 -7
  22. package/dist/core/protocol-table.d.ts +14 -0
  23. package/dist/core/protocol-table.js +23 -0
  24. package/dist/core/runner/active-skill-scope.js +7 -7
  25. package/dist/core/runner/prepare-memory.js +4 -1
  26. package/dist/core/runner/prepare-task.js +17 -16
  27. package/dist/core/runner/runtask.js +9 -3
  28. package/dist/core/runner/session-rule-policy.js +1 -1
  29. package/dist/core/runner/synthetic-tools.js +2 -2
  30. package/dist/core/runner/tool-disclosure.d.ts +0 -1
  31. package/dist/core/runner/tool-disclosure.js +0 -2
  32. package/dist/core/runner/turn-attachments.js +2 -1
  33. package/dist/core/sensitive-path-policy.js +2 -2
  34. package/dist/core/tool-policy.d.ts +0 -3
  35. package/dist/core/tool-policy.js +29 -15
  36. package/dist/index.d.ts +1 -0
  37. package/dist/index.js +1 -0
  38. package/dist/orchestration/run-spec.js +1 -1
  39. package/dist/orchestration/run-workflow-tool.js +2 -2
  40. package/dist/orchestration/workflow-script-store.d.ts +1 -1
  41. package/dist/orchestration/workflow-script-store.js +1 -1
  42. package/dist/prompt-assembly/assemble.js +0 -1
  43. package/dist/prompt-assembly/tool-catalog.d.ts +2 -1
  44. package/dist/prompts/default.d.ts +0 -2
  45. package/dist/prompts/default.js +1 -5
  46. package/dist/stores/cc/mailbox-store.d.ts +4 -0
  47. package/dist/stores/cc/mailbox-store.js +16 -2
  48. package/dist/stores/file/session-policy-store.d.ts +10 -1
  49. package/dist/stores/file/session-policy-store.js +20 -4
  50. package/dist/tools/fs/fs-search-tools.js +1 -2
  51. package/dist/tools/fs/fs-shared.d.ts +0 -1
  52. package/dist/tools/fs/fs-shared.js +0 -1
  53. package/dist/tools/todo.js +1 -1
  54. package/package.json +1 -1
@@ -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";
@@ -1835,7 +1836,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1835
1836
  ...(assembled.sections
1836
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 } : {}) })) }
1837
1838
  : {}),
1838
- tools: projectToolManifest(tools, (t) => (t.name.startsWith("mcp__") ? "mcp" : "caller")),
1839
+ tools: projectToolManifest(tools, (t) => protocolOf(t.name)?.id ?? "caller"),
1839
1840
  };
1840
1841
  const epochDeclaredSections = (assembled.sections ?? [])
1841
1842
  .filter((s) => s.origin === "provider")
@@ -2209,7 +2210,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2209
2210
  err.code = "config.legacy_tool_name";
2210
2211
  throw err;
2211
2212
  }
2212
- if (n.includes("__") && !n.startsWith("mcp__")) {
2213
+ if (n.includes("__") && protocolOf(n) === undefined) {
2213
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__".`);
2214
2215
  err.code = "config.legacy_tool_name";
2215
2216
  throw err;
@@ -2339,7 +2340,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2339
2340
  if (round >= 3) {
2340
2341
  return {
2341
2342
  action: "deny",
2342
- 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`,
2343
2344
  };
2344
2345
  }
2345
2346
  let re;
@@ -2349,7 +2350,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2349
2350
  catch (err) {
2350
2351
  return {
2351
2352
  action: "deny",
2352
- 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)}`,
2353
2354
  };
2354
2355
  }
2355
2356
  if (re.action === "deny")
@@ -2363,7 +2364,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2363
2364
  toolName: creq.toolName,
2364
2365
  toolCallId: creq.toolCallId,
2365
2366
  args: editArgs,
2366
- 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)`,
2367
2368
  ...askSourceIdentity(),
2368
2369
  ...(re.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
2369
2370
  }, onAskOf, csignal ?? abortController.signal);
@@ -2381,7 +2382,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2381
2382
  if (first === undefined) {
2382
2383
  return {
2383
2384
  action: "deny",
2384
- reason: `inherited parent policy could not be arbitrated for "${creq.toolName}" ` +
2385
+ message: `inherited parent policy could not be arbitrated for "${creq.toolName}" ` +
2385
2386
  `(shared-instance first decision unavailable); denied fail-closed`,
2386
2387
  };
2387
2388
  }
@@ -2399,7 +2400,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2399
2400
  }
2400
2401
  return {
2401
2402
  action: "deny",
2402
- 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 ` +
2403
2404
  `ask cannot be reconstructed in a delegated child; denied fail-closed (tighten-only)`,
2404
2405
  };
2405
2406
  }
@@ -2409,7 +2410,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2409
2410
  toolName: creq.toolName,
2410
2411
  toolCallId: creq.toolCallId,
2411
2412
  args: presentedArgs,
2412
- 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)`,
2413
2414
  ...askSourceIdentity(),
2414
2415
  ...(first.action === "ask" && first.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
2415
2416
  }, pc.onAsk, csignal ?? abortController.signal);
@@ -2419,7 +2420,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2419
2420
  return first;
2420
2421
  return {
2421
2422
  action: "deny",
2422
- 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`,
2423
2424
  };
2424
2425
  }
2425
2426
  if (resolved.action !== "allow")
@@ -2440,7 +2441,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2440
2441
  catch (err) {
2441
2442
  return {
2442
2443
  action: "deny",
2443
- 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)}`,
2444
2445
  };
2445
2446
  }
2446
2447
  if (decision.action !== "ask")
@@ -2455,7 +2456,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2455
2456
  }
2456
2457
  return {
2457
2458
  action: "deny",
2458
- 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 ` +
2459
2460
  `ask cannot be reconstructed in a delegated child; denied fail-closed (tighten-only)`,
2460
2461
  };
2461
2462
  }
@@ -2465,7 +2466,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2465
2466
  toolName: creq.toolName,
2466
2467
  toolCallId: creq.toolCallId,
2467
2468
  args: presentedArgs,
2468
- 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)`,
2469
2470
  ...askSourceIdentity(),
2470
2471
  ...(decision.action === "ask" && decision.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
2471
2472
  }, pc.onAsk, csignal ?? abortController.signal);
@@ -2475,7 +2476,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2475
2476
  return decision;
2476
2477
  return {
2477
2478
  action: "deny",
2478
- 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`,
2479
2480
  };
2480
2481
  }
2481
2482
  if (resolved.action !== "allow")
@@ -2630,7 +2631,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2630
2631
  const adjudicate = effectivePolicy
2631
2632
  ? (req) => raceAbort(Promise.resolve(effectivePolicy.check({ ...req, budget: budgetSnapshot }, abortController.signal)), abortController.signal, () => ({
2632
2633
  action: "deny",
2633
- reason: "policy check aborted (task timed out or cancelled)",
2634
+ message: "policy check aborted (task timed out or cancelled)",
2634
2635
  }))
2635
2636
  : undefined;
2636
2637
  const sanitizePreview = (node, depth = 0) => {
@@ -2672,7 +2673,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2672
2673
  if (inheritedUnavailableAsks.delete(req.toolCallId)) {
2673
2674
  return {
2674
2675
  action: "deny",
2675
- 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 ` +
2676
2677
  `(no live approver reachable, or a durable-park mandate applies), and no durable approval park is ` +
2677
2678
  `available — denied fail-closed (the inherited constraint stands).`,
2678
2679
  decisionReason: "mode",
@@ -2705,7 +2706,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2705
2706
  const preview = approvalPreviewOf(req.toolName, req.args);
2706
2707
  return preview !== undefined ? { preview } : {};
2707
2708
  })(),
2708
- message: decision.message ?? decision.reason ?? `approval required for "${req.toolName}"`,
2709
+ message: decision.message ?? decision.message ?? `approval required for "${req.toolName}"`,
2709
2710
  ...askSourceIdentity(),
2710
2711
  ...(decision.action === "ask" && decision.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
2711
2712
  }, onAsk, abortController.signal);
@@ -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
  }
@@ -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 ?? []);
@@ -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;
@@ -3,7 +3,7 @@ import { isAbsolute, join, normalize as normalizePath, sep } from "node:path";
3
3
  import { BASH_READONLY_DEFAULT_ALLOW, parseLeadingCommandName } from "../tools/fs/index.js";
4
4
  import { writeTargetPath } from "../tools/fs/safety.js";
5
5
  export function decisionText(d) {
6
- return d.message ?? d.reason;
6
+ return d.message;
7
7
  }
8
8
  const ALLOW = { action: "allow" };
9
9
  function withTimeout(p, ms, onTimeout) {
@@ -39,10 +39,10 @@ export function createAllowDenyPolicy(opts) {
39
39
  check(req) {
40
40
  const toolName = req.toolName;
41
41
  if (deny.has(toolName)) {
42
- return { action: "deny", reason: `tool "${req.toolName}" is denied by policy` };
42
+ return { action: "deny", message: `tool "${req.toolName}" is denied by policy` };
43
43
  }
44
44
  if (allow && !allow.has(toolName)) {
45
- return { action: "deny", reason: `tool "${req.toolName}" is not in the allowlist` };
45
+ return { action: "deny", message: `tool "${req.toolName}" is not in the allowlist` };
46
46
  }
47
47
  return ALLOW;
48
48
  },
@@ -66,11 +66,11 @@ export function createApprovalPolicy(opts) {
66
66
  async check(req, signal) {
67
67
  const toolName = req.toolName;
68
68
  if (deny.has(toolName)) {
69
- return { action: "deny", reason: `tool "${req.toolName}" is denied by policy` };
69
+ return { action: "deny", message: `tool "${req.toolName}" is denied by policy` };
70
70
  }
71
71
  if (need.has(toolName)) {
72
72
  if (signal?.aborted) {
73
- return { action: "deny", reason: `approval aborted for "${req.toolName}" (task ended)` };
73
+ return { action: "deny", message: `approval aborted for "${req.toolName}" (task ended)` };
74
74
  }
75
75
  let ok;
76
76
  try {
@@ -79,13 +79,19 @@ export function createApprovalPolicy(opts) {
79
79
  catch (err) {
80
80
  return {
81
81
  action: "deny",
82
- reason: `approval errored for "${req.toolName}": ${err instanceof Error ? err.message : String(err)}`,
82
+ message: `approval errored for "${req.toolName}": ${err instanceof Error ? err.message : String(err)}`,
83
83
  };
84
84
  }
85
- return ok ? ALLOW : { action: "deny", reason: `approval denied for "${req.toolName}"` };
85
+ const okRaw = ok;
86
+ if (okRaw === true)
87
+ return ALLOW;
88
+ if (okRaw !== false) {
89
+ return { action: "deny", message: `approval callback for "${req.toolName}" returned an out-of-contract value (${typeof ok}) — denied fail-closed (return true, false, or the {allow} object)` };
90
+ }
91
+ return { action: "deny", message: `approval denied for "${req.toolName}"` };
86
92
  }
87
93
  if (opts.denyByDefault && !auto.has(toolName)) {
88
- return { action: "deny", reason: `tool "${req.toolName}" requires explicit allow` };
94
+ return { action: "deny", message: `tool "${req.toolName}" requires explicit allow` };
89
95
  }
90
96
  return ALLOW;
91
97
  },
@@ -130,7 +136,7 @@ export function createCoarseCommandNamePolicy(opts) {
130
136
  const shellTools = canonicalToolNameSet(opts.tools);
131
137
  const defaultAction = opts.defaultAction ?? "ask";
132
138
  const fallback = (reason) => defaultAction === "deny"
133
- ? { action: "deny", reason, decisionReason: "rule" }
139
+ ? { action: "deny", message: reason, decisionReason: "rule" }
134
140
  : { action: "ask", message: reason, decisionReason: "rule" };
135
141
  return {
136
142
  check(req) {
@@ -145,7 +151,7 @@ export function createCoarseCommandNamePolicy(opts) {
145
151
  return fallback(`command is not a single simple command (${parsed.reject})`);
146
152
  }
147
153
  if (deny.has(parsed.name)) {
148
- return { action: "deny", reason: `command "${parsed.name}" is denied by policy`, decisionReason: "rule" };
154
+ return { action: "deny", message: `command "${parsed.name}" is denied by policy`, decisionReason: "rule" };
149
155
  }
150
156
  if (allow && !allow.has(parsed.name)) {
151
157
  return fallback(`command "${parsed.name}" is not in the allowlist`);
@@ -394,7 +400,7 @@ export function createUnverifiableDeletePolicy(opts) {
394
400
  action: "ask",
395
401
  decisionReason: "safety",
396
402
  requiresRealApproval: true,
397
- reason: `Unverifiable recursive delete (fail-closed unless cleared): ${finding}. ` +
403
+ message: `Unverifiable recursive delete (fail-closed unless cleared): ${finding}. ` +
398
404
  `Re-run the delete with the resolved literal path written into the command itself ` +
399
405
  `(or assign the variable in the same command, e.g. \`DIR=/exact/path; rm -rf "$DIR"\`) so the target can be verified.`,
400
406
  };
@@ -468,7 +474,7 @@ export function createTranscriptIntegrityPolicy(opts) {
468
474
  action: "ask",
469
475
  decisionReason: "safety",
470
476
  requiresRealApproval: true,
471
- reason: `Session-transcript write (fail-closed unless cleared): ${what}. Session transcripts (the .jsonl files ` +
477
+ message: `Session-transcript write (fail-closed unless cleared): ${what}. Session transcripts (the .jsonl files ` +
472
478
  `under the agent data dir's sessions/ directory) are harness-written session state, not agent working ` +
473
479
  `files — modifying or deleting them tampers with the run's own audit trail. Reading them (ls/cat/grep ` +
474
480
  `as a single simple command) is fine.`,
@@ -613,7 +619,15 @@ export async function resolveAsk(req, onAsk, signal) {
613
619
  }
614
620
  return { action: "allow", updatedInput: edit.value, decisionReason: "mode" };
615
621
  }
616
- return ok
617
- ? { action: "allow", decisionReason: "mode", presentedInput: presented.value }
618
- : { action: "deny", message: `approval denied for "${req.toolName}": ${req.message}`, decisionReason: "mode" };
622
+ const okRaw = ok;
623
+ if (okRaw === true)
624
+ return { action: "allow", decisionReason: "mode", presentedInput: presented.value };
625
+ if (okRaw !== false) {
626
+ return {
627
+ action: "deny",
628
+ message: `the approver for "${req.toolName}" returned an out-of-contract value (${typeof ok}) — denied fail-closed (return true, false, "unavailable", or the {allow} object)`,
629
+ decisionReason: "mode",
630
+ };
631
+ }
632
+ return { action: "deny", message: `approval denied for "${req.toolName}": ${req.message}`, decisionReason: "mode" };
619
633
  }
package/dist/index.d.ts CHANGED
@@ -40,6 +40,7 @@ export { decideAutoPromote, deriveTripwire, FROZEN_DENYLIST_FLOOR, type AutoProm
40
40
  export { runCascade, type CascadeRung, type CascadeConfig, type CascadeAttempt, type CascadeRunResult, type GateVerdict, } from "./agents/cascade.js";
41
41
  export { pgQuery, mysqlQuery, sqliteQuery } from "./tools/sql-adapters.js";
42
42
  export { materializeMcpTools, MCP_PREFIX, type MaterializedMcp, type McpServerStatus, type McpRefreshResult } from "./core/mcp.js";
43
+ export { PROTOCOL_TABLE, MCP_NAMESPACE, protocolOf, type ProtocolNamespace, type ProtocolId } from "./core/protocol-table.js";
43
44
  export { InMemorySessionPolicyStore, SessionPolicyError, loosenReasons, normalizeRules, stripRev, type SessionPolicyStore, type SessionPermissionRules, type StoredSessionRules, type SessionRulesRecord, type PutRulesOptions, } from "./core/session-policy-store.js";
44
45
  export { SAFETY_MERGE_CONFORMANCE_CORPUS, type SafetyMergeVector } from "./core/safety-merge-corpus.js";
45
46
  export { SAFETY_AXIS_VOCABULARY } from "./core/safety-axis-vocab.js";
package/dist/index.js CHANGED
@@ -37,6 +37,7 @@ export { decideAutoPromote, deriveTripwire, FROZEN_DENYLIST_FLOOR, } from "./cor
37
37
  export { runCascade, } from "./agents/cascade.js";
38
38
  export { pgQuery, mysqlQuery, sqliteQuery } from "./tools/sql-adapters.js";
39
39
  export { materializeMcpTools, MCP_PREFIX } from "./core/mcp.js";
40
+ export { PROTOCOL_TABLE, MCP_NAMESPACE, protocolOf } from "./core/protocol-table.js";
40
41
  export { InMemorySessionPolicyStore, SessionPolicyError, loosenReasons, normalizeRules, stripRev, } from "./core/session-policy-store.js";
41
42
  export { SAFETY_MERGE_CONFORMANCE_CORPUS } from "./core/safety-merge-corpus.js";
42
43
  export { SAFETY_AXIS_VOCABULARY } from "./core/safety-axis-vocab.js";
@@ -28,7 +28,7 @@ function frozenDenyPolicy(rootDir, frozenResolved) {
28
28
  const absForm = isAbsolutePathForm(raw);
29
29
  const abs = pathKey(absForm ? raw : resolve(rootDir, raw));
30
30
  if (frozen.has(abs)) {
31
- return { action: "deny", reason: `${raw} is part of the frozen specification surface (read-only).` };
31
+ return { action: "deny", message: `${raw} is part of the frozen specification surface (read-only).` };
32
32
  }
33
33
  return { action: "allow" };
34
34
  },
@@ -344,8 +344,8 @@ export async function createRunWorkflowTool(d) {
344
344
  }
345
345
  if (resolved === undefined)
346
346
  return structuredError(`unknown workflow name: ${JSON.stringify(rawName)}`);
347
- script = typeof resolved === "string" ? resolved : resolved.script;
348
- if (typeof resolved !== "string" && "defaultArgs" in resolved)
347
+ script = resolved.script;
348
+ if ("defaultArgs" in resolved)
349
349
  registeredDefaultArgs = resolved.defaultArgs;
350
350
  if (typeof resolved !== "string" && typeof resolved.stringArgKey === "string" && resolved.stringArgKey.length > 0) {
351
351
  registeredStringArgKey = resolved.stringArgKey;
@@ -12,7 +12,7 @@ export interface WorkflowScriptStore {
12
12
  readonly scopePartitioned: true;
13
13
  persist(runId: string, script: string, scope: string): Promise<string> | string;
14
14
  load(scriptPath: string, scope: string): Promise<string> | string;
15
- resolveName?(name: string): Promise<string | NamedWorkflowResolution | undefined> | string | NamedWorkflowResolution | undefined;
15
+ resolveName?(name: string): Promise<NamedWorkflowResolution | undefined> | NamedWorkflowResolution | undefined;
16
16
  list?(): NamedWorkflowListing[] | Promise<NamedWorkflowListing[]>;
17
17
  }
18
18
  export declare function mergeWorkflowArgs(callArgs: unknown, defaultArgs: unknown): unknown;
@@ -95,7 +95,7 @@ export function createFileWorkflowScriptStore(dir) {
95
95
  const script = readFileSync(path, "utf-8");
96
96
  const argsPath = join(root, `${stem}.args.json`);
97
97
  if (!existsSync(argsPath))
98
- return script;
98
+ return { script };
99
99
  let defaultArgs;
100
100
  try {
101
101
  defaultArgs = JSON.parse(readFileSync(argsPath, "utf-8"));
@@ -101,7 +101,6 @@ export function assemblePrompt(inputs) {
101
101
  userSystemPrompt: inputs.userSystemPrompt,
102
102
  userAppendSystemPrompt: inputs.userAppendSystemPrompt,
103
103
  tools: inputs.tools,
104
- memoryEnabled: false,
105
104
  consolidationEnabled: false,
106
105
  ...facts,
107
106
  };
@@ -1,6 +1,7 @@
1
1
  import type { TSchema } from "typebox";
2
2
  import type { AgentTool } from "../internal/harness-types.js";
3
- export type ToolOrigin = "core" | "caller" | "mcp" | "synthetic";
3
+ import type { ProtocolId } from "../core/protocol-table.js";
4
+ export type ToolOrigin = "core" | "caller" | "synthetic" | ProtocolId;
4
5
  export interface ToolContractDescriptor {
5
6
  contractId: string;
6
7
  implementationRevision: string;
@@ -64,8 +64,6 @@ export interface StablePromptContext {
64
64
  userSystemPrompt?: string;
65
65
  userAppendSystemPrompt?: string;
66
66
  tools: AgentTool[];
67
- memoryEnabled: boolean;
68
- consolidationEnabled?: boolean;
69
67
  policyEnabled?: boolean;
70
68
  hooksEnabled?: boolean;
71
69
  isolationEnabled?: boolean;
@@ -365,10 +365,6 @@ export function constitutionBlocks(ctx) {
365
365
  blocks.push({ id: "mode.worktree", text: WORKTREE_NOTICE });
366
366
  if (ctx.goalEnabled)
367
367
  blocks.push({ id: "mode.goal", text: GOAL_COMPLETION_GUIDANCE });
368
- if (ctx.memoryEnabled)
369
- blocks.push({ id: "memory.safety", text: MEMORY_SAFETY });
370
- if (ctx.memoryEnabled && !ctx.consolidationEnabled)
371
- blocks.push({ id: "memory.hygiene", text: MEMORY_HYGIENE });
372
368
  return blocks;
373
369
  }
374
370
  export function composeConstitution(roleBase, ctx) {
@@ -393,7 +389,7 @@ export function analyzePromptCacheFriendliness(provider, opts = {}) {
393
389
  const build = (memoryBlock) => {
394
390
  if (!provider.stableSystem)
395
391
  throw new Error("PromptProvider does not implement stableSystem");
396
- return composeSystemPrompt(provider.stableSystem({ userSystemPrompt: opts.userSystemPrompt, tools, memoryEnabled: true }), memoryBlock);
392
+ return composeSystemPrompt(provider.stableSystem({ userSystemPrompt: opts.userSystemPrompt, tools }), memoryBlock);
397
393
  };
398
394
  const a = build(PROBE_MEMORY_A);
399
395
  const b = build(PROBE_MEMORY_B);