@sema-agent/core 7.41.0 → 7.42.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 (79) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/dist/agents/subagent.js +2 -0
  3. package/dist/core/agent-definition.d.ts +1 -1
  4. package/dist/core/allow-entry.d.ts +241 -0
  5. package/dist/core/allow-entry.js +340 -0
  6. package/dist/core/allow-rule-shadow.d.ts +18 -0
  7. package/dist/core/allow-rule-shadow.js +1 -0
  8. package/dist/core/ask-origin.d.ts +5 -5
  9. package/dist/core/ask-origin.js +1 -1
  10. package/dist/core/auto-mode.d.ts +3 -2
  11. package/dist/core/checkpoint-store.d.ts +11 -19
  12. package/dist/core/checkpoint-store.js +1 -1
  13. package/dist/core/decision-receipt.d.ts +4 -2
  14. package/dist/core/decision-receipt.js +1 -1
  15. package/dist/core/engine-notice.d.ts +8 -5
  16. package/dist/core/gate-fold.js +1 -1
  17. package/dist/core/gate-lanes.d.ts +1 -49
  18. package/dist/core/gate-lanes.js +85 -223
  19. package/dist/core/governance-codes.d.ts +3 -3
  20. package/dist/core/governance-codes.js +3 -3
  21. package/dist/core/hooks.d.ts +13 -10
  22. package/dist/core/hooks.js +5 -3
  23. package/dist/core/mode-policy.d.ts +3 -3
  24. package/dist/core/mode-policy.js +2 -2
  25. package/dist/core/permission-rules.d.ts +24 -23
  26. package/dist/core/permission-rules.js +37 -33
  27. package/dist/core/reminder-disclosure.d.ts +53 -2
  28. package/dist/core/reminder-disclosure.js +49 -36
  29. package/dist/core/rule-lane-reads.d.ts +84 -0
  30. package/dist/core/rule-lane-reads.js +164 -0
  31. package/dist/core/runner/contracts.d.ts +4 -2
  32. package/dist/core/runner/gate-exit.d.ts +3 -3
  33. package/dist/core/runner/image.js +4 -3
  34. package/dist/core/runner/inline-image.d.ts +14 -0
  35. package/dist/core/runner/inline-image.js +26 -0
  36. package/dist/core/runner/permission-rule-lanes.d.ts +3 -1
  37. package/dist/core/runner/permission-rule-lanes.js +1 -1
  38. package/dist/core/runner/prepare-ask-lane.d.ts +0 -19
  39. package/dist/core/runner/prepare-ask-lane.js +2 -2
  40. package/dist/core/runner/prepare-inherited-gate.d.ts +2 -0
  41. package/dist/core/runner/prepare-inherited-gate.js +1 -0
  42. package/dist/core/runner/prepare-park-ask.js +3 -3
  43. package/dist/core/runner/prepare-policy-chain.d.ts +12 -0
  44. package/dist/core/runner/prepare-policy-chain.js +10 -9
  45. package/dist/core/runner/prepare-project-context.d.ts +13 -3
  46. package/dist/core/runner/prepare-project-context.js +5 -4
  47. package/dist/core/runner/prepare-task.js +8 -5
  48. package/dist/core/runner/prepare-turn-wiring.d.ts +16 -3
  49. package/dist/core/runner/prepare-turn-wiring.js +19 -18
  50. package/dist/core/runner/run-leg.js +1 -1
  51. package/dist/core/runner/runtask.js +2 -1
  52. package/dist/core/runner-deps.d.ts +11 -0
  53. package/dist/core/skill-grant.d.ts +8 -47
  54. package/dist/core/skill-grant.js +15 -136
  55. package/dist/core/synthetic-read-verdict.d.ts +108 -0
  56. package/dist/core/synthetic-read-verdict.js +111 -0
  57. package/dist/core/task-spec.d.ts +18 -0
  58. package/dist/core/tool-name-readings.d.ts +1 -1
  59. package/dist/core/tool-name-readings.js +1 -1
  60. package/dist/core/tool-policy.d.ts +25 -24
  61. package/dist/core/tool-policy.js +26 -17
  62. package/dist/core/tool-spec.d.ts +7 -0
  63. package/dist/core/trace.d.ts +16 -0
  64. package/dist/index.d.ts +5 -1
  65. package/dist/index.js +4 -0
  66. package/dist/internal/harness-types.d.ts +1 -1
  67. package/dist/tools/fs/dir-entries.d.ts +69 -0
  68. package/dist/tools/fs/dir-entries.js +60 -0
  69. package/dist/tools/fs/fs-pdf.d.ts +7 -11
  70. package/dist/tools/fs/fs-pdf.js +5 -14
  71. package/dist/tools/fs/fs-read.d.ts +173 -2
  72. package/dist/tools/fs/fs-read.js +399 -286
  73. package/dist/tools/fs/image-admission.d.ts +51 -0
  74. package/dist/tools/fs/image-admission.js +36 -0
  75. package/dist/tools/fs/search.js +3 -7
  76. package/package.json +1 -1
  77. package/test/export-surface.snapshot.json +29 -1
  78. package/wire-changes/core@7.41.1.json +159 -0
  79. package/wire-changes/core@7.42.0.json +1107 -0
@@ -1,13 +1,13 @@
1
- import { attestClassifierDenyCause, classifierDenyCauseOf, decisionText, describeThrown, refuseOutOfContractDecision } from "./tool-policy.js";
2
- import { arbitrateByMode, classifyAsk, fileCommandOptionsSentence, mandateDisclosureOf, persistedRuleMandateOf, ruleClearanceOf, safetyAxisOf, stampAskOrigin } from "./ask-origin.js";
1
+ import { attestClassifierDenyCause, classifierDenyCauseOf, decisionText, refuseOutOfContractDecision } from "./tool-policy.js";
2
+ import { arbitrateByMode, classifyAsk, fileCommandOptionsSentence, persistedRuleMandateOf, ruleClearanceOf, safetyAxisOf, stampAskOrigin } from "./ask-origin.js";
3
3
  import { classifierParseFailureDenyMessage, denialLimitFallbackMessage, unarmedWindow } from "./auto-mode.js";
4
4
  import { classifierUnavailableArm } from "./classifier-unavailable-arm.js";
5
5
  import { SETTLEMENT_NOBODY_JUDGED } from "./gate-outcome.js";
6
6
  import { inlineUntrusted } from "./untrusted-text.js";
7
- import { isRuleBehavior } from "./permission-rule-model.js";
8
7
  import { ASK_USER_QUESTION_TOOL_NAME } from "./ask-question.js";
9
8
  import { applyPersistedTightening, disclosedRuleSet } from "./persisted-rule-arms.js";
10
9
  import { skillAllowDecision, skillRowRefusal } from "./skill-grant.js";
10
+ import { allowRuleShadowOf } from "./allow-entry.js";
11
11
  import { CD_GIT_COMPOUND_SENTENCE, changesDirectoryBeforeGit, readOnlyShellVerdict } from "./read-only-shell.js";
12
12
  import { readShellCommand } from "./shell-lexer.js";
13
13
  import { COMMAND_RULE_TOOL } from "./permission-rule-model.js";
@@ -15,146 +15,13 @@ import { catalogRuleFaceOf } from "./tool-registry.js";
15
15
  import { callFaceSeat } from "./tool-roster.js";
16
16
  import { heldBy, wireOf } from "./tool-name-readings.js";
17
17
  import { asksUnstated, modeAcceptsFileCommand, permissionSeatAt, unstatedAskMessage } from "./mode-policy.js";
18
- import { ORG_ADJUDICATION_TIMEOUT_MS, ORG_RULE_DECISION_REASON, ORG_UNAVAILABLE_DECISION_REASON, settleOrgVerdictWithin } from "./permission-rule-org.js";
18
+ import { ORG_RULE_DECISION_REASON, ORG_UNAVAILABLE_DECISION_REASON } from "./permission-rule-org.js";
19
19
  import { exitGate } from "./runner/gate-exit.js";
20
+ import { readOrgRuleLane, readPersistedRuleLane } from "./rule-lane-reads.js";
20
21
  import { traceHookCrash } from "./gate-crash-trace.js";
21
22
  import { createGateDeadline, awaitWriteProtectionJudgment, withGateTimeout } from "./gate-bounded-await.js";
22
23
  import { thrownText } from "./thrown-value.js";
23
24
  const QUESTION_TOOL_NAMES = new Set([ASK_USER_QUESTION_TOOL_NAME]);
24
- function isPlainOwnRecord(x) {
25
- try {
26
- if (x === null || typeof x !== "object" || Array.isArray(x))
27
- return false;
28
- const p = Object.getPrototypeOf(x);
29
- return p === Object.prototype || p === null;
30
- }
31
- catch {
32
- return false;
33
- }
34
- }
35
- function ownDataValue(obj, key) {
36
- try {
37
- const d = Object.getOwnPropertyDescriptor(obj, key);
38
- return d && "value" in d ? d.value : undefined;
39
- }
40
- catch {
41
- return undefined;
42
- }
43
- }
44
- export function normalizeOrgGateVerdict(answer, unreadable) {
45
- const clean = (v) => Object.assign(Object.create(null), v);
46
- const readable = (x) => {
47
- try {
48
- return x !== null && (typeof x === "object" || typeof x === "function") && !Array.isArray(x);
49
- }
50
- catch {
51
- return false;
52
- }
53
- };
54
- if (!readable(answer))
55
- return clean({ status: "unavailable", disclosures: [unreadable] });
56
- const status = ownDataValue(answer, "status");
57
- if (status === "unavailable") {
58
- const disclosures = ownDataValue(answer, "disclosures");
59
- let lines = [];
60
- if (Array.isArray(disclosures)) {
61
- try {
62
- for (let i = 0; i < disclosures.length; i++) {
63
- const d = disclosures[i];
64
- if (typeof d === "string")
65
- lines.push(d);
66
- }
67
- }
68
- catch {
69
- lines = [];
70
- }
71
- }
72
- return clean({ status: "unavailable", disclosures: lines.length > 0 ? lines : [unreadable] });
73
- }
74
- if (status !== "available")
75
- return clean({ status: "unavailable", disclosures: [unreadable] });
76
- const revision = ownDataValue(answer, "revision");
77
- const revisionCell = typeof revision === "number" && Number.isFinite(revision) ? { revision } : {};
78
- const verdict = ownDataValue(answer, "verdict");
79
- if (verdict === undefined)
80
- return clean({ status: "available", ...revisionCell });
81
- if (!readable(verdict))
82
- return clean({ status: "unavailable", disclosures: [unreadable] });
83
- const behavior = ownDataValue(verdict, "behavior");
84
- const rule = ownDataValue(verdict, "rule");
85
- if ((behavior !== "deny" && behavior !== "ask") || typeof rule !== "string")
86
- return clean({ status: "unavailable", disclosures: [unreadable] });
87
- const reason = ownDataValue(verdict, "unreadable");
88
- const unreadableCell = behavior === "ask" && typeof reason === "string" ? { unreadable: reason } : {};
89
- return clean({ status: "available", verdict: clean({ behavior: behavior, rule, ...unreadableCell }), ...revisionCell });
90
- }
91
- function copiedUsableDots(dots) {
92
- let copied;
93
- try {
94
- if (Array.isArray(dots)) {
95
- const walked = [];
96
- for (let i = 0; i < dots.length; i++) {
97
- const d = dots[i];
98
- walked.push(isPlainOwnRecord(d) ? { actor: ownDataValue(d, "actor"), counter: ownDataValue(d, "counter") } : { actor: undefined, counter: undefined });
99
- }
100
- copied = walked;
101
- }
102
- }
103
- catch {
104
- copied = undefined;
105
- }
106
- const usable = copied?.every((d) => typeof d.actor === "string" && typeof d.counter === "number" && Number.isFinite(d.counter)) === true && copied.length > 0;
107
- return usable ? { dots: copied } : {};
108
- }
109
- export function normalizePersistedRuleHit(hit) {
110
- if (!isPlainOwnRecord(hit))
111
- return {};
112
- if (ownDataValue(hit, "unreadable") === true) {
113
- const reason = ownDataValue(hit, "reason");
114
- return typeof reason === "string" ? { unreadable: true, reason } : { unreadable: true };
115
- }
116
- const rulesRaw = ownDataValue(hit, "rules");
117
- if (rulesRaw === undefined) {
118
- const coverageRaw = ownDataValue(hit, "segmentCoverage");
119
- let rows;
120
- try {
121
- if (Array.isArray(coverageRaw)) {
122
- const walked = [];
123
- for (let i = 0; i < coverageRaw.length; i++) {
124
- const r = coverageRaw[i];
125
- walked.push(isPlainOwnRecord(r) ? { segment: ownDataValue(r, "segment"), covered: ownDataValue(r, "covered") } : { segment: undefined, covered: undefined });
126
- }
127
- rows = walked.every((r) => typeof r.segment === "string" && typeof r.covered === "boolean") ? walked : undefined;
128
- }
129
- }
130
- catch {
131
- rows = undefined;
132
- }
133
- return rows !== undefined && rows.length > 0 ? { coverage: rows.map((r) => ({ segment: r.segment, covered: r.covered })) } : {};
134
- }
135
- let members;
136
- try {
137
- if (Array.isArray(rulesRaw)) {
138
- const walked = [];
139
- for (let i = 0; i < rulesRaw.length; i++) {
140
- const m = rulesRaw[i];
141
- walked.push(isPlainOwnRecord(m) ? { rule: ownDataValue(m, "rule"), dots: ownDataValue(m, "dots") } : { rule: undefined, dots: undefined });
142
- }
143
- members = walked;
144
- }
145
- }
146
- catch {
147
- members = undefined;
148
- }
149
- if (members === undefined || members.length === 0)
150
- return {};
151
- if (!members.every((m) => typeof m.rule === "string"))
152
- return {};
153
- const behavior = ownDataValue(hit, "behavior");
154
- if (!isRuleBehavior(behavior))
155
- return {};
156
- return { hit: { behavior, rules: members.map((m) => ({ rule: m.rule, ...copiedUsableDots(m.dots) })) } };
157
- }
158
25
  function parkWith(suspendAsk, parkArgs, carry) {
159
26
  return suspendAsk(...parkArgs, carry);
160
27
  }
@@ -200,21 +67,7 @@ export async function runGateLanes(pass) {
200
67
  if (wireOf("lanes.orgContentAskExemption", pass.req) === ASK_USER_QUESTION_TOOL_NAME && input.orgRules.contentAskToolMounted === true)
201
68
  return { decision: current, org: undefined };
202
69
  let decided = current;
203
- let pending;
204
- try {
205
- pending = Promise.resolve(input.orgRules.adjudicate({ toolName, args, toolCallId, ...callFaceSeat(pass.req.face) }));
206
- }
207
- catch (err) {
208
- pending = Promise.resolve({ status: "unavailable", disclosures: [`the org adjudication face threw before answering: ${describeThrown(err)}`] });
209
- }
210
- const raw = await settleOrgVerdictWithin(pending.catch((err) => ({
211
- status: "unavailable",
212
- disclosures: [`the org adjudication face threw: ${describeThrown(err)}`],
213
- })), {
214
- status: "unavailable",
215
- disclosures: [`the org adjudication face did not answer within ${ORG_ADJUDICATION_TIMEOUT_MS}ms (or the task ended first)`],
216
- }, { ...(input.abortSignal !== undefined ? { signal: input.abortSignal } : {}), timeoutMs: ORG_ADJUDICATION_TIMEOUT_MS });
217
- const answer = normalizeOrgGateVerdict(raw, "the org adjudication face answered something this gate cannot read as a verdict — treated as unreadable governance, never as an empty one");
70
+ const answer = await readOrgRuleLane(input.orgRules, { toolName, args, toolCallId, ...callFaceSeat(pass.req.face) }, input.abortSignal);
218
71
  if (answer.status === "unavailable") {
219
72
  orgEvidence = { revisionAbsent: "unavailable", ruleAbsent: "unavailable" };
220
73
  orgRealApprovalRequired = true;
@@ -262,16 +115,7 @@ export async function runGateLanes(pass) {
262
115
  const orgAnswer = await applyOrgLayer(pass.decision, pass.policyRewrite !== undefined ? pass.policyRewrite : pass.currentInput);
263
116
  pass.decision = orgAnswer.decision;
264
117
  const ruleCallUnreadableOf = (lane) => lane?.unreadable === true && lane.reason !== undefined;
265
- const readPersistedRules = async (req) => {
266
- let pendingHit;
267
- try {
268
- pendingHit = Promise.resolve(input.persistedRules?.adjudicate(req));
269
- }
270
- catch {
271
- return { unreadable: true };
272
- }
273
- return await settleOrgVerdictWithin(pendingHit.then(normalizePersistedRuleHit).catch(() => ({ unreadable: true })), { unreadable: true }, { ...(input.abortSignal !== undefined ? { signal: input.abortSignal } : {}), timeoutMs: ORG_ADJUDICATION_TIMEOUT_MS });
274
- };
118
+ const readPersistedRules = (req) => readPersistedRuleLane(input.persistedRules, req, input.abortSignal);
275
119
  const laneReq = { ...pass.req, ...(pass.policyRewrite !== undefined ? { args: pass.policyRewrite } : {}) };
276
120
  const laneAnswer = input.persistedRules !== undefined && wireOf("lanes.persistedLaneSkip", pass.req) !== ASK_USER_QUESTION_TOOL_NAME ? await readPersistedRules(laneReq) : undefined;
277
121
  {
@@ -294,81 +138,101 @@ export async function runGateLanes(pass) {
294
138
  pass.currentInput = pass.policyRewrite;
295
139
  pass.req.args = pass.policyRewrite;
296
140
  }
297
- const persistedRuleMandate = persistedRuleMandateOf({
298
- ...mandateMarksNow(),
299
- probeMandated: pass.decision.action === "ask" && pass.decision.probeMandated === true,
300
- });
301
141
  const ruleMayClear = () => ruleClearanceOf(pass.decision.action === "ask" ? pass.decision : undefined, mandateMarksNow()).ruleMayClear;
142
+ const classMayClear = () => pass.decision.action === "ask" && classifyAsk(pass.decision, askOriginFacts(orgAnswer.org, ruleCallUnreadableOf(laneAnswer))).row.ruleMayClear;
302
143
  let personalEvidence = { dotsAbsent: input.persistedRules === undefined ? "not_wired" : "not_adjudicated" };
303
144
  if (laneAnswer?.unreadable === true)
304
145
  personalEvidence = { dotsAbsent: "unavailable" };
305
146
  let laneCoverage;
147
+ const questionTool = wireOf("lanes.allowLayerQuestionSkip", pass.req) === ASK_USER_QUESTION_TOOL_NAME;
306
148
  const allowLayerMayClear = () => !orgRealApprovalRequired &&
307
149
  pass.decision.action === "ask" &&
308
150
  pass.decision.requiresRealApproval !== true &&
309
151
  pass.decision.decisionReason !== "hook" &&
310
- wireOf("lanes.allowLayerQuestionSkip", pass.req) !== ASK_USER_QUESTION_TOOL_NAME &&
152
+ !questionTool &&
311
153
  !markedUnresolvable();
154
+ const unionDotsOf = (rules) => {
155
+ const seen = new Set();
156
+ const union = [];
157
+ for (const r of rules) {
158
+ if (r.dots === undefined || r.dots.length === 0)
159
+ return { dotsAbsent: "not_reported" };
160
+ for (const d of r.dots) {
161
+ const key = JSON.stringify([d.actor, d.counter]);
162
+ if (!seen.has(key))
163
+ (seen.add(key), union.push(d));
164
+ }
165
+ }
166
+ return { dots: union };
167
+ };
312
168
  if (input.persistedRules && allowLayerMayClear()) {
313
169
  const answer = laneAnswer ?? {};
314
- const hitRules = answer.hit?.behavior === "allow" ? answer.hit.rules : undefined;
315
170
  laneCoverage = answer.coverage;
316
- personalEvidence = { dotsAbsent: answer.unreadable === true ? "unavailable" : "no_match" };
317
- const unionDotsOf = (rules) => {
318
- const seen = new Set();
319
- const union = [];
320
- for (const r of rules) {
321
- if (r.dots === undefined || r.dots.length === 0)
322
- return { dotsAbsent: "not_reported" };
323
- for (const d of r.dots) {
324
- const key = JSON.stringify([d.actor, d.counter]);
325
- if (!seen.has(key)) {
326
- seen.add(key);
327
- union.push(d);
328
- }
329
- }
171
+ personalEvidence = answer.hit?.behavior === "ask" ? unionDotsOf(answer.hit.rules) : { dotsAbsent: answer.unreadable === true ? "unavailable" : "no_match" };
172
+ }
173
+ const afterRuleClear = () => {
174
+ if (input.permissionMode === undefined || input.permissionMode() === "auto")
175
+ input.autoMode?.denialTracking?.recordAllow();
176
+ };
177
+ const mode = input.permissionMode?.();
178
+ const rewrite = pass.policyRewrite !== undefined ? { updatedInput: pass.policyRewrite } : {};
179
+ const listAllow = (decisionReason, whose, entries) => ({
180
+ action: "allow",
181
+ message: `${whose} allowedTools (${entries.map((e) => inlineUntrusted(e, 200)).join(", ")}) ${entries.length === 1 ? "covers" : "cover"} this call`,
182
+ decisionReason,
183
+ ...rewrite,
184
+ });
185
+ const storedAllows = laneAnswer?.hit?.behavior === "allow" ? laneAnswer.hit.rules : undefined;
186
+ const members = [
187
+ ["hook", () => (pass.hookAllowed === true && pass.policyRewrite === undefined ? { entries: [], clear: () => ({ action: "allow", message: "a PreToolUse hook allowed this call", decisionReason: "hook" }), afterClear: async () => undefined } : undefined)],
188
+ ["deployment", () => {
189
+ const hit = input.deploymentAllows?.allowing(pass.req, mode);
190
+ return hit && { entries: hit.entries, clear: () => listAllow("deployment_rule", "the deployment's", hit.entries), afterClear: () => notifier.notifyAsync(() => input.deploymentAllows?.onAllowed?.({ toolName: pass.req.toolName, toolCallId, entries: hit.entries }), "toolGate.allowedToolsResolved") };
191
+ }],
192
+ ["persisted", () => storedAllows && {
193
+ entries: storedAllows.map((r) => r.rule),
194
+ clear: () => ({ action: "allow", message: storedAllows.length === 1 ? `a persisted allow rule (${disclosedRuleSet(storedAllows)}) covers this call` : `persisted allow rules (${disclosedRuleSet(storedAllows)}) cover every segment of this call`, decisionReason: "persisted_rule", ...rewrite }),
195
+ afterClear: () => notifier.notifyAsync(() => input.persistedRules?.onResolved?.({ toolName: pass.req.toolName, toolCallId, rules: storedAllows.map((r) => r.rule) }), "toolGate.persistedRuleResolved"),
196
+ shadowed: () => (personalEvidence = unionDotsOf(storedAllows)),
197
+ }],
198
+ ["skill", () => {
199
+ const hit = input.skillGrants?.allowing(pass.req, mode);
200
+ return hit && { entries: hit.entries, clear: () => skillAllowDecision(hit, pass.policyRewrite), afterClear: () => notifier.notifyAsync(() => input.skillGrants?.onAllowed?.({ toolName: pass.req.toolName, toolCallId, ...hit }), "toolGate.skillGrantResolved") };
201
+ }],
202
+ ["request", () => {
203
+ const hit = input.requestAllows?.allowing(pass.req, mode);
204
+ return hit && { entries: hit.entries, clear: () => listAllow("request_rule", "this run's", hit.entries), afterClear: () => notifier.notifyAsync(() => input.requestAllows?.onAllowed?.({ toolName: pass.req.toolName, toolCallId, entries: hit.entries }), "toolGate.allowedToolsResolved") };
205
+ }],
206
+ ];
207
+ const shadows = [];
208
+ if (!questionTool) {
209
+ for (const [member, match] of members) {
210
+ if (pass.decision.action !== "ask")
211
+ break;
212
+ const hit = match();
213
+ if (hit === undefined)
214
+ continue;
215
+ if (allowLayerMayClear() && ruleMayClear() && classMayClear()) {
216
+ pass.decision = hit.clear();
217
+ await hit.afterClear();
218
+ if (member !== "hook")
219
+ afterRuleClear();
220
+ break;
330
221
  }
331
- return { dots: union };
332
- };
333
- if (answer.hit?.behavior === "ask")
334
- personalEvidence = unionDotsOf(answer.hit.rules);
335
- if (hitRules !== undefined && ruleMayClear()) {
336
- pass.decision = {
337
- action: "allow",
338
- message: hitRules.length === 1
339
- ? `a persisted allow rule (${disclosedRuleSet(hitRules)}) covers this call`
340
- : `persisted allow rules (${disclosedRuleSet(hitRules)}) cover every segment of this call`,
341
- decisionReason: "persisted_rule",
342
- ...(pass.policyRewrite !== undefined ? { updatedInput: pass.policyRewrite } : {}),
343
- };
344
- await notifier.notifyAsync(() => input.persistedRules?.onResolved?.({ toolName: pass.req.toolName, toolCallId, rules: hitRules.map((r) => r.rule) }), "toolGate.persistedRuleResolved");
345
- }
346
- else if (hitRules !== undefined) {
347
- personalEvidence = unionDotsOf(hitRules);
348
- const shownRule = disclosedRuleSet(hitRules);
349
- const mandateNoun = persistedRuleMandate === undefined && pass.decision.matchedAskRule === undefined
350
- ? "this call arrived already marked as requiring confirmation (an earlier layer of this run, or the task that delegated it, judged it so) — it is cleared by confirming this call, never by a standing allow rule"
351
- : mandateDisclosureOf(persistedRuleMandate);
352
- pass.decision = {
353
- ...pass.decision,
354
- persistedRuleShadowed: shownRule,
355
- message: hitRules.length === 1
356
- ? `${pass.decision.message !== undefined ? `${pass.decision.message} ` : ""}(a persisted allow rule (${shownRule}) matches this call but does not clear the ask — ${mandateNoun})`
357
- : `${pass.decision.message !== undefined ? `${pass.decision.message} ` : ""}(persisted allow rules (${shownRule}) match this call but do not clear the ask — ${mandateNoun})`,
358
- };
222
+ shadows.push(allowRuleShadowOf(member, hit.entries));
223
+ hit.shadowed?.();
359
224
  }
360
225
  }
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");
226
+ if (pass.decision.action === "ask") {
227
+ const { allowRuleShadowed: _claimed, ...standing } = pass.decision;
228
+ pass.decision = shadows.length > 0 ? { ...standing, allowRuleShadowed: shadows } : standing;
365
229
  }
366
230
  const originFacts = askOriginFacts(orgAnswer.org, ruleCallUnreadableOf(laneAnswer));
367
231
  if (pass.decision.action === "ask") {
368
232
  const stamped = { ...pass.decision, ruleEvidence: mintRuleEvidence(personalEvidence), ...(laneCoverage !== undefined ? { segmentCoverage: laneCoverage } : {}) };
369
233
  pass.decision = stampAskOrigin(stamped, originFacts, classifyStanding(stamped, originFacts));
370
234
  }
371
- if (allowLayerMayClear() && pass.decision.action === "ask" && ruleMayClear() && pass.probeBoundaryDeclined !== true && wireOf("lanes.readOnlyShellAllow", pass.req) === COMMAND_RULE_TOOL) {
235
+ if (allowLayerMayClear() && pass.decision.action === "ask" && ruleMayClear() && classMayClear() && pass.probeBoundaryDeclined !== true && wireOf("lanes.readOnlyShellAllow", pass.req) === COMMAND_RULE_TOOL) {
372
236
  const commandParam = catalogRuleFaceOf(COMMAND_RULE_TOOL)?.primaryParams[0] ?? "command";
373
237
  const args = pass.req.args;
374
238
  const command = typeof args === "object" && args !== null && !Array.isArray(args) ? args[commandParam] : undefined;
@@ -382,6 +246,7 @@ export async function runGateLanes(pass) {
382
246
  ...(pass.policyRewrite !== undefined ? { updatedInput: pass.policyRewrite } : {}),
383
247
  };
384
248
  await notifier.notifyAsync(() => input.onReadOnlyAllowed?.({ toolName: pass.req.toolName, toolCallId, command }), "toolGate.readOnlyAllowed");
249
+ afterRuleClear();
385
250
  }
386
251
  else if (shape !== undefined && pass.tightenedBy === "shell_gate" && changesDirectoryBeforeGit(shape, input.trackedCwd?.())) {
387
252
  pass.decision = { ...pass.decision, message: CD_GIT_COMPOUND_SENTENCE };
@@ -530,7 +395,7 @@ export async function runGateLanes(pass) {
530
395
  ...(d.action === "ask" && d.authoredBy === "deployment" ? { authoredBy: "deployment" } : {}),
531
396
  });
532
397
  if (suspendAsk && pass.decision.action === "ask") {
533
- const parkArgs = [pass.req, pass.currentInput, safety, undefined, realApprovalOf(pass.decision), pass.decision.action === "ask" ? pass.decision.persistedRuleShadowed : undefined, pass.decision.action === "ask" ? pass.decision.decisionReason : undefined, pass.decision.action === "ask" ? pass.decision.probeReason : undefined, pass.decision.action === "ask" ? pass.decision.probeCause : undefined, pass.decision.action === "ask" ? pass.decision.segmentCoverage : undefined, pass.decision.action === "ask" ? pass.decision.matchedAskRule : undefined, pass.decision.action === "ask" ? pass.decision.probeMandated : undefined];
398
+ const parkArgs = [pass.req, pass.currentInput, safety, undefined, realApprovalOf(pass.decision), pass.decision.action === "ask" ? pass.decision.allowRuleShadowed : undefined, pass.decision.action === "ask" ? pass.decision.decisionReason : undefined, pass.decision.action === "ask" ? pass.decision.probeReason : undefined, pass.decision.action === "ask" ? pass.decision.probeCause : undefined, pass.decision.action === "ask" ? pass.decision.segmentCoverage : undefined, pass.decision.action === "ask" ? pass.decision.matchedAskRule : undefined, pass.decision.action === "ask" ? pass.decision.probeMandated : undefined];
534
399
  const suspended = await parkWith(suspendAsk, parkArgs, carryOf(pass.decision));
535
400
  if (suspended) {
536
401
  if ("parkFailed" in suspended)
@@ -550,7 +415,7 @@ export async function runGateLanes(pass) {
550
415
  pass.req.args = outcome.presentedInput;
551
416
  }
552
417
  if (suspendAsk && outcome.parkDeclined && pass.parkFailed === undefined) {
553
- const parkArgs = [pass.req, pass.currentInput, safety, true, realApprovalOf(pass.decision), pass.decision.action === "ask" ? pass.decision.persistedRuleShadowed : undefined, pass.decision.action === "ask" ? pass.decision.decisionReason : undefined, pass.decision.action === "ask" ? pass.decision.probeReason : undefined, pass.decision.action === "ask" ? pass.decision.probeCause : undefined, pass.decision.action === "ask" ? pass.decision.segmentCoverage : undefined, pass.decision.action === "ask" ? pass.decision.matchedAskRule : undefined, pass.decision.action === "ask" ? pass.decision.probeMandated : undefined];
418
+ const parkArgs = [pass.req, pass.currentInput, safety, true, realApprovalOf(pass.decision), pass.decision.action === "ask" ? pass.decision.allowRuleShadowed : undefined, pass.decision.action === "ask" ? pass.decision.decisionReason : undefined, pass.decision.action === "ask" ? pass.decision.probeReason : undefined, pass.decision.action === "ask" ? pass.decision.probeCause : undefined, pass.decision.action === "ask" ? pass.decision.segmentCoverage : undefined, pass.decision.action === "ask" ? pass.decision.matchedAskRule : undefined, pass.decision.action === "ask" ? pass.decision.probeMandated : undefined];
554
419
  const suspended = await parkWith(suspendAsk, parkArgs, carryOf(pass.decision));
555
420
  if (suspended) {
556
421
  if ("parkFailed" in suspended)
@@ -586,7 +451,7 @@ export async function runGateLanes(pass) {
586
451
  pass.deniedBy = "ask_resolution";
587
452
  pass.decision = resolved;
588
453
  if (resolved.action === "deny" && resolved.approverUnavailable === true && suspendAsk && pass.parkFailed === undefined) {
589
- const parkArgs = [pass.req, pass.currentInput, safety, true, realApprovalOf(askBeforeResolve), askBeforeResolve.action === "ask" ? askBeforeResolve.persistedRuleShadowed : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.decisionReason : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.probeReason : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.probeCause : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.segmentCoverage : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.matchedAskRule : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.probeMandated : undefined];
454
+ const parkArgs = [pass.req, pass.currentInput, safety, true, realApprovalOf(askBeforeResolve), askBeforeResolve.action === "ask" ? askBeforeResolve.allowRuleShadowed : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.decisionReason : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.probeReason : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.probeCause : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.segmentCoverage : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.matchedAskRule : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.probeMandated : undefined];
590
455
  const suspended = await parkWith(suspendAsk, parkArgs, carryOf(askBeforeResolve));
591
456
  if (suspended) {
592
457
  if ("parkFailed" in suspended)
@@ -613,7 +478,6 @@ export async function runGateLanes(pass) {
613
478
  let editArgs = pass.decision.updatedInput;
614
479
  let editDenied;
615
480
  let editRewrittenSinceHuman = false;
616
- let editHookAllowed = false;
617
481
  for (let round = 0;; round++) {
618
482
  if (round >= 3) {
619
483
  editDenied = {
@@ -623,7 +487,6 @@ export async function runGateLanes(pass) {
623
487
  pass.deniedBy = "policy";
624
488
  break;
625
489
  }
626
- editHookAllowed = false;
627
490
  if (screening !== undefined) {
628
491
  let hr;
629
492
  let hrUnanswered;
@@ -665,7 +528,6 @@ export async function runGateLanes(pass) {
665
528
  editArgs = hr.updatedInput;
666
529
  editRewrittenSinceHuman = true;
667
530
  }
668
- editHookAllowed = hr.action === "allow";
669
531
  }
670
532
  }
671
533
  let recheck = { action: "allow" };
@@ -727,7 +589,7 @@ export async function runGateLanes(pass) {
727
589
  editArgs = recheck.updatedInput;
728
590
  editRewrittenSinceHuman = true;
729
591
  }
730
- if (!editRewrittenSinceHuman || !asksUnstated(recheck, permissionSeatAt(input, editArgs)?.seat, input.permissionMode?.(), editHookAllowed && recheck.updatedInput === undefined))
592
+ if (!editRewrittenSinceHuman || !asksUnstated(recheck, permissionSeatAt(input, editArgs)?.seat, input.permissionMode?.()))
731
593
  break;
732
594
  recheck = { action: "ask", message: permissionSeatAt(input, editArgs)?.question ?? unstatedAskMessage(toolName) };
733
595
  pass.tightenedBy = "unstated";
@@ -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", "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"];
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", "permission.allowed_tool_not_granted", "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"];
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. */
@@ -181,6 +181,7 @@ declare const NOTICE_AUDIENCE_TABLE: {
181
181
  readonly "permission.ask_released_by_mode": "user";
182
182
  readonly "permission.read_deny_unapplied": "user";
183
183
  readonly "permission.rule_store_unavailable": "user";
184
+ readonly "permission.allowed_tool_not_granted": "user";
184
185
  readonly "memory.capture_opted_out": "user";
185
186
  readonly "memory.capture_optout_unpersisted": "user";
186
187
  readonly "memory.consolidation_withheld": "operator";
@@ -233,7 +234,6 @@ declare const NOTICE_AUDIENCE_TABLE: {
233
234
  readonly "route.tool_call_id_folded": "operator";
234
235
  readonly "context.tool_turn_repaired": "operator";
235
236
  readonly "skills.listing_truncated": "operator";
236
- readonly "skills.allowed_tool_not_granted": "operator";
237
237
  readonly "agents.listing_truncated": "operator";
238
238
  readonly "tool_result.offload_put_failed": "operator";
239
239
  readonly "config.forward_sink_failed": "operator";
@@ -294,6 +294,7 @@ declare const NOTICE_MINTER_TABLE: {
294
294
  readonly "permission.ask_released_by_mode": "engine";
295
295
  readonly "permission.read_deny_unapplied": "engine";
296
296
  readonly "permission.rule_store_unavailable": "engine";
297
+ readonly "permission.allowed_tool_not_granted": "engine";
297
298
  readonly "peer.inbound_disposition": "engine";
298
299
  readonly "peer.held_settled": "engine";
299
300
  readonly "peer.idle_subscription": "engine";
@@ -344,7 +345,6 @@ declare const NOTICE_MINTER_TABLE: {
344
345
  readonly "memory.capture_opted_out": "engine";
345
346
  readonly "memory.capture_optout_unpersisted": "engine";
346
347
  readonly "skills.listing_truncated": "engine";
347
- readonly "skills.allowed_tool_not_granted": "engine";
348
348
  readonly "agents.listing_truncated": "engine";
349
349
  readonly "tool_result.offload_put_failed": "engine";
350
350
  readonly "instructions.source_changed": "server";
@@ -143,6 +143,7 @@ export const ENGINE_NOTICE_CODES = Object.freeze([
143
143
  "permission.ask_released_by_mode",
144
144
  "permission.read_deny_unapplied",
145
145
  "permission.rule_store_unavailable",
146
+ "permission.allowed_tool_not_granted",
146
147
  "peer.inbound_disposition",
147
148
  "peer.held_settled",
148
149
  "peer.idle_subscription",
@@ -193,7 +194,6 @@ export const ENGINE_NOTICE_CODES = Object.freeze([
193
194
  "memory.capture_opted_out",
194
195
  "memory.capture_optout_unpersisted",
195
196
  "skills.listing_truncated",
196
- "skills.allowed_tool_not_granted",
197
197
  "agents.listing_truncated",
198
198
  "tool_result.offload_put_failed",
199
199
  "instructions.source_changed",
@@ -228,6 +228,7 @@ const NOTICE_AUDIENCE_TABLE = {
228
228
  "permission.ask_released_by_mode": "user",
229
229
  "permission.read_deny_unapplied": "user",
230
230
  "permission.rule_store_unavailable": "user",
231
+ "permission.allowed_tool_not_granted": "user",
231
232
  "memory.capture_opted_out": "user",
232
233
  "memory.capture_optout_unpersisted": "user",
233
234
  "memory.consolidation_withheld": "operator",
@@ -280,7 +281,6 @@ const NOTICE_AUDIENCE_TABLE = {
280
281
  "route.tool_call_id_folded": "operator",
281
282
  "context.tool_turn_repaired": "operator",
282
283
  "skills.listing_truncated": "operator",
283
- "skills.allowed_tool_not_granted": "operator",
284
284
  "agents.listing_truncated": "operator",
285
285
  "tool_result.offload_put_failed": "operator",
286
286
  "config.forward_sink_failed": "operator",
@@ -323,6 +323,7 @@ const NOTICE_MINTER_TABLE = {
323
323
  "permission.ask_released_by_mode": "engine",
324
324
  "permission.read_deny_unapplied": "engine",
325
325
  "permission.rule_store_unavailable": "engine",
326
+ "permission.allowed_tool_not_granted": "engine",
326
327
  "peer.inbound_disposition": "engine",
327
328
  "peer.held_settled": "engine",
328
329
  "peer.idle_subscription": "engine",
@@ -373,7 +374,6 @@ const NOTICE_MINTER_TABLE = {
373
374
  "memory.capture_opted_out": "engine",
374
375
  "memory.capture_optout_unpersisted": "engine",
375
376
  "skills.listing_truncated": "engine",
376
- "skills.allowed_tool_not_granted": "engine",
377
377
  "agents.listing_truncated": "engine",
378
378
  "tool_result.offload_put_failed": "engine",
379
379
  "instructions.source_changed": "server",
@@ -3,7 +3,7 @@ import type { ExecutionEnv, FileError, Result, SessionTreeEntry } from "../inter
3
3
  import type { DecisionReason, PermissionResult, ResolvedAsk, ToolCallFace, ToolCallRequest, ToolPolicy } from "./tool-policy.js";
4
4
  import type { GateOutcome } from "./gate-outcome.js";
5
5
  import { type PreToolUseScreening } from "./runner/gate-exit.js";
6
- export { normalizeOrgGateVerdict, normalizePersistedRuleHit } from "./gate-lanes.js";
6
+ export { normalizeOrgGateVerdict, normalizePersistedRuleHit } from "./rule-lane-reads.js";
7
7
  export { cloneObserverInput } from "./runner/gate-exit.js";
8
8
  import { type AskClass } from "./ask-class.js";
9
9
  import { type PermissionMode } from "./ask-origin.js";
@@ -13,6 +13,7 @@ import type { WiringLegKind } from "./wiring-manifest.js";
13
13
  import { type SafeNotifier } from "./safe-notify.js";
14
14
  import type { PermissionSeatInputs } from "./mode-policy.js";
15
15
  import type { SkillGateSeats } from "./skill-grant.js";
16
+ import type { AllowEntryGateSeats } from "./allow-entry.js";
16
17
  /**
17
18
  * In-process hook seam (design/37) — a provider-agnostic interception layer modeled on CC's hooks,
18
19
  * kept process-internal (no shell/HTTP executors, no settings files). The original tool-call trio:
@@ -834,10 +835,10 @@ export interface HookToolOutput {
834
835
  * `updatedInput` rewrite) plus optional `additionalContext` injected into the eventual tool result.
835
836
  *
836
837
  * **The provenance members are not hook-settable inputs** (#174). `decisionReason` is the ENGINE's account
837
- * of which layer produced a verdict, and `persistedRuleShadowed` is the gate's own #144 disclosure that a
838
- * stored rule matched without clearing the ask. The gate stamps both itself — a `decisionReason` on a hook
839
- * result is DISCARDED (a hook ask always reads `"hook"` downstream), and a `persistedRuleShadowed` is
840
- * dropped rather than rendered on an approval card as a rule no lane matched.
838
+ * of which layer produced a verdict, and `allowRuleShadowed` is the allow layer's own disclosure that a
839
+ * member matched without clearing the ask. The gate stamps both itself — a `decisionReason` on a hook
840
+ * result is DISCARDED (a hook ask always reads `"hook"` downstream), and an `allowRuleShadowed` is
841
+ * dropped rather than rendered on an approval card as an entry no member matched.
841
842
  *
842
843
  * Which of a hook's OTHER members reach the surviving decision depends on which fold arm ran, so read
843
844
  * the arm, not a single list: an ask promoted from a policy `allow` is rebuilt from the hook's `message`,
@@ -1132,7 +1133,7 @@ export type PersistedRuleAnswer = PersistedRuleHit | PersistedRuleCoverage | Per
1132
1133
  * signal; when the caller also supplies {@link ToolGateInput.callSignal}, the Runner's closures
1133
1134
  * additionally bind their waits to that per-call signal (`AbortSignal.any` of the two), so a turn
1134
1135
  * interrupt releases a pending ask instead of leaving it hanging on the run's lifetime alone. */
1135
- export interface ToolGateInput extends PermissionSeatInputs, SkillGateSeats {
1136
+ export interface ToolGateInput extends PermissionSeatInputs, SkillGateSeats, AllowEntryGateSeats {
1136
1137
  /** RB-473 batch 2b — bounded disclosure for a HOST callback that throws inside this gate call's
1137
1138
  * isolation scope (onHookError itself, the deny observer, the durable-park observer). The runner
1138
1139
  * wires this to an `observer.notify_failed` trace event; absent = counted only (unchanged). */
@@ -1250,9 +1251,9 @@ export interface ToolGateInput extends PermissionSeatInputs, SkillGateSeats {
1250
1251
  * ASK rule, from the org-unavailable tighten (whose resume semantics differ — see
1251
1252
  * {@link import("./checkpoint-store.js").RealApprovalGateBit}), or from a policy/hook. */
1252
1253
  realApproval?: import("./checkpoint-store.js").RealApprovalGateBit,
1253
- /** The matched-but-outranked persisted rule (the surviving ask's `persistedRuleShadowed`)
1254
+ /** The allow-layer members that matched and could not clear (the surviving ask's `allowRuleShadowed`)
1254
1255
  * — threaded so the park mint's risk descriptor carries the disclosure on the durable route. */
1255
- shadowedRule?: string,
1256
+ allowRuleShadowed?: readonly import("./allow-rule-shadow.js").AllowRuleShadow[],
1256
1257
  /** The surviving ask's `decisionReason` — threaded so the park mint's rule-suggestion door can
1257
1258
  * read the ask's origin on the durable route exactly as the synchronous mints read it off the
1258
1259
  * decision (a hook-raised ask is one no persisted rule may clear, so its parked card must not
@@ -1561,7 +1562,7 @@ export interface ToolGateInput extends PermissionSeatInputs, SkillGateSeats {
1561
1562
  * · #144 (ruled): a MANDATED ask is never consumed — an operator's shellGate:"always" tier and a
1562
1563
  * tool's own egress/irreversibility marks are structural requirements, not classifier hesitation
1563
1564
  * ("allow rules silence the classifier's questions, never a mandated one"). When a rule matches
1564
- * but cannot clear, the surviving ask discloses it (message + `persistedRuleShadowed`).
1565
+ * but cannot clear, the surviving ask discloses it (`allowRuleShadowed`, member `persisted`).
1565
1566
  * · #502: an ask the call's own reversibility probe declared STRUCTURAL (`probeMandated`) joins that
1566
1567
  * family. It is the one PER-CALL member of it, and the reason there has to be one: the built-in
1567
1568
  * shell probe raises it for a read outside the session's allowed directories, which is a boundary
@@ -1800,7 +1801,9 @@ export declare function askCarryRowMembers(carry: AskCarry | undefined): {
1800
1801
  export declare function createPreToolUseScreening(preToolUse: Hooks["preToolUse"] | undefined, input: Pick<ToolGateInput, "event" | "trackedCwd" | "hookEnv" | "identity" | "onHookError" | "abortSignal">, hookSeatMs: number, notifier: SafeNotifier): PreToolUseScreening | undefined;
1801
1802
  /**
1802
1803
  * The design/37 **two-phase tool gate** — the single chokepoint that makes the load-bearing invariant
1803
- * structural ("a hook's `allow` cannot bypass the policy's `deny`/`ask`"):
1804
+ * structural: a hook's `allow` passes no deny, no ask rule and no mandated question; it is one member of the
1805
+ * allow layer and clears only the questions an allow rule may clear (a deployment that wants a question put to a
1806
+ * person whatever a hook says marks it `authoredBy: "deployment"` or `requiresRealApproval`):
1804
1807
  *
1805
1808
  * 1. **collect** — run the PreToolUse hook, threading any `updatedInput` rewrite into `currentInput`
1806
1809
  * and collecting `additionalContext`. A hook `deny` short-circuits to a block immediately; a hook
@@ -1,10 +1,10 @@
1
- import { decisionText, describeThrown, receiveHookResult, isEngineRefusal } from "./tool-policy.js";
1
+ import { abortedSeatDeny, decisionText, describeThrown, receiveHookResult, isEngineRefusal } from "./tool-policy.js";
2
2
  import { asThrownError } from "./thrown-value.js";
3
3
  import { createSettlementLedger, engineFailClosedExit, hookDenyExit } from "./runner/gate-exit.js";
4
4
  import { traceHookCrash } from "./gate-crash-trace.js";
5
5
  import { runGateFold } from "./gate-fold.js";
6
6
  import { runGateLanes } from "./gate-lanes.js";
7
- export { normalizeOrgGateVerdict, normalizePersistedRuleHit } from "./gate-lanes.js";
7
+ export { normalizeOrgGateVerdict, normalizePersistedRuleHit } from "./rule-lane-reads.js";
8
8
  export { cloneObserverInput } from "./runner/gate-exit.js";
9
9
  import { brandPolicyAskClass } from "./ask-class.js";
10
10
  import { isAskOrigin, screenAskClassMembers } from "./ask-origin.js";
@@ -194,7 +194,9 @@ function screenPreToolUseResult(r) {
194
194
  }
195
195
  export function createPreToolUseConstraintPolicy(preToolUse, env, onCrash, timeoutMs) {
196
196
  return brandPolicyAskClass({
197
- check: async (req) => {
197
+ check: async (req, signal) => {
198
+ if (signal?.aborted)
199
+ return abortedSeatDeny(req, `the inherited PreToolUse screening of "${req.toolName}" was not consulted: the run or turn was already interrupted`, "hook");
198
200
  let r;
199
201
  const boundMs = resolveHookTimeoutMs(timeoutMs, onCrash);
200
202
  try {