@sema-agent/core 7.40.1 → 7.41.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 (134) hide show
  1. package/CHANGELOG.md +188 -0
  2. package/dist/agents/agent-transcript-tool.d.ts +10 -2
  3. package/dist/agents/agent-transcript-tool.js +52 -26
  4. package/dist/agents/list-agents-tool.d.ts +17 -2
  5. package/dist/agents/list-agents-tool.js +28 -16
  6. package/dist/agents/peer-session-drain.d.ts +3 -1
  7. package/dist/agents/peer-session-drain.js +1 -0
  8. package/dist/agents/subagent-steps.d.ts +12 -2
  9. package/dist/agents/subagent-steps.js +8 -4
  10. package/dist/agents/subagent.d.ts +1 -17
  11. package/dist/agents/subagent.js +12 -24
  12. package/dist/core/agent-definition.d.ts +19 -40
  13. package/dist/core/agent-state.d.ts +91 -0
  14. package/dist/core/agent-state.js +79 -0
  15. package/dist/core/ask-origin.d.ts +28 -6
  16. package/dist/core/ask-origin.js +9 -5
  17. package/dist/core/ask-unresolvable-notice.d.ts +27 -4
  18. package/dist/core/ask-unresolvable-notice.js +8 -0
  19. package/dist/core/auto-promote.d.ts +14 -27
  20. package/dist/core/auto-promote.js +5 -5
  21. package/dist/core/checkpoint-store.d.ts +11 -12
  22. package/dist/core/decision-receipt.d.ts +5 -2
  23. package/dist/core/decision-receipt.js +1 -1
  24. package/dist/core/engine-notice.d.ts +7 -1
  25. package/dist/core/gate-crash-trace.d.ts +1 -1
  26. package/dist/core/gate-fold.js +7 -3
  27. package/dist/core/gate-lanes.js +22 -3
  28. package/dist/core/governance-codes.d.ts +3 -1
  29. package/dist/core/governance-codes.js +3 -0
  30. package/dist/core/hooks.d.ts +20 -6
  31. package/dist/core/hooks.js +28 -28
  32. package/dist/core/mcp-tool-meta.d.ts +24 -0
  33. package/dist/core/mcp-tool-meta.js +17 -0
  34. package/dist/core/mcp.d.ts +1 -6
  35. package/dist/core/mcp.js +6 -7
  36. package/dist/core/mode-policy.d.ts +25 -0
  37. package/dist/core/mode-policy.js +8 -0
  38. package/dist/core/protocol-table.d.ts +3 -0
  39. package/dist/core/protocol-table.js +3 -0
  40. package/dist/core/reminder-mint.d.ts +21 -1
  41. package/dist/core/reminder-mint.js +20 -0
  42. package/dist/core/retired-keys.d.ts +2 -1
  43. package/dist/core/retired-keys.js +13 -2
  44. package/dist/core/rule-denial-sentence.d.ts +2 -2
  45. package/dist/core/runner/call-hook-stations.d.ts +106 -0
  46. package/dist/core/runner/call-hook-stations.js +118 -0
  47. package/dist/core/runner/contracts.d.ts +35 -25
  48. package/dist/core/runner/gate-exit.d.ts +5 -1
  49. package/dist/core/runner/gate-exit.js +10 -6
  50. package/dist/core/runner/prepare-delegation-surface.d.ts +1 -0
  51. package/dist/core/runner/prepare-delegation-surface.js +4 -4
  52. package/dist/core/runner/prepare-gate-stations.d.ts +8 -3
  53. package/dist/core/runner/prepare-gate-stations.js +13 -70
  54. package/dist/core/runner/prepare-inherited-gate.d.ts +6 -3
  55. package/dist/core/runner/prepare-inherited-gate.js +5 -3
  56. package/dist/core/runner/prepare-park-ask.d.ts +4 -0
  57. package/dist/core/runner/prepare-park-ask.js +4 -1
  58. package/dist/core/runner/prepare-policy-chain.d.ts +6 -5
  59. package/dist/core/runner/prepare-policy-chain.js +14 -25
  60. package/dist/core/runner/prepare-project-context.d.ts +15 -16
  61. package/dist/core/runner/prepare-project-context.js +15 -11
  62. package/dist/core/runner/prepare-run-refs.d.ts +8 -8
  63. package/dist/core/runner/prepare-run-refs.js +3 -3
  64. package/dist/core/runner/prepare-safety-scan.js +3 -3
  65. package/dist/core/runner/prepare-suspend-saga.d.ts +2 -1
  66. package/dist/core/runner/prepare-suspend-saga.js +2 -1
  67. package/dist/core/runner/prepare-task.js +14 -12
  68. package/dist/core/runner/prepare-turn-wiring.d.ts +3 -1
  69. package/dist/core/runner/prepare-turn-wiring.js +16 -13
  70. package/dist/core/runner/resume-admission.js +2 -1
  71. package/dist/core/runner/resume-apply.js +12 -23
  72. package/dist/core/runner/resume-checkpoint-screen.js +3 -0
  73. package/dist/core/runner/run-leg.js +61 -47
  74. package/dist/core/runner/runtask.d.ts +1 -1
  75. package/dist/core/runner/session-rule-policy.d.ts +7 -19
  76. package/dist/core/runner/session-rule-policy.js +1 -2
  77. package/dist/core/runner/skill-base-dir.d.ts +10 -0
  78. package/dist/core/runner/skill-base-dir.js +4 -0
  79. package/dist/core/runner/synthetic-tools.d.ts +11 -17
  80. package/dist/core/runner/synthetic-tools.js +7 -8
  81. package/dist/core/safety-axis-vocab.d.ts +2 -2
  82. package/dist/core/safety-axis-vocab.js +2 -0
  83. package/dist/core/session-policy-store.d.ts +63 -11
  84. package/dist/core/session-policy-store.js +52 -6
  85. package/dist/core/skill-grant.d.ts +239 -0
  86. package/dist/core/skill-grant.js +313 -0
  87. package/dist/core/skills-directory.d.ts +5 -34
  88. package/dist/core/skills-directory.js +9 -58
  89. package/dist/core/task-registry-agent.d.ts +14 -2
  90. package/dist/core/task-registry-agent.js +23 -8
  91. package/dist/core/task-registry-shared.d.ts +6 -0
  92. package/dist/core/task-registry.d.ts +16 -0
  93. package/dist/core/task-registry.js +19 -6
  94. package/dist/core/task-spec.d.ts +10 -0
  95. package/dist/core/tool-face.d.ts +3 -2
  96. package/dist/core/tool-name-aliases.d.ts +38 -12
  97. package/dist/core/tool-name-aliases.js +16 -0
  98. package/dist/core/tool-name-readings.d.ts +6 -2
  99. package/dist/core/tool-name-readings.js +6 -4
  100. package/dist/core/tool-policy.d.ts +2 -2
  101. package/dist/core/tool-registry.d.ts +1 -4
  102. package/dist/core/tool-registry.js +0 -3
  103. package/dist/core/tool-spec.d.ts +11 -13
  104. package/dist/core/tools.d.ts +1 -1
  105. package/dist/core/trace.d.ts +15 -0
  106. package/dist/core/types.d.ts +1 -1
  107. package/dist/engine/harness/agent-harness.d.ts +12 -1
  108. package/dist/engine/harness/agent-harness.js +17 -13
  109. package/dist/engine/loop/agent-loop.d.ts +16 -1
  110. package/dist/engine/loop/agent-loop.js +9 -15
  111. package/dist/engine/loop/types.d.ts +2 -0
  112. package/dist/index.d.ts +9 -4
  113. package/dist/index.js +4 -2
  114. package/dist/stores/file/adoption/adopt.js +23 -16
  115. package/dist/stores/file/index.d.ts +8 -2
  116. package/dist/stores/file/index.js +2 -2
  117. package/dist/stores/file/mailbox-store.js +2 -2
  118. package/dist/stores/file/permission-rule-adopt.js +10 -1
  119. package/dist/stores/file/permission-rule-store.d.ts +6 -4
  120. package/dist/stores/file/permission-rule-store.js +127 -59
  121. package/dist/stores/file/session-policy-store.d.ts +86 -14
  122. package/dist/stores/file/session-policy-store.js +135 -118
  123. package/dist/stores/file/task-list-store.d.ts +5 -0
  124. package/dist/stores/file/task-list-store.js +18 -4
  125. package/dist/stores/file/usage-window-store.d.ts +9 -1
  126. package/dist/stores/file/usage-window-store.js +47 -33
  127. package/dist/tools/fs/safety.d.ts +1 -1
  128. package/package.json +1 -1
  129. package/test/export-surface.snapshot.json +45 -5
  130. package/wire-changes/core@7.41.0.json +1865 -0
  131. package/dist/core/runner/active-skill-scope.d.ts +0 -58
  132. package/dist/core/runner/active-skill-scope.js +0 -133
  133. package/dist/core/skill-tool-specifier.d.ts +0 -80
  134. package/dist/core/skill-tool-specifier.js +0 -57
@@ -7,13 +7,14 @@ import { inlineUntrusted } from "./untrusted-text.js";
7
7
  import { isRuleBehavior } from "./permission-rule-model.js";
8
8
  import { ASK_USER_QUESTION_TOOL_NAME } from "./ask-question.js";
9
9
  import { applyPersistedTightening, disclosedRuleSet } from "./persisted-rule-arms.js";
10
+ import { skillAllowDecision, skillRowRefusal } from "./skill-grant.js";
10
11
  import { CD_GIT_COMPOUND_SENTENCE, changesDirectoryBeforeGit, readOnlyShellVerdict } from "./read-only-shell.js";
11
12
  import { readShellCommand } from "./shell-lexer.js";
12
13
  import { COMMAND_RULE_TOOL } from "./permission-rule-model.js";
13
14
  import { catalogRuleFaceOf } from "./tool-registry.js";
14
15
  import { callFaceSeat } from "./tool-roster.js";
15
16
  import { heldBy, wireOf } from "./tool-name-readings.js";
16
- import { asksUnstated, modeAcceptsFileCommand, unstatedAskMessage } from "./mode-policy.js";
17
+ import { asksUnstated, modeAcceptsFileCommand, permissionSeatAt, unstatedAskMessage } from "./mode-policy.js";
17
18
  import { ORG_ADJUDICATION_TIMEOUT_MS, ORG_RULE_DECISION_REASON, ORG_UNAVAILABLE_DECISION_REASON, settleOrgVerdictWithin } from "./permission-rule-org.js";
18
19
  import { exitGate } from "./runner/gate-exit.js";
19
20
  import { traceHookCrash } from "./gate-crash-trace.js";
@@ -167,6 +168,7 @@ export async function runGateLanes(pass) {
167
168
  egress: input.egress,
168
169
  shellGated: input.shellGated,
169
170
  irreversibility: input.irreversibility,
171
+ requiresUserInteraction: input.requiresUserInteraction,
170
172
  probeUnanswered: pass.probeUnanswered === true,
171
173
  writeProtected: pass.writeProtected === true,
172
174
  writeProtectionUnresolved: pass.writeProtectionUnresolved === true,
@@ -279,6 +281,12 @@ export async function runGateLanes(pass) {
279
281
  pass.deniedBy = "persisted_rule";
280
282
  if (tightened.tightened === "deny")
281
283
  pass.denyCause = undefined;
284
+ const skillRefusal = pass.decision.action === "deny" ? undefined : skillRowRefusal(input.skillGrants, laneReq);
285
+ if (skillRefusal !== undefined) {
286
+ pass.decision = skillRefusal;
287
+ pass.deniedBy = "policy";
288
+ pass.denyCause = undefined;
289
+ }
282
290
  }
283
291
  if (pass.decision.action === "ask") {
284
292
  pass.decision = { ...pass.decision };
@@ -350,6 +358,11 @@ export async function runGateLanes(pass) {
350
358
  };
351
359
  }
352
360
  }
361
+ const skillHit = allowLayerMayClear() && pass.decision.action === "ask" && ruleMayClear() ? input.skillGrants?.allowing(pass.req, input.permissionMode?.()) : undefined;
362
+ if (skillHit !== undefined) {
363
+ pass.decision = skillAllowDecision(skillHit, pass.policyRewrite);
364
+ await notifier.notifyAsync(() => input.skillGrants?.onAllowed?.({ toolName: pass.req.toolName, toolCallId, ...skillHit }), "toolGate.skillGrantResolved");
365
+ }
353
366
  const originFacts = askOriginFacts(orgAnswer.org, ruleCallUnreadableOf(laneAnswer));
354
367
  if (pass.decision.action === "ask") {
355
368
  const stamped = { ...pass.decision, ruleEvidence: mintRuleEvidence(personalEvidence), ...(laneCoverage !== undefined ? { segmentCoverage: laneCoverage } : {}) };
@@ -703,14 +716,20 @@ export async function runGateLanes(pass) {
703
716
  break;
704
717
  }
705
718
  }
719
+ const editSkillRefusal = skillRowRefusal(input.skillGrants, { toolName, args: editArgs, toolCallId, ...(pass.req.face !== undefined ? { face: pass.req.face } : {}) });
720
+ if (editSkillRefusal !== undefined) {
721
+ editDenied = editSkillRefusal;
722
+ pass.deniedBy = "policy";
723
+ break;
724
+ }
706
725
  if (recheck.action === "allow") {
707
726
  if (recheck.updatedInput !== undefined) {
708
727
  editArgs = recheck.updatedInput;
709
728
  editRewrittenSinceHuman = true;
710
729
  }
711
- if (!editRewrittenSinceHuman || !asksUnstated(recheck, input.permissionSeat, input.permissionMode?.(), editHookAllowed && recheck.updatedInput === undefined))
730
+ if (!editRewrittenSinceHuman || !asksUnstated(recheck, permissionSeatAt(input, editArgs)?.seat, input.permissionMode?.(), editHookAllowed && recheck.updatedInput === undefined))
712
731
  break;
713
- recheck = { action: "ask", message: unstatedAskMessage(toolName) };
732
+ recheck = { action: "ask", message: permissionSeatAt(input, editArgs)?.question ?? unstatedAskMessage(toolName) };
714
733
  pass.tightenedBy = "unstated";
715
734
  }
716
735
  if (recheck.updatedInput !== undefined) {
@@ -136,7 +136,7 @@ export type NoticeAudience = "user" | "operator";
136
136
  * the union's catalog ({@link EngineNotice.code}). The groups below say only which family a row joins,
137
137
  * so the catalog is not written a second time here and the two cannot drift.
138
138
  */
139
- export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.context_settings_swapped", "config.read_face_swapped", "config.read_face_deployment_clamped", "config.write_face_swapped", "config.write_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "config.artifact_host_invalid", "config.task_root_not_canonical", "config.write_protection_target_view_absent", "config.write_protection_unresolved", "config.execution_env_capability_invalid", "config.tool_card_undeclared", "config.tool_face_undeclared", "config.tool_face_invalid", "config.forward_sink_failed", "config.durable_gate_unavailable", "config.peer_admission_out_of_range", "config.peer_lane_unmounted", "config.secret_env_scrubbed", "config.permission_mode_unavailable", "permission.ask_released_by_mode", "permission.read_deny_unapplied", "permission.rule_store_unavailable", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "classifier.fallback", "classifier.role_defaulted", "classifier.contract_broken", "classifier.unavailable", "checkpoint.execution_outcome_unrecorded", "delegation.transcript_integrity", "delegation.ask_unresolvable", "mcp.injection_dropped", "mcp.revocation_probe_failed", "mcp.server_redialed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "memory.restore_withheld", "memory.project_scope_absent", "route.fallback_to_primary", "route.classifier_unroutable", "route.base_url_changed_key_unchanged", "route.response_model_mismatch", "route.tool_call_id_folded", "context.tool_turn_repaired", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "task.interrupt_unconsumed", "task.tool_turn_incomplete", "approval.read_root_granted", "approval.read_root_grant_rejected", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "skills.listing_truncated", "agents.listing_truncated", "tool_result.offload_put_failed", "instructions.source_changed", "session.records_skipped", "memory.layer_locked_legacy", "server.model_unreachable"];
139
+ export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.context_settings_swapped", "config.read_face_swapped", "config.read_face_deployment_clamped", "config.write_face_swapped", "config.write_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "config.artifact_host_invalid", "config.task_root_not_canonical", "config.write_protection_target_view_absent", "config.write_protection_unresolved", "config.execution_env_capability_invalid", "config.tool_card_undeclared", "config.tool_face_undeclared", "config.tool_face_invalid", "config.forward_sink_failed", "config.durable_gate_unavailable", "config.peer_admission_out_of_range", "config.peer_lane_unmounted", "config.secret_env_scrubbed", "config.permission_mode_unavailable", "permission.ask_released_by_mode", "permission.read_deny_unapplied", "permission.rule_store_unavailable", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "classifier.fallback", "classifier.role_defaulted", "classifier.contract_broken", "classifier.unavailable", "checkpoint.execution_outcome_unrecorded", "delegation.transcript_integrity", "delegation.ask_unresolvable", "mcp.injection_dropped", "mcp.revocation_probe_failed", "mcp.server_redialed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "memory.restore_withheld", "memory.project_scope_absent", "route.fallback_to_primary", "route.classifier_unroutable", "route.base_url_changed_key_unchanged", "route.response_model_mismatch", "route.tool_call_id_folded", "context.tool_turn_repaired", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "task.interrupt_unconsumed", "task.tool_turn_incomplete", "approval.read_root_granted", "approval.read_root_grant_rejected", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "skills.listing_truncated", "skills.allowed_tool_not_granted", "agents.listing_truncated", "tool_result.offload_put_failed", "instructions.source_changed", "session.records_skipped", "memory.layer_locked_legacy", "server.model_unreachable"];
140
140
  /** A catalogued code (see {@link ENGINE_NOTICE_CODES}); {@link NOTICE_MINTER} says who mints it. NOT the type of
141
141
  * `EngineNotice.code`, which stays `string` — a host forwarding its own notices through the same
142
142
  * sink is a supported shape, and narrowing that field would break it. */
@@ -233,6 +233,7 @@ declare const NOTICE_AUDIENCE_TABLE: {
233
233
  readonly "route.tool_call_id_folded": "operator";
234
234
  readonly "context.tool_turn_repaired": "operator";
235
235
  readonly "skills.listing_truncated": "operator";
236
+ readonly "skills.allowed_tool_not_granted": "operator";
236
237
  readonly "agents.listing_truncated": "operator";
237
238
  readonly "tool_result.offload_put_failed": "operator";
238
239
  readonly "config.forward_sink_failed": "operator";
@@ -343,6 +344,7 @@ declare const NOTICE_MINTER_TABLE: {
343
344
  readonly "memory.capture_opted_out": "engine";
344
345
  readonly "memory.capture_optout_unpersisted": "engine";
345
346
  readonly "skills.listing_truncated": "engine";
347
+ readonly "skills.allowed_tool_not_granted": "engine";
346
348
  readonly "agents.listing_truncated": "engine";
347
349
  readonly "tool_result.offload_put_failed": "engine";
348
350
  readonly "instructions.source_changed": "server";
@@ -193,6 +193,7 @@ export const ENGINE_NOTICE_CODES = Object.freeze([
193
193
  "memory.capture_opted_out",
194
194
  "memory.capture_optout_unpersisted",
195
195
  "skills.listing_truncated",
196
+ "skills.allowed_tool_not_granted",
196
197
  "agents.listing_truncated",
197
198
  "tool_result.offload_put_failed",
198
199
  "instructions.source_changed",
@@ -279,6 +280,7 @@ const NOTICE_AUDIENCE_TABLE = {
279
280
  "route.tool_call_id_folded": "operator",
280
281
  "context.tool_turn_repaired": "operator",
281
282
  "skills.listing_truncated": "operator",
283
+ "skills.allowed_tool_not_granted": "operator",
282
284
  "agents.listing_truncated": "operator",
283
285
  "tool_result.offload_put_failed": "operator",
284
286
  "config.forward_sink_failed": "operator",
@@ -371,6 +373,7 @@ const NOTICE_MINTER_TABLE = {
371
373
  "memory.capture_opted_out": "engine",
372
374
  "memory.capture_optout_unpersisted": "engine",
373
375
  "skills.listing_truncated": "engine",
376
+ "skills.allowed_tool_not_granted": "engine",
374
377
  "agents.listing_truncated": "engine",
375
378
  "tool_result.offload_put_failed": "engine",
376
379
  "instructions.source_changed": "server",
@@ -2,6 +2,7 @@ import type { ActorAssertion, DocumentContent, ImageContent, TextContent } from
2
2
  import type { ExecutionEnv, FileError, Result, SessionTreeEntry } from "../internal/harness-types.js";
3
3
  import type { DecisionReason, PermissionResult, ResolvedAsk, ToolCallFace, ToolCallRequest, ToolPolicy } from "./tool-policy.js";
4
4
  import type { GateOutcome } from "./gate-outcome.js";
5
+ import { type PreToolUseScreening } from "./runner/gate-exit.js";
5
6
  export { normalizeOrgGateVerdict, normalizePersistedRuleHit } from "./gate-lanes.js";
6
7
  export { cloneObserverInput } from "./runner/gate-exit.js";
7
8
  import { type AskClass } from "./ask-class.js";
@@ -9,6 +10,9 @@ import { type PermissionMode } from "./ask-origin.js";
9
10
  import type { AutoModeDenialTracker } from "./auto-mode.js";
10
11
  export { formatHookFeedback } from "./reminder-mint.js";
11
12
  import type { WiringLegKind } from "./wiring-manifest.js";
13
+ import { type SafeNotifier } from "./safe-notify.js";
14
+ import type { PermissionSeatInputs } from "./mode-policy.js";
15
+ import type { SkillGateSeats } from "./skill-grant.js";
12
16
  /**
13
17
  * In-process hook seam (design/37) — a provider-agnostic interception layer modeled on CC's hooks,
14
18
  * kept process-internal (no shell/HTTP executors, no settings files). The original tool-call trio:
@@ -1128,7 +1132,7 @@ export type PersistedRuleAnswer = PersistedRuleHit | PersistedRuleCoverage | Per
1128
1132
  * signal; when the caller also supplies {@link ToolGateInput.callSignal}, the Runner's closures
1129
1133
  * additionally bind their waits to that per-call signal (`AbortSignal.any` of the two), so a turn
1130
1134
  * interrupt releases a pending ask instead of leaving it hanging on the run's lifetime alone. */
1131
- export interface ToolGateInput {
1135
+ export interface ToolGateInput extends PermissionSeatInputs, SkillGateSeats {
1132
1136
  /** RB-473 batch 2b — bounded disclosure for a HOST callback that throws inside this gate call's
1133
1137
  * isolation scope (onHookError itself, the deny observer, the durable-park observer). The runner
1134
1138
  * wires this to an `observer.notify_failed` trace event; absent = counted only (unchanged). */
@@ -1462,9 +1466,10 @@ export interface ToolGateInput {
1462
1466
  /** WHO put the tool's egress / irreversibility marks on the seat ({@link import("./ask-origin.js").ToolMarkSource});
1463
1467
  * absent or empty with a mark present reads as the deployment's own (fail-closed: an operator's mandate). */
1464
1468
  toolMarkSources?: ReadonlySet<import("./ask-origin.js").ToolMarkSource>;
1465
- /** The called tool's permission seat ({@link import("./tool-spec.js").ToolSpec.permissionSeat}): `"passthrough"` ⇒ an allow no layer stated
1466
- * meets the mode's answer for an unstated call ({@link import("./mode-policy.js").ModePolicy.unstated}). Absent ⇒ the tool answers for itself. */
1467
- permissionSeat?: "passthrough";
1469
+ /** The called tool's seat carries its server's declaration that a call must be put to a person
1470
+ * ({@link import("../engine/loop/types.js").AgentTool.mcpRequiresUserInteraction}): a surviving `allow` is tightened to an `ask`, and every ask on the call carries
1471
+ * the class `requires_user_interaction` and the real-approval bit (no rule, grant, classifier, bypass mode, blanket seat or policy-settled approver answers it). Absent ⇒ not declared. */
1472
+ requiresUserInteraction?: true;
1468
1473
  /** The deployment's A2A peer-isolation switch: a set switch makes the A2A egress default an operator mandate. */
1469
1474
  a2aIsolatePeers?: boolean;
1470
1475
  /** Observe a mode RELEASE (fired before the released allow leaves the arbitration point; `readUnestablished`: the read station could not establish what the call reads). Observe-only. */
@@ -1499,8 +1504,10 @@ export interface ToolGateInput {
1499
1504
  * this leg) and no durable park took the call, so the fail-closed deny STANDS. Fired once per such call
1500
1505
  * from the gate's single deny exit, after every park re-route has had its attempt, with the facts the
1501
1506
  * `delegation.ask_unresolvable` notice is composed from (`ask-unresolvable-notice.ts`);
1502
- * `parkLaneExisted` = a park lane was armed for this run. Observe-only: a throwing seat never alters
1503
- * the deny. Absent ⇒ the deny stands silently (a host driving the gate directly, no notice sink).
1507
+ * `parkLaneExisted` = a park lane was armed for this run; `remedy` = the caller-side change that would have let the
1508
+ * call run, present for the causes that say nobody could be asked (`approver_unavailable`, `no_approver_wired`).
1509
+ * Observe-only: a throwing seat never alters the deny. Absent ⇒ the deny stands silently (a host driving the gate
1510
+ * directly, no notice sink).
1504
1511
  */
1505
1512
  onAskUnresolvable?: (info: {
1506
1513
  toolName: string;
@@ -1784,6 +1791,13 @@ export declare function askCarryRowMembers(carry: AskCarry | undefined): {
1784
1791
  askClass?: import("./ask-origin.js").AskClass;
1785
1792
  classes?: readonly import("./ask-origin.js").AskClass[];
1786
1793
  };
1794
+ /**
1795
+ * The PreToolUse screening seat ({@link PreToolUseScreening}) for ONE call — seat runner, seat bound, per-screening
1796
+ * context and the fail-closed / deny sentences, built in ONE place for every site that screens a call: the gate's
1797
+ * phase 1 and approved-edit re-screen (through {@link runToolGate}'s pass) and the resumed leg's re-screen of an
1798
+ * approved call. Undefined when no face is wired. `hookSeatMs` is the caller's validated bound.
1799
+ */
1800
+ export declare function createPreToolUseScreening(preToolUse: Hooks["preToolUse"] | undefined, input: Pick<ToolGateInput, "event" | "trackedCwd" | "hookEnv" | "identity" | "onHookError" | "abortSignal">, hookSeatMs: number, notifier: SafeNotifier): PreToolUseScreening | undefined;
1787
1801
  /**
1788
1802
  * The design/37 **two-phase tool gate** — the single chokepoint that makes the load-bearing invariant
1789
1803
  * structural ("a hook's `allow` cannot bypass the policy's `deny`/`ask`"):
@@ -247,11 +247,11 @@ export function askCarryRowMembers(carry) {
247
247
  ...(classed !== undefined ? classed : {}),
248
248
  };
249
249
  }
250
- export async function runToolGate(input) {
251
- const { event, preToolUse, adjudicate, resolveAsk, suspendAsk } = input;
252
- const { toolCallId, toolName } = event;
253
- const callSignal = input.callSignal;
254
- const screenedName = wireOf("gate.preToolUse", event);
250
+ export function createPreToolUseScreening(preToolUse, input, hookSeatMs, notifier) {
251
+ if (preToolUse === undefined)
252
+ return undefined;
253
+ const { toolCallId, toolName } = input.event;
254
+ const screenedName = wireOf("gate.preToolUse", input.event);
255
255
  const hookCtx = (seatSignal) => ({
256
256
  toolCallId,
257
257
  toolName: screenedName,
@@ -263,38 +263,38 @@ export async function runToolGate(input) {
263
263
  ...(input.hookEnv !== undefined ? { env: input.hookEnv } : {}),
264
264
  ...(input.identity !== undefined ? { identity: input.identity } : {}),
265
265
  });
266
+ const bound = { timeoutMs: hookSeatMs, ...(input.abortSignal !== undefined ? { signal: input.abortSignal } : {}), abortEnds: true };
267
+ return {
268
+ screen: (args) => runHookSeat("preToolUse", bound, (sig) => preToolUse(screenedName, args, hookCtx(sig))),
269
+ screened: screenPreToolUseResult,
270
+ crashReason: preToolUseCrashReason,
271
+ unansweredReason: async (subject, cause) => {
272
+ if (cause === "timeout")
273
+ traceHookCrash(input, hookSeatExpiredError("preToolUse", hookSeatMs, cause, `${subject} was NOT executed (fail-closed) and the screening face was signalled to stop`), notifier);
274
+ return preToolUseUnansweredReason(subject, hookSeatMs, cause);
275
+ },
276
+ deniedReason: (answer) => decisionText(answer) ?? `tool "${toolName}" blocked by a PreToolUse hook`,
277
+ refusedReason: (answer) => String(answer.message ?? `the PreToolUse hook's result for "${toolName}" was refused by the engine`),
278
+ };
279
+ }
280
+ export async function runToolGate(input) {
281
+ const { event, preToolUse, adjudicate, resolveAsk, suspendAsk } = input;
282
+ const { toolCallId, toolName } = event;
283
+ const callSignal = input.callSignal;
266
284
  const modelInput = event.input;
267
285
  const notifier = createSafeNotifier(input.onNotifyError !== undefined ? { onError: input.onNotifyError } : undefined);
268
286
  const hookSeatMs = resolveHookTimeoutMs(input.hookTimeoutMs, (err) => traceHookCrash(input, err, notifier));
269
- const seatBound = (abortEnds) => ({
270
- timeoutMs: hookSeatMs,
271
- ...(input.abortSignal !== undefined ? { signal: input.abortSignal } : {}),
272
- ...(abortEnds ? { abortEnds: true } : {}),
273
- });
274
- const preToolUseUnansweredBlock = async (subject, cause) => {
275
- const reason = preToolUseUnansweredReason(subject, hookSeatMs, cause);
276
- if (cause === "timeout") {
277
- traceHookCrash(input, hookSeatExpiredError("preToolUse", hookSeatMs, cause, `${subject} was NOT executed (fail-closed) and the screening face was signalled to stop`), notifier);
278
- }
279
- return reason;
280
- };
287
+ const observerBound = { timeoutMs: hookSeatMs, ...(input.abortSignal !== undefined ? { signal: input.abortSignal } : {}) };
281
288
  const notifyPermissionDeniedSeat = async (payload) => {
282
289
  if (input.permissionDenied === undefined)
283
290
  return;
284
291
  await notifier.notifyAsync(async () => {
285
- const seat = await runHookSeat("permissionDenied", seatBound(false), (sig) => input.permissionDenied?.({ ...payload, signal: sig }));
292
+ const seat = await runHookSeat("permissionDenied", observerBound, (sig) => input.permissionDenied?.({ ...payload, signal: sig }));
286
293
  if (seat.expired)
287
294
  throw hookSeatExpiredError("permissionDenied", hookSeatMs, seat.cause, "the deny observation was abandoned; the deny itself is unchanged");
288
295
  }, "toolGate.permissionDenied");
289
296
  };
290
- const screening = preToolUse !== undefined
291
- ? {
292
- screen: (args) => runHookSeat("preToolUse", seatBound(true), (sig) => preToolUse(screenedName, args, hookCtx(sig))),
293
- screened: screenPreToolUseResult,
294
- crashReason: preToolUseCrashReason,
295
- unansweredReason: preToolUseUnansweredBlock,
296
- }
297
- : undefined;
297
+ const screening = createPreToolUseScreening(preToolUse, input, hookSeatMs, notifier);
298
298
  const pass = {
299
299
  input,
300
300
  event,
@@ -341,13 +341,13 @@ export async function runToolGate(input) {
341
341
  }
342
342
  if (r) {
343
343
  if (isEngineRefusal(r)) {
344
- return await engineFailClosedExit(pass, String(r.message ?? `the PreToolUse hook's result for "${toolName}" was refused by the engine`));
344
+ return await engineFailClosedExit(pass, screening.refusedReason(r));
345
345
  }
346
346
  if (r.additionalContext) {
347
347
  pass.preToolContext.push(r.additionalContext);
348
348
  }
349
349
  if (r.action === "deny") {
350
- return hookDenyExit(pass, r);
350
+ return hookDenyExit(pass, screening.deniedReason(r));
351
351
  }
352
352
  if (r.updatedInput !== undefined) {
353
353
  pass.currentInput = r.updatedInput;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The per-tool `_meta` declarations a server makes in its tools/list entry that the engine reads at intake: a result-size
3
+ * threshold (`anthropic/maxResultSizeChars`) and the requirement that a call be put to a person
4
+ * (`anthropic/requiresUserInteraction`). Pure readers over the entry's `_meta` — the intake (`mcp.ts`) stamps what they answer
5
+ * on the mounted tool.
6
+ */
7
+ /**
8
+ * F4-② (CC :320015/:320061): resolve a server's self-declared per-tool result-size threshold from its
9
+ * `_meta`. Non-numeric/non-finite/non-positive → undefined (declaration ignored); valid → capped at
10
+ * {@link MCP_META_RESULT_SIZE_CAP}. Exported for tests.
11
+ */
12
+ export declare function resolveMcpDeclaredResultSize(meta: Record<string, unknown> | undefined): number | undefined;
13
+ /**
14
+ * A server's own per-tool declaration that a call must be put to a person, read from its `_meta` by the reference
15
+ * implementation's rule: the boolean `true` declares it, nothing else does. `declared` is the seat fact the intake stamps on
16
+ * the mounted tool. A PRESENT value that is not a boolean (the string "true", the number 1, an object, null) is a declaration
17
+ * the server wrote and this reader cannot honour: `advisory` is the sentence for the operator warning lane saying so (the tool
18
+ * stays mounted and reads as undeclared), instead of letting a typo quietly read as "no interaction required". `false` and
19
+ * absence are well-formed and say nothing. Exported for tests.
20
+ */
21
+ export declare function resolveMcpRequiresUserInteraction(meta: unknown): {
22
+ declared: boolean;
23
+ advisory?: string;
24
+ };
@@ -0,0 +1,17 @@
1
+ const MCP_META_RESULT_SIZE_CAP = 500_000;
2
+ export function resolveMcpDeclaredResultSize(meta) {
3
+ const declared = meta?.["anthropic/maxResultSizeChars"];
4
+ if (typeof declared !== "number" || !Number.isFinite(declared) || declared <= 0)
5
+ return undefined;
6
+ return Math.min(declared, MCP_META_RESULT_SIZE_CAP);
7
+ }
8
+ const MCP_REQUIRES_USER_INTERACTION_META_KEY = "anthropic/requiresUserInteraction";
9
+ export function resolveMcpRequiresUserInteraction(meta) {
10
+ const value = meta?.[MCP_REQUIRES_USER_INTERACTION_META_KEY];
11
+ if (value === true)
12
+ return { declared: true };
13
+ if (value === undefined || value === false)
14
+ return { declared: false };
15
+ const kind = value === null ? "null" : Array.isArray(value) ? "an array" : typeof value === "object" ? "an object" : `a ${typeof value}`;
16
+ return { declared: false, advisory: `its _meta["${MCP_REQUIRES_USER_INTERACTION_META_KEY}"] is ${kind}, not the boolean true — the declaration is ignored, so this tool is judged like a server tool that did not declare it` };
17
+ }
@@ -5,6 +5,7 @@ import type { ImageContent, TextContent } from "../internal/llm.js";
5
5
  import { type McpImageResizer } from "./image-downsample.js";
6
6
  import type { McpServerSpec, OnElicit, ToolEffect } from "./types.js";
7
7
  import { type ReminderDisclosureCounts } from "./reminder-disclosure.js";
8
+ export { resolveMcpDeclaredResultSize } from "./mcp-tool-meta.js";
8
9
  /**
9
10
  * The safety axes (design/77 §4 irreversibility, design/70 egress) derived from one materialized MCP
10
11
  * tool's server-advertised `annotations`. These ride alongside the {@link AgentTool} (which is vendored
@@ -376,12 +377,6 @@ export interface McpServerStatus {
376
377
  * silently re-narrowed by a cast.
377
378
  */
378
379
  export declare const MCP_PREFIX: "mcp__";
379
- /**
380
- * F4-② (CC :320015/:320061): resolve a server's self-declared per-tool result-size threshold from its
381
- * `_meta`. Non-numeric/non-finite/non-positive → undefined (declaration ignored); valid → capped at
382
- * {@link MCP_META_RESULT_SIZE_CAP}. Exported for tests.
383
- */
384
- export declare function resolveMcpDeclaredResultSize(meta: Record<string, unknown> | undefined): number | undefined;
385
380
  /**
386
381
  * Apply the token gate to a mapped MCP content array (CC `$Mp`/`BMp` shape): text blocks consume a
387
382
  * cumulative char budget of `tokens×4`; past the budget they are sliced/dropped head-keep, and the CC
package/dist/core/mcp.js CHANGED
@@ -17,6 +17,8 @@ import { sliceHeadSafe, sliceTailSafe } from "./surrogate-safe-slice.js";
17
17
  import { truncateError } from "./tool-errors.js";
18
18
  import { delimitUntrusted, inlineUntrusted, sanitizeUntrustedText } from "./untrusted-text.js";
19
19
  import { discloseReminderShaped, observeReminderMarkEcho } from "./reminder-disclosure.js";
20
+ import { resolveMcpDeclaredResultSize, resolveMcpRequiresUserInteraction } from "./mcp-tool-meta.js";
21
+ export { resolveMcpDeclaredResultSize } from "./mcp-tool-meta.js";
20
22
  import { withContentOrigin } from "./memory-engine/content-origin.js";
21
23
  import { snapshotMcpEntries } from "./locked-config.js";
22
24
  import { validateJsonSchemaShape } from "./runner/strict-output-schema.js";
@@ -25,13 +27,6 @@ import { readMember, thrownIs, thrownName, thrownText } from "./thrown-value.js"
25
27
  export const MCP_PREFIX = MCP_NAMESPACE.prefix;
26
28
  const MCP_OUTPUT_TOKENS_DEFAULT = 25_000;
27
29
  const MCP_CHARS_PER_TOKEN = 4;
28
- const MCP_META_RESULT_SIZE_CAP = 500_000;
29
- export function resolveMcpDeclaredResultSize(meta) {
30
- const declared = meta?.["anthropic/maxResultSizeChars"];
31
- if (typeof declared !== "number" || !Number.isFinite(declared) || declared <= 0)
32
- return undefined;
33
- return Math.min(declared, MCP_META_RESULT_SIZE_CAP);
34
- }
35
30
  function mcpMaxOutputTokens() {
36
31
  return resolveEnvBudget(process.env.MAX_MCP_OUTPUT_TOKENS, { name: "MAX_MCP_OUTPUT_TOKENS", unit: "tokens", default: MCP_OUTPUT_TOKENS_DEFAULT });
37
32
  }
@@ -1663,6 +1658,9 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
1663
1658
  const writeEffect = (axis?.effect ?? "write") === "write";
1664
1659
  const mcpToolMeta = t._meta;
1665
1660
  const mcpMaxResultSizeChars = resolveMcpDeclaredResultSize(mcpToolMeta);
1661
+ const interaction = resolveMcpRequiresUserInteraction(mcpToolMeta);
1662
+ if (interaction.advisory !== undefined)
1663
+ advisories.push({ tool: inlineUntrusted(t.name), reason: interaction.advisory });
1666
1664
  const mcpAlwaysLoad = mcpToolMeta?.["anthropic/alwaysLoad"] === true;
1667
1665
  const mounted = {
1668
1666
  name: namespacedName,
@@ -1671,6 +1669,7 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
1671
1669
  parameters: (effectiveInputSchema ?? { type: "object" }),
1672
1670
  ...(mcpMaxResultSizeChars !== undefined ? { mcpMaxResultSizeChars } : {}),
1673
1671
  ...(mcpAlwaysLoad ? { mcpAlwaysLoad: true } : {}),
1672
+ ...(interaction.declared ? { mcpRequiresUserInteraction: true } : {}),
1674
1673
  mcpRemoteName: remoteName, ...(t.annotations?.readOnlyHint === true ? { mcpReadOnlyHint: true } : {}), permissionSeat: "passthrough",
1675
1674
  execute: async (_toolCallId, params, signal) => {
1676
1675
  const what = `The call to tool ${inlineUntrusted(remoteName)}`;
@@ -83,10 +83,35 @@ export declare function mountedToolDeclaresReadOnly(tools: readonly AgentTool[],
83
83
  export declare function asksUnstated(d: PermissionResult, seat: "passthrough" | undefined, mode: PermissionMode | undefined, hookStated: boolean): boolean;
84
84
  /** The unstated question's sentence (the reference implementation's passthrough sentence, without its product name). */
85
85
  export declare const unstatedAskMessage: (toolName: string) => string;
86
+ /** One call's permission seat: `passthrough` (an allow nobody stated meets the mode's unstated answer) and the question
87
+ * put when it does — absent ⇒ {@link unstatedAskMessage}. */
88
+ export interface PermissionSeatReading {
89
+ readonly seat: "passthrough";
90
+ readonly question?: string;
91
+ }
92
+ /** The gate's permission-seat inputs (a member group of `ToolGateInput`), read through {@link permissionSeatAt}. */
93
+ export interface PermissionSeatInputs {
94
+ /** The called tool's permission seat ({@link import("./tool-spec.js").ToolSpec.permissionSeat}): `"passthrough"` ⇒ an allow no layer stated
95
+ * meets the mode's answer for an unstated call ({@link ModePolicy.unstated}). Absent ⇒ the tool answers for itself. */
96
+ permissionSeat?: "passthrough";
97
+ /** The seat read off THIS call's arguments, for a tool whose seat depends on them (the Runner wires it for the `Skill`
98
+ * tool: a skill that declares a list asks `Execute skill: <name>`). Present ⇒ it is the reading. */
99
+ permissionSeatOf?: (args: unknown) => PermissionSeatReading | undefined;
100
+ }
101
+ /**
102
+ * The ONE reading of a call's permission seat, taken on the arguments the gate is judging (the policy's rewrite and the
103
+ * hook's included): the per-call reader where the tool's seat depends on its arguments, else the mounted tool's static
104
+ * seat. Read at the fold's last station and at the approved-edit round, on that round's edited bytes.
105
+ */
106
+ export declare function permissionSeatAt(input: PermissionSeatInputs, args: unknown): PermissionSeatReading | undefined;
86
107
  /** The PERMISSION SEAT of the LAST mounted instance of this name (the one a call dispatches to) — `passthrough` when the
87
108
  * instance states no permission of its own ({@link AgentTool.permissionSeat}), else `undefined` (the engine's stations and
88
109
  * the deployment's declarations answer for it). Read per call off the leg's live mount array, so a refresh is seen. */
89
110
  export declare function mountedToolPermissionSeat(tools: readonly AgentTool[], toolName: string): "passthrough" | undefined;
111
+ /** Does the LAST mounted instance of this name (the one a call dispatches to) carry its server's declaration that a call must be put to a
112
+ * person ({@link AgentTool.mcpRequiresUserInteraction})? Read per call off the leg's live mount array, so a refresh is seen. The gate's
113
+ * seat for the `requires_user_interaction` class. */
114
+ export declare function mountedToolRequiresUserInteraction(tools: readonly AgentTool[], toolName: string): boolean;
90
115
  /**
91
116
  * Is this call a WRITE the `plan` mode asks about? Two faces, one question — does the call write by its declaration:
92
117
  * · a covered path write (the catalog's protective floor: a file-write tool, or a same-name tool wearing its slot), and
@@ -46,9 +46,17 @@ export function asksUnstated(d, seat, mode, hookStated) {
46
46
  return d.action === "allow" && d.decisionReason === undefined && !hookStated && seat === "passthrough" && mode !== undefined && modePolicyOf(mode).unstated === "ask";
47
47
  }
48
48
  export const unstatedAskMessage = (toolName) => `The agent requested permission to use ${toolName}, but it has not been granted yet.`;
49
+ export function permissionSeatAt(input, args) {
50
+ if (input.permissionSeatOf !== undefined)
51
+ return input.permissionSeatOf(args);
52
+ return input.permissionSeat === "passthrough" ? { seat: "passthrough" } : undefined;
53
+ }
49
54
  export function mountedToolPermissionSeat(tools, toolName) {
50
55
  return tools.findLast((t) => t.name === toolName)?.permissionSeat === "passthrough" ? "passthrough" : undefined;
51
56
  }
57
+ export function mountedToolRequiresUserInteraction(tools, toolName) {
58
+ return tools.findLast((t) => t.name === toolName)?.mcpRequiresUserInteraction === true;
59
+ }
52
60
  const ENGINE_ROSTER_SOURCES = new Set(["builtin", "synthetic"]);
53
61
  export function planRefusesWrite(req, roster, readOnlyDeclared) {
54
62
  if (isProtectedWrite(req))
@@ -80,6 +80,9 @@ export declare const PROTOCOL_TABLE: readonly ProtocolNamespace[];
80
80
  * table rather than written out: a message that hard-codes ONE protocol becomes wrong — while staying
81
81
  * green — the moment a second one is appended, and both messages tell a deployment what to write. */
82
82
  export declare const NAMESPACED_NAME_SHAPES: string;
83
+ /** Whether `name` carries the namespace separator — reserved for protocol tool names: a deployment tool's name or alias
84
+ * carrying it is refused, and a rule name carrying it with no protocol prefix names no tool. The one spelling of the test. */
85
+ export declare function carriesNamespaceSeparator(name: string): boolean;
83
86
  /** The namespace owning `name`, or `undefined` for a caller/first-party tool. */
84
87
  export declare function protocolOf(name: string): ProtocolNamespace | undefined;
85
88
  export {};
@@ -38,6 +38,9 @@ export const MCP_NAMESPACE = makeProtocolNamespace("mcp", MCP_PREFIX_NAME);
38
38
  export const A2A_NAMESPACE = makeProtocolNamespace("a2a", A2A_PREFIX_NAME);
39
39
  export const PROTOCOL_TABLE = [MCP_NAMESPACE, A2A_NAMESPACE];
40
40
  export const NAMESPACED_NAME_SHAPES = PROTOCOL_TABLE.map((ns) => `${ns.prefix}<peer>__<tool>`).join(", ");
41
+ export function carriesNamespaceSeparator(name) {
42
+ return name.includes(NAME_SEP);
43
+ }
41
44
  export function protocolOf(name) {
42
45
  return PROTOCOL_TABLE.find((ns) => name.startsWith(ns.prefix));
43
46
  }
@@ -19,7 +19,8 @@
19
19
  * - **declare** ({@link reminderMarkDeclaration}) — the system-prompt declaration copy that gives
20
20
  * the mark its meaning to the model.
21
21
  * plus the tag renderers ({@link openSystemReminder} / {@link mintSystemReminder}) mint sites call
22
- * so no call site inlines the marking syntax.
22
+ * so no call site inlines the marking syntax, and the block readers ({@link markedReminderBodiesOf} /
23
+ * {@link stripMarkedReminderBlocks}) for the same reason on the reading side.
23
24
  *
24
25
  * SCHEME EVOLUTION CONTRACT (ruled 2026-08-19): the marking scheme is REPLACEABLE. The current
25
26
  * scheme is an open-tag attribute (`mark="<22-char base64url>"`), but nothing outside this module
@@ -116,3 +117,22 @@ export declare function engineRegionTextsOf(messages: readonly EngineRegionMessa
116
117
  * that must judge what the engine actually SAID reads block bodies and decides per block.
117
118
  */
118
119
  export declare function markedReminderBodiesOf(text: string, mark: string): string[];
120
+ /**
121
+ * STRIP port — the reading half of {@link openSystemReminder}: `text` without the blocks the engine marked.
122
+ *
123
+ * A marked block is an open tag whose mark attribute carries a mark of the current scheme, through the next close
124
+ * tag; the attribute's spacing and quote style are not part of that judgment (the engine mints one spelling, a model
125
+ * that relays a block rewrites it freely, and the mark is what must not travel). Everything in it
126
+ * goes, and so do the blank lines that stood between it and the text after it, so what followed a block starts
127
+ * where the block started and the rest of the text keeps its own lines. A marked open with no close (a result cut
128
+ * mid-block) goes to the end of the text — the open tag is the engine's own mint, and what follows it is the block.
129
+ *
130
+ * Only a tag carrying a valid mark is a block here. Reminder-shaped text without the mark, with a value the scheme
131
+ * does not produce, with whitespace around the `=` or another attribute beside the mark, or spelled in another case is data (the declaration says so) and is
132
+ * returned untouched; so is a text with no marked open at all — the same string back.
133
+ *
134
+ * For a caller that PROJECTS a tool result into a shorter view for a reader who does not hold the result's session
135
+ * (the parent of a child agent): the block is the engine's note to the context that ran the tool, not part of what
136
+ * the tool returned, and carries that context's mark.
137
+ */
138
+ export declare function stripMarkedReminderBlocks(text: string): string;
@@ -70,3 +70,23 @@ export function markedReminderBodiesOf(text, mark) {
70
70
  }
71
71
  return out;
72
72
  }
73
+ const MARKED_OPEN_TAG = /<system-reminder\s+mark=(["'])([^"'<>]*)\1\s*>/g;
74
+ const CLOSE_TAG = "</system-reminder>";
75
+ const BLANK_LINES = /(?:[ \t]*\r?\n)*/y;
76
+ export function stripMarkedReminderBlocks(text) {
77
+ let out = "";
78
+ let from = 0;
79
+ for (const open of text.matchAll(MARKED_OPEN_TAG)) {
80
+ const at = open.index;
81
+ if (at < from || !isValidReminderMark(open[2]))
82
+ continue;
83
+ out += text.slice(from, at);
84
+ const closeAt = text.indexOf(CLOSE_TAG, at + open[0].length);
85
+ if (closeAt < 0)
86
+ return out;
87
+ BLANK_LINES.lastIndex = closeAt + CLOSE_TAG.length;
88
+ BLANK_LINES.exec(text);
89
+ from = BLANK_LINES.lastIndex;
90
+ }
91
+ return from === 0 ? text : out + text.slice(from);
92
+ }
@@ -5,7 +5,8 @@
5
5
  * optional seat reads. Every retired-key door reads through here so no door grows its own idea of "present";
6
6
  * each composes its own refusal from the answer. A table row is `{ key: replacement }` — the sentence that says
7
7
  * what replaced the seat, quoted in the refusal. A dotted key names a member of a member bag (`attachments.x`),
8
- * read the same way one step at a time; a step that is not an object or a function holds nothing.
8
+ * read the same way one step at a time; a step that is not an object or a function holds nothing. A step spelled
9
+ * `name[]` names an array member and reads every element it holds (`skills[].manifest`: any element carrying it).
9
10
  */
10
11
  /** The first retired key present on `bag`, in table order, or undefined when none is. */
11
12
  export declare function retiredKeyOf(bag: object, table: Readonly<Record<string, string>>): {
@@ -1,7 +1,17 @@
1
+ function presentAt(at, steps) {
2
+ if (steps.length === 0)
3
+ return at !== undefined;
4
+ if (at === null || (typeof at !== "object" && typeof at !== "function"))
5
+ return false;
6
+ const [step, ...rest] = steps;
7
+ if (!step.endsWith("[]"))
8
+ return presentAt(Reflect.get(at, step), rest);
9
+ const items = Reflect.get(at, step.slice(0, -2));
10
+ return Array.isArray(items) && items.some((item) => presentAt(item, rest));
11
+ }
1
12
  export function retiredKeyOf(bag, table) {
2
13
  for (const [key, replacement] of Object.entries(table)) {
3
- const value = key.split(".").reduce((at, step) => (at !== null && (typeof at === "object" || typeof at === "function") ? Reflect.get(at, step) : undefined), bag);
4
- if (value !== undefined)
14
+ if (presentAt(bag, key.split(".")))
5
15
  return { key, replacement };
6
16
  }
7
17
  return undefined;
@@ -18,4 +28,5 @@ export const RETIRED_TASK_SPEC_KEYS = Object.freeze({
18
28
  autoModeRequested: 'the permission mode is one key now — write `permissionMode: "auto"`',
19
29
  approverPosture: 'a posture never authorized anything; drop the key (the engine renders the posture from the effective mode). Releasing asks is a separate decision: `permissionMode: "bypassPermissions"`',
20
30
  "attachments.planModeReminder": "the read-only planning posture is stated whenever the leg's mode is `plan` (first turn, every 5 turns, and an exit frame when it ends) — drop the key",
31
+ "skills[].manifest": "a skill's tools are its `allowedTools` / `disallowedTools` now — an allow-rule and a deny-rule source for the run the skill is activated in, never a fence; move `manifest.allowTools` to `allowedTools`. To fence tools write deny rules (or a delegated agent definition's `allowTools`); to fence write paths use session `allowDirs`",
21
32
  });
@@ -33,8 +33,8 @@ declare const RULE_SOURCE_PHRASE: {
33
33
  readonly named: "permission rule";
34
34
  };
35
35
  /**
36
- * The deny-narrowing fold as a WHOLE — session rules, inherited ancestor rules, the frozen constraint
37
- * projections and the active skill scope. Used where the refusing layer's own sentence is carried as the
36
+ * The deny-narrowing fold as a WHOLE — session rules, inherited ancestor rules and the frozen constraint
37
+ * projections. Used where the refusing layer's own sentence is carried as the
38
38
  * detail and the wrapper must not claim to know which of them spoke.
39
39
  */
40
40
  readonly narrowing: {