@sema-agent/core 7.39.2 โ†’ 7.40.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/dist/core/a2a.js +1 -0
  3. package/dist/core/ask-origin.d.ts +19 -6
  4. package/dist/core/ask-origin.js +6 -0
  5. package/dist/core/checkpoint-store.d.ts +8 -8
  6. package/dist/core/gate-fold.js +5 -0
  7. package/dist/core/gate-lanes.js +8 -4
  8. package/dist/core/hands-band.d.ts +0 -12
  9. package/dist/core/hooks.d.ts +3 -0
  10. package/dist/core/hooks.js +4 -1
  11. package/dist/core/mcp.js +1 -1
  12. package/dist/core/mode-policy.d.ts +17 -2
  13. package/dist/core/mode-policy.js +13 -6
  14. package/dist/core/permission-rule-model.d.ts +29 -0
  15. package/dist/core/permission-rule-model.js +15 -2
  16. package/dist/core/permission-rules.d.ts +2 -10
  17. package/dist/core/permission-rules.js +22 -19
  18. package/dist/core/runner/denial-limit-arms.js +1 -1
  19. package/dist/core/runner/gate-exit.d.ts +5 -1
  20. package/dist/core/runner/permission-rule-lanes.js +7 -5
  21. package/dist/core/runner/persisted-path-rows.js +3 -3
  22. package/dist/core/runner/prepare-gate-stations.d.ts +1 -1
  23. package/dist/core/runner/prepare-gate-stations.js +2 -0
  24. package/dist/core/runner/prepare-hands-readface.d.ts +1 -1
  25. package/dist/core/runner/prepare-hands-readface.js +0 -1
  26. package/dist/core/runner/prepare-policy-chain.js +11 -7
  27. package/dist/core/runner/prepare-wiring-manifest.d.ts +1 -1
  28. package/dist/core/runner/prepare-wiring-manifest.js +10 -5
  29. package/dist/core/runner/resume-apply.js +3 -3
  30. package/dist/core/runner/resume-preflight.js +3 -3
  31. package/dist/core/runner/retired-runner-deps.js +1 -0
  32. package/dist/core/runner-deps.d.ts +3 -1
  33. package/dist/core/tool-policy.d.ts +14 -0
  34. package/dist/core/tool-policy.js +15 -6
  35. package/dist/core/tool-spec.d.ts +10 -0
  36. package/dist/core/tools.js +1 -0
  37. package/dist/engine/loop/types.d.ts +6 -0
  38. package/dist/orchestration/workflow.js +1 -1
  39. package/dist/tools/fs/fs-read.d.ts +1 -1
  40. package/dist/tools/fs/fs-read.js +3 -5
  41. package/dist/tools/fs/fs-shared.d.ts +0 -27
  42. package/dist/tools/fs/fs-shared.js +0 -3
  43. package/dist/tools/fs/index.d.ts +1 -9
  44. package/dist/tools/fs/index.js +1 -1
  45. package/package.json +1 -1
  46. package/wire-changes/core@7.40.0.json +376 -0
  47. package/wire-changes/core@7.40.1.json +39 -0
@@ -35,6 +35,8 @@
35
35
  * compile time (`unsupported.path_base`) rather than left to reach nothing at run time โ€” a deny that
36
36
  * silently matches nothing is the failure form this module's header calls the most dangerous one. A
37
37
  * path-form ALLOW stays refused (`unsupported.file_glob`): the Read directory allow is the consent store's.
38
+ * A `Read(...)` / `Edit(...)` deny or ask reaches the calls on its behavior's FACES (`PATH_RULE_FACES`), not only
39
+ * its own tool: a `Read` ask asks for a Grep / Glob rooted in its path, judged on that call's own target.
38
40
  *
39
41
  * ๐Ÿ”ด ไธŽ CC ็š„ๆœ‰ๆ„ๅทฎ(design/127 r2):
40
42
  * - CC ๅ‚ๆ•ฐ็บง generic ้“ๆ—  allow lane(gnn ๅชๆœ‰ deny/ask)โ€”โ€” ๆˆ‘ไปฌๅŒๆ ท:`allow` + ๅ‚ๆ•ฐ็บง content =
@@ -191,16 +193,6 @@ export interface RuleFaceView {
191
193
  export declare function catalogRuleFaceView(toolName: string): RuleFaceView | undefined;
192
194
  /** Exported for the lockstep guard only (the scalar vocabulary of a catalog tool) โ€” not part of the rule DSL. */
193
195
  export declare const fileToolParamVocabulary: (canonicalTool: string) => ReadonlySet<string> | undefined;
194
- /**
195
- * The path DENY rules that speak for a FACE rather than for one tool (the reference implementation's reading: a Read
196
- * deny rule covers every built-in tool that reads a file and the shell's file commands; an Edit deny rule every tool
197
- * that edits one; a Read deny also refuses writing the same path). Such a rule reaches any call whose declared path
198
- * target is on one of its faces โ€” the write tools, the read tools, the search roots โ€” and it is exported to the
199
- * engine's deny tables (`ToolPolicy.pathDenyRules`) so the shell's reads and writes and the
200
- * traversals meet it too. A path rule on any other tool keeps its tool's name as its reach (`Write(<path>)` is judged on
201
- * the Write tool, a stricter reading the reference does not make).
202
- */
203
- export declare const PATH_DENY_RULE_FACES: Readonly<Record<string, readonly DenyFace[]>>;
204
196
  /**
205
197
  * The deny-table row of ONE face-keyed path deny rule, resolved against `bases` โ€” the row's reach IS the rule's (the same
206
198
  * parsed rule, the same bases). Every source of such rows builds them here: a policy's compiled rules, and the person's
@@ -1,7 +1,7 @@
1
1
  import { MCP_NAMESPACE, protocolOf } from "./protocol-table.js";
2
2
  import { catalogRuleFaceOf, pathTargetOf } from "./tool-registry.js";
3
3
  import { indexOfUnescaped, lastIndexOfUnescaped, parsePermissionRule } from "./permission-rule-syntax.js";
4
- import { PATH_RULE_BASE_LABEL, READ_RULE_TOOL, isUsablePathBase, parseRuleText, pathRuleHeadOf, pathRuleReachOf, programRunReachOf, ruleBasesNeeded } from "./permission-rule-model.js";
4
+ import { PATH_RULE_BASE_LABEL, PATH_RULE_FACES, callPathFaceOf, isUsablePathBase, parseRuleText, pathRuleHeadOf, pathRuleReachOf, programRunReachOf, ruleBasesNeeded } from "./permission-rule-model.js";
5
5
  import { pathFamilyOf, pathIdentityOf, spellingOfIdentity } from "../tools/fs/safety.js";
6
6
  import { effectivePathTargetOf } from "./effective-path-target.js";
7
7
  import { protectivePathTargetOf } from "./tool-registry.js";
@@ -82,7 +82,6 @@ export const fileToolParamVocabulary = (canonicalTool) => {
82
82
  const face = catalogRuleFaceOf(canonicalTool);
83
83
  return face?.params === undefined || pathTargetOf(canonicalTool) === undefined ? undefined : new Set(face.params);
84
84
  };
85
- export const PATH_DENY_RULE_FACES = Object.freeze({ [READ_RULE_TOOL]: ["read", "write"], Edit: ["write"] });
86
85
  export function pathRuleDenyRowOf(rule, bases) {
87
86
  const family = pathFamilyOf(bases);
88
87
  const tree = bases.root ?? bases.cwd;
@@ -99,10 +98,6 @@ export function pathRuleDenyRowOf(rule, bases) {
99
98
  ...(head !== undefined ? { head: spellingOfIdentity(head, family) } : {}),
100
99
  };
101
100
  }
102
- function callPathFaceOf(req) {
103
- const target = protectivePathTargetOf(req);
104
- return target === undefined ? undefined : target.access === "read" ? "read" : "write";
105
- }
106
101
  function countStars(s) {
107
102
  let n = 0;
108
103
  for (const c of s)
@@ -218,7 +213,7 @@ function compile(rules, caps, primaryFieldGeneric, ruleFaces, pathBases = {}) {
218
213
  }
219
214
  if (issues.length > 0 && issues[issues.length - 1].rule === text && issues[issues.length - 1].code === "unsupported.path_base")
220
215
  continue;
221
- const faces = r.behavior === "deny" ? PATH_DENY_RULE_FACES[toolName] : undefined;
216
+ const faces = PATH_RULE_FACES[toolName]?.[r.behavior];
222
217
  entry.content[r.behavior].push({ parsed: parsedPath.rule, ruleText: text, ...(r.source !== undefined ? { source: r.source } : {}), ...(faces !== undefined ? { faces } : {}) });
223
218
  byTool.set(toolName, entry);
224
219
  continue;
@@ -340,8 +335,15 @@ export function createPermissionRulePolicy(rules, opts) {
340
335
  }
341
336
  const coveringHit = (lane, toolName) => lane.length === 0 ? undefined : lane.find((r) => namespacedRuleNameCovers(r.ruleName, toolName));
342
337
  const hasCovering = namespacedCovering.deny.length > 0 || namespacedCovering.ask.length > 0 || namespacedCovering.allow.length > 0;
343
- const faceDenyRules = [...byTool.values()].flatMap((e) => e.content.deny.filter((r) => r.faces !== undefined));
344
- const pathDenyRules = faceDenyRules.map((r) => pathRuleDenyRowOf({ parsed: r.parsed, ruleText: r.ruleText, faces: r.faces }, pathBases));
338
+ const faceRules = {
339
+ deny: [...byTool.values()].flatMap((e) => e.content.deny.filter((r) => r.faces !== undefined)),
340
+ ask: [...byTool.values()].flatMap((e) => e.content.ask.filter((r) => r.faces !== undefined)),
341
+ };
342
+ const pathDenyRules = faceRules.deny.map((r) => pathRuleDenyRowOf({ parsed: r.parsed, ruleText: r.ruleText, faces: r.faces }, pathBases));
343
+ const contentLaneOf = (behavior, entry, callFace) => [
344
+ ...(entry?.content[behavior].filter((r) => r.faces === undefined) ?? []),
345
+ ...(callFace !== undefined ? faceRules[behavior].filter((r) => r.faces.includes(callFace)) : []),
346
+ ];
345
347
  const commandParam = catalogRuleFaceOf("Bash")?.primaryParams[0] ?? "command";
346
348
  const contentHit = (lane, req) => {
347
349
  if (lane.length === 0)
@@ -410,8 +412,8 @@ export function createPermissionRulePolicy(rules, opts) {
410
412
  if (coveringDeny)
411
413
  return { action: "deny", message: ruleDenial(req, coveringDeny.ruleText, coveringDeny.source) };
412
414
  }
413
- const callFace = faceDenyRules.length > 0 ? callPathFaceOf(req) : undefined;
414
- const denyLane = [...(entry?.content.deny.filter((r) => r.faces === undefined) ?? []), ...(callFace !== undefined ? faceDenyRules.filter((r) => r.faces.includes(callFace)) : [])];
415
+ const callFace = faceRules.deny.length > 0 || faceRules.ask.length > 0 ? callPathFaceOf(req) : undefined;
416
+ const denyLane = contentLaneOf("deny", entry, callFace);
415
417
  const contentDeny = denyLane.length > 0 ? contentHit(denyLane, req) : undefined;
416
418
  if (contentDeny !== undefined && contentDeny.unreadable === undefined) {
417
419
  return { action: "deny", message: ruleDenial(req, contentDeny.rule.ruleText, contentDeny.rule.source) };
@@ -434,23 +436,24 @@ export function createPermissionRulePolicy(rules, opts) {
434
436
  if (coveringAsk)
435
437
  return { action: "ask", message: ruleAskMessage(coveringAsk.ruleText, coveringAsk.source), matchedAskRule: coveringAsk.ruleText };
436
438
  }
439
+ const askLane = contentLaneOf("ask", entry, callFace);
440
+ const contentAsk = askLane.length > 0 ? contentHit(askLane, req) : undefined;
441
+ if (contentAsk !== undefined) {
442
+ if (contentAsk.unreadable !== undefined)
443
+ return unreadableAsk({ rule: contentAsk.rule, unreadable: contentAsk.unreadable }, "content_ask", req);
444
+ return { action: "ask", message: ruleAskMessage(contentAsk.rule.ruleText, contentAsk.rule.source), matchedAskRule: contentAsk.rule.ruleText };
445
+ }
437
446
  if (entry) {
438
- const contentAsk = contentHit(entry.content.ask, req);
439
- if (contentAsk !== undefined) {
440
- if (contentAsk.unreadable !== undefined)
441
- return unreadableAsk({ rule: contentAsk.rule, unreadable: contentAsk.unreadable }, "content_ask", req);
442
- return { action: "ask", message: ruleAskMessage(contentAsk.rule.ruleText, contentAsk.rule.source), matchedAskRule: contentAsk.rule.ruleText };
443
- }
444
447
  const paramAsk = matchParamRules(entry.param.ask, req.args, caps.maxScalarValueChars);
445
448
  if (paramAsk) {
446
449
  return { action: "ask", message: ruleAskMessage(paramAsk.ruleText, paramAsk.source), matchedAskRule: paramAsk.ruleText };
447
450
  }
448
451
  if (entry.bare.allow) {
449
- return { action: "allow" };
452
+ return { action: "allow", decisionReason: "rule" };
450
453
  }
451
454
  }
452
455
  if (covering && coveringHit(namespacedCovering.allow, req.toolName)) {
453
- return { action: "allow" };
456
+ return { action: "allow", decisionReason: "rule" };
454
457
  }
455
458
  if (defaultAction === "allow")
456
459
  return { action: "allow" };
@@ -103,7 +103,7 @@ export async function judgeInheritedClassifier(opts) {
103
103
  const attributed = (d) => (verdict.classifier !== undefined ? attestClassifierRound(d, verdict.classifier, req) : d);
104
104
  if (verdict.kind === "allow") {
105
105
  autoMode.denialTracking?.recordAllow();
106
- return { kind: "allow", result: attributed({ action: "allow", ...(opts.allowRewrite !== undefined ? { updatedInput: opts.allowRewrite } : {}) }) };
106
+ return { kind: "allow", result: attributed({ action: "allow", decisionReason: "classifier", ...(opts.allowRewrite !== undefined ? { updatedInput: opts.allowRewrite } : {}) }) };
107
107
  }
108
108
  if (verdict.kind === "unavailable") {
109
109
  const arm = classifierUnavailableArm({ ask, req, cause: verdict.cause, seat: opts.unavailableSeat, aborted: opts.signal.aborted });
@@ -214,6 +214,10 @@ export interface GatePass {
214
214
  hookAsk: Extract<PermissionResult, {
215
215
  action: "ask";
216
216
  }> | undefined;
217
+ /** borrowed-mutable โ€” the phase-1 hook answered its own `allow` (the exact word): a statement about this call, read by
218
+ * the fold's last station only while the policy left the arguments as the hook saw them. Writer: the dispatch (phase 1).
219
+ * Reader: the fold's unstated station. */
220
+ hookAllowed?: true;
217
221
  /** borrowed-mutable โ€” set by a park attempt that FAILED (see `ParkAttemptFailed`). Read at the single deny
218
222
  * exit โ€” whatever the fallback chain then decides to say, it says it knowing the park was tried first.
219
223
  * Writers: the three park sites. */
@@ -256,7 +260,7 @@ export interface GatePass {
256
260
  * policy's answer; then rewritten by every station that changes the verdict (fold, tightens, org, rule
257
261
  * lane, stamp, classifier, sandbox, content lane, resolution, edit chain). */
258
262
  decision: PermissionResult;
259
- /** borrowed-mutable โ€” which of the gate's OWN post-fold tightens minted the standing ask, if one did โ€” an
263
+ /** borrowed-mutable โ€” which of the gate's OWN post-fold stations (the tightens and the unstated station) minted the standing ask, if one did โ€” an
260
264
  * input of the ask-origin derivation (`AskOriginFacts.tightened`). Written at most once per pass: every
261
265
  * tighten fires only over a surviving `allow`, and the first one to fire leaves an `ask` the others do not
262
266
  * touch. Recorded by the gate rather than read off `decisionReason` because that member is a policy's to
@@ -1,6 +1,6 @@
1
1
  import { persistedRuleMandateOf, ruleClearanceOf } from "../ask-origin.js";
2
2
  import {} from "../hooks.js";
3
- import { adjudicatePersistedPathRules, adjudicatePersistedRules, laneToolNamesOf, ruleToolGrammarOf, segmentCoverageOf, suggestRulesForCommand } from "../permission-rule-model.js";
3
+ import { adjudicatePersistedPathRules, adjudicatePersistedRules, callPathFaceOf, faceAskSpeaksFor, laneToolNamesOf, ruleToolGrammarOf, segmentCoverageOf, suggestRulesForCommand } from "../permission-rule-model.js";
4
4
  import { isAbsolutePathForm } from "../../tools/fs/safety.js";
5
5
  import { effectivePathTargetOf } from "../effective-path-target.js";
6
6
  import { declaredPathTargetOf, protectivePathTargetOf } from "../tool-registry.js";
@@ -45,7 +45,7 @@ export function pathRuleLaneAnswer(table, req, ctx) {
45
45
  const allow = effectivePathTargetOf(req, declaredPathTargetOf(req), bases);
46
46
  if (tighten === undefined && allow === undefined)
47
47
  return undefined;
48
- return persistedRuleAnswerOf(adjudicatePersistedPathRules(table, { tool: req.toolName, cwd: ctx.root, sessionId: ctx.sessionId }, { ...(tighten !== undefined ? { tighten } : {}), ...(allow !== undefined ? { allow } : {}) }, bases));
48
+ return persistedRuleAnswerOf(adjudicatePersistedPathRules(table, { tool: req.toolName, cwd: ctx.root, sessionId: ctx.sessionId, face: callPathFaceOf(req) }, { ...(tighten !== undefined ? { tighten } : {}), ...(allow !== undefined ? { allow } : {}) }, bases));
49
49
  }
50
50
  export function orgRuleLane(cfg) {
51
51
  return {
@@ -127,8 +127,10 @@ export function createPermissionRuleLanes(cfg) {
127
127
  const personalAdjudicate = async (req) => {
128
128
  if (anonymous && !cfg.localOwnerDeclared)
129
129
  return undefined;
130
- const laneNames = laneToolNamesOf(req);
131
- if (laneNames.length === 0)
130
+ const judged = laneToolNamesOf(req).map((n) => [n, ruleToolGrammarOf(n)]);
131
+ if (!judged.some(([, g]) => g === "path") && faceAskSpeaksFor(callPathFaceOf(req)))
132
+ judged.push([req.toolName, "path"]);
133
+ if (judged.length === 0)
132
134
  return undefined;
133
135
  let view;
134
136
  try {
@@ -140,7 +142,7 @@ export function createPermissionRuleLanes(cfg) {
140
142
  if (view.unreadable !== undefined)
141
143
  skipped(view.unreadable, view.unreadableSources);
142
144
  try {
143
- const answer = strongestAnswer(laneNames.map((n) => judgeAgainst(view.rules, { ...req, toolName: n }, ruleToolGrammarOf(n))));
145
+ const answer = strongestAnswer(judged.map(([n, grammar]) => judgeAgainst(view.rules, { ...req, toolName: n }, grammar)));
144
146
  return view.unreadable !== undefined && (answer === undefined || "segmentCoverage" in answer) ? { unreadable: true } : answer;
145
147
  }
146
148
  catch (err) {
@@ -1,7 +1,7 @@
1
1
  import { ownerStoreOf } from "../permission-rule-provider.js";
2
- import { READ_RULE_TOOL, eligiblePersisted, isUsablePathBase, ruleBasesNeeded } from "../permission-rule-model.js";
2
+ import { PATH_RULE_FACES, READ_RULE_TOOL, eligiblePersisted, isUsablePathBase, ruleBasesNeeded } from "../permission-rule-model.js";
3
3
  import { isAbsolutePathForm, pathFamilyOf } from "../../tools/fs/safety.js";
4
- import { PATH_DENY_RULE_FACES, pathRuleDenyRowOf } from "../permission-rules.js";
4
+ import { pathRuleDenyRowOf } from "../permission-rules.js";
5
5
  import { inlineUntrusted } from "../untrusted-text.js";
6
6
  import { DEFAULT_GATE_AWAIT_TIMEOUT_MS, gateWaitFailureOf, withGateTimeout } from "../gate-bounded-await.js";
7
7
  import { thrownText } from "../thrown-value.js";
@@ -34,7 +34,7 @@ export async function persistedPathRowsOf(cfg) {
34
34
  readDirectories.push(rule.command);
35
35
  continue;
36
36
  }
37
- const faces = PATH_DENY_RULE_FACES[rule.tool];
37
+ const faces = PATH_RULE_FACES[rule.tool]?.deny;
38
38
  if (rule.behavior !== "deny" || faces === undefined || (rule.match !== "path" && rule.match !== "subpath") || !eligiblePersisted(rule, { ...call, tool: rule.tool }, "deny"))
39
39
  continue;
40
40
  const missing = ruleBasesNeeded(rule).filter((b) => !isUsablePathBase(b === "cwd" ? bases.root : bases[b], family));
@@ -25,7 +25,7 @@
25
25
  import type { AgentHarness, AgentTool } from "../../internal/harness.js";
26
26
  import type { AskClass } from "../ask-class.js";
27
27
  import type { Arbitration, ToolMarkSource } from "../ask-origin.js";
28
- import type { PermissionModeController } from "../mode-policy.js";
28
+ import { type PermissionModeController } from "../mode-policy.js";
29
29
  import type { AutoModeDecider, AutoModeDenialTracker } from "../auto-mode.js";
30
30
  import { type ComplianceCapability } from "../compliance.js";
31
31
  import { type HookEnvCapabilities, type HookInvocationIdentity, type Hooks } from "../hooks.js";
@@ -1,4 +1,5 @@
1
1
  import { SEND_MESSAGE_TOOL_NAME } from "../../agents/send-message-tool.js";
2
+ import { mountedToolPermissionSeat } from "../mode-policy.js";
2
3
  import { complianceCallDenial } from "../compliance.js";
3
4
  import { cloneObserverInput, formatHookFeedback, hookSeatExpiredError, runHookSeat, runToolGate } from "../hooks.js";
4
5
  import { screenGateOutcome } from "../gate-outcome.js";
@@ -137,6 +138,7 @@ export function prepareGateStations(input) {
137
138
  ...(autoModeDecider.current !== undefined ? { autoMode: { decider: autoModeDecider.current, ...(autoModeDenialTracking.current !== undefined ? { denialTracking: autoModeDenialTracking.current } : {}), unavailable: classifierUnavailableSeat({ station: "leg", ...(autoModeDecider.current.timeoutMs !== undefined ? { budgetMs: autoModeDecider.current.timeoutMs } : {}), approvalSeat: askReachesPerson(askRoute), onNotice: deps.onNotice, sessionId, runId }) } } : {}),
138
139
  permissionMode: () => permissionMode.current,
139
140
  ...(markSources.size > 0 ? { toolMarkSources: markSources } : {}),
141
+ ...(mountedToolPermissionSeat(tools, e.toolName) === "passthrough" ? { permissionSeat: "passthrough" } : {}),
140
142
  ...(a2aIsolatePeers === true ? { a2aIsolatePeers: true } : {}),
141
143
  onModeReleased,
142
144
  ancestorArbitrationOf: (toolCallId) => ancestorArbitrations.get(toolCallId),
@@ -149,7 +149,7 @@ export interface PrepareHandsReadFaceInput {
149
149
  reversibilityProbes: ReversibilityProbes;
150
150
  /** borrowed-readonly โ€” the run's reminder provenance mark (minted/adopted
151
151
  * by the driver's adoption ladder); threaded into the hands band so the fs-side engine mints
152
- * (the Read cyber/dedup/offset/empty reminders, the gh rate-limit hint) stamp the same mark the
152
+ * (the Read dedup/offset/empty reminders, the gh rate-limit hint) stamp the same mark the
153
153
  * system-prompt declaration names. */
154
154
  reminderMark: string;
155
155
  /** borrowed-mutable โ€” the leg's disclosure trigger counters; the hands
@@ -366,7 +366,6 @@ export async function prepareHandsMount(input) {
366
366
  ...(deps.hands?.commitCoAuthor !== undefined ? { commitCoAuthor: deps.hands.commitCoAuthor } : {}),
367
367
  ...(deps.hands?.readImageDownsampler !== undefined ? { readImageDownsampler: deps.hands.readImageDownsampler } : {}),
368
368
  ...(deps.hands?.autoBackgroundOnTimeout !== undefined ? { autoBackgroundOnTimeout: deps.hands.autoBackgroundOnTimeout } : {}),
369
- ...(deps.hands?.readCyberReminder !== undefined ? { readCyberReminder: deps.hands.readCyberReminder } : {}),
370
369
  beforeWrite: async (w) => {
371
370
  const deploymentGate = deps.hands?.beforeWrite;
372
371
  if (deploymentGate) {
@@ -364,7 +364,8 @@ export async function preparePolicyChain(input) {
364
364
  if (re.action === "deny")
365
365
  return vetoed(re);
366
366
  if (re.action === "allow") {
367
- const out = { action: "allow", updatedInput: re.updatedInput !== undefined ? re.updatedInput : editArgs };
367
+ const stated = re.updatedInput === undefined ? "mode" : re.decisionReason;
368
+ const out = { action: "allow", updatedInput: re.updatedInput !== undefined ? re.updatedInput : editArgs, ...(stated !== undefined ? { decisionReason: stated } : {}) };
368
369
  carryEngineAttestations(re, out);
369
370
  carryPrecedingSettlement(settledBy, out);
370
371
  return out;
@@ -384,7 +385,7 @@ export async function preparePolicyChain(input) {
384
385
  re = editAsk;
385
386
  const editArbitrated = arbitrateInherited(re, creq.toolCallId, judged);
386
387
  if (editArbitrated !== undefined) {
387
- const out = editArbitrated.action === "allow" ? { action: "allow", updatedInput: editArgs } : editArbitrated;
388
+ const out = editArbitrated.action === "allow" ? { action: "allow", updatedInput: editArgs, decisionReason: "mode" } : editArbitrated;
388
389
  carryPrecedingSettlement(settledBy, out);
389
390
  return out;
390
391
  }
@@ -408,7 +409,7 @@ export async function preparePolicyChain(input) {
408
409
  return rr;
409
410
  settledBy = rr;
410
411
  if (rr.updatedInput === undefined) {
411
- const out = { action: "allow", updatedInput: editArgs };
412
+ const out = { action: "allow", updatedInput: editArgs, decisionReason: "mode" };
412
413
  carryPrecedingSettlement(rr, out);
413
414
  return out;
414
415
  }
@@ -461,7 +462,7 @@ export async function preparePolicyChain(input) {
461
462
  const { ask: inheritedAsk, fallback, mintedHere } = judged;
462
463
  if (fallback === undefined && sandboxAdmissionArmed && policyAskClassOf(pc.policy) === "sandbox_local" && !sandboxBoundaryCapable(creq) && judged.classification.row.sandboxAdmission) {
463
464
  recordAncestorSandboxAdmission(creq.toolCallId, creq.toolName);
464
- return { action: "allow" };
465
+ return { action: "allow", decisionReason: "sandbox" };
465
466
  }
466
467
  const arbitrated = arbitrateInherited(inheritedAsk, creq.toolCallId, judged);
467
468
  if (arbitrated !== undefined)
@@ -496,7 +497,7 @@ export async function preparePolicyChain(input) {
496
497
  return recheckApprovedEdit(pc.policy, pc.onAsk, creq, resolved.updatedInput, resolved, csignal, pc.autoMode);
497
498
  }
498
499
  recordInheritedAskGrant(creq.toolCallId, pc.onAsk, resolved.presentedInput, askWaitMs, resolved, askClassMembersOf(judged.classification).classes);
499
- return { action: "allow" };
500
+ return { action: "allow", decisionReason: "mode" };
500
501
  },
501
502
  }
502
503
  : wrapToolPolicy(pc.policy, async (creq, csignal) => {
@@ -535,7 +536,7 @@ export async function preparePolicyChain(input) {
535
536
  const { ask: inheritedAsk, fallback, mintedHere } = judged;
536
537
  if (fallback === undefined && sandboxAdmissionArmed && policyAskClassOf(pc.policy) === "sandbox_local" && !sandboxBoundaryCapable(creq) && judged.classification.row.sandboxAdmission) {
537
538
  recordAncestorSandboxAdmission(creq.toolCallId, creq.toolName);
538
- return decision.updatedInput !== undefined ? { action: "allow", updatedInput: decision.updatedInput } : { action: "allow" };
539
+ return decision.updatedInput !== undefined ? { action: "allow", updatedInput: decision.updatedInput, decisionReason: "sandbox" } : { action: "allow", decisionReason: "sandbox" };
539
540
  }
540
541
  const arbitrated = arbitrateInherited(inheritedAsk.action === "ask" ? { ...inheritedAsk, ...(decision.updatedInput !== undefined ? { updatedInput: decision.updatedInput } : {}) } : inheritedAsk, creq.toolCallId, judged);
541
542
  if (arbitrated !== undefined)
@@ -635,6 +636,7 @@ export async function preparePolicyChain(input) {
635
636
  const finalReq = { ...req, args: decision.updatedInput };
636
637
  let asked;
637
638
  const revalidated = {};
639
+ let restated;
638
640
  try {
639
641
  for (const layer of recheckLayers) {
640
642
  const d = refuseOutOfContractDecision(await layer.check(finalReq, signal));
@@ -653,6 +655,8 @@ export async function preparePolicyChain(input) {
653
655
  }
654
656
  if (d.action === "ask" && asked === undefined)
655
657
  asked = d;
658
+ if (d.action === "allow" && restated === undefined && d.decisionReason !== undefined)
659
+ restated = d.decisionReason;
656
660
  carryEngineAttestations(d, revalidated);
657
661
  }
658
662
  }
@@ -665,7 +669,7 @@ export async function preparePolicyChain(input) {
665
669
  return surfaced;
666
670
  }
667
671
  if (decision.action === "allow") {
668
- const owned = { ...decision };
672
+ const owned = { ...decision, ...(decision.decisionReason === undefined && restated !== undefined ? { decisionReason: restated } : {}) };
669
673
  carryEngineAttestations(decision, owned);
670
674
  carryEngineAttestations(revalidated, owned);
671
675
  return owned;
@@ -33,7 +33,7 @@ import { type LegWriteProtection } from "./write-protection-station.js";
33
33
  import { type UsageWindow } from "../usage-window-store.js";
34
34
  import type { ToolRoster } from "../tool-roster.js";
35
35
  import { type AutoModeArmFact, type WiringManifest, type WiringManifestMcpEntry } from "../wiring-manifest.js";
36
- import type { PermissionModeController } from "../mode-policy.js";
36
+ import { type PermissionModeController } from "../mode-policy.js";
37
37
  import type { AnnounceOnceSink, Prepared, PrepareResume, RunInternals, UsageGovernance } from "./contracts.js";
38
38
  export interface PrepareWiringManifestInput {
39
39
  /** borrowed-readonly โ€” the REBOUND spec. Read: `onAsk` (the own seat), `onQuestion` (the question seam), `handsReadOnly` / `tools` (the ungated scan),
@@ -12,6 +12,7 @@ import { mountWriteProtection } from "./write-protection-station.js";
12
12
  import { autoRunOnRecommendationEffective } from "./memory-consolidation-driver.js";
13
13
  import { GLOBAL_USAGE_KEY, usageRetryAfterMs, windowsGovernCost } from "../usage-window-store.js";
14
14
  import { LSP_CONTRACT_ID, askSeatEffective, countElicitOptIns, deriveHandsSeam, deriveLspSeam, deriveWiringManifest, resolveAskSeamForm, resolveDeclaredDurability, resolveElicitSeam, resolveQuestionSeam, resolveSubagentTranscriptTier } from "../wiring-manifest.js";
15
+ import { modePolicyOf } from "../mode-policy.js";
15
16
  import { DEFAULT_IRREVERSIBLE_SCOPE } from "./checkpoint-scope.js";
16
17
  export function mcpManifestEntries(entries, statuses) {
17
18
  return (entries ?? []).map((entry) => {
@@ -120,15 +121,18 @@ export function prepareWiringManifest(input) {
120
121
  const onAsk = spec.onAsk ?? deps.onAsk;
121
122
  const handWriteTools = toolRoster.entries.filter((e) => e.mountedBy === "hands" && (e.effect === "write" || e.effect === "idempotent")).map((e) => e.name);
122
123
  const hasEffectAwareGate = Boolean(policyLayers.length > 0 || (hooks?.preToolUse !== undefined && !preToolUseObservational));
123
- const destructiveMcpUngated = mcp.tools.some((t) => !irreversibleTools.has(t.name) && !egressTools.has(t.name) && (toolEffects.get(t.name) ?? "write") !== "read");
124
+ const peerWriteUngated = modePolicyOf(permissionMode.current).unstated === "ask"
125
+ ? []
126
+ : mcp.tools.filter((t) => t.permissionSeat === "passthrough" && !irreversibleTools.has(t.name) && !egressTools.has(t.name) && (toolEffects.get(t.name) ?? "write") !== "read").map((t) => t.name);
124
127
  const firstPartyWriteUngated = (spec.tools ?? [])
125
128
  .map((t) => t.name)
126
129
  .filter((name) => !irreversibleTools.has(name) && !egressTools.has(name) && toolEffects.get(name) === "write");
127
- if (!hasEffectAwareGate && (handWriteTools.length > 0 || destructiveMcpUngated || firstPartyWriteUngated.length > 0)) {
130
+ const handOrFirstPartyUngated = !hasEffectAwareGate && (handWriteTools.length > 0 || firstPartyWriteUngated.length > 0);
131
+ if (handOrFirstPartyUngated || peerWriteUngated.length > 0) {
128
132
  const what = [
129
- handWriteTools.length > 0 ? `hand tools [${handWriteTools.join(", ")}]` : undefined,
130
- destructiveMcpUngated ? "MCP write tools" : undefined,
131
- firstPartyWriteUngated.length > 0 ? `write tools [${firstPartyWriteUngated.join(", ")}]` : undefined,
133
+ !hasEffectAwareGate && handWriteTools.length > 0 ? `hand tools [${handWriteTools.join(", ")}]` : undefined,
134
+ peerWriteUngated.length > 0 ? `server write tools under ${permissionMode.current} [${peerWriteUngated.join(", ")}]` : undefined,
135
+ !hasEffectAwareGate && firstPartyWriteUngated.length > 0 ? `write tools [${firstPartyWriteUngated.join(", ")}]` : undefined,
132
136
  ]
133
137
  .filter(Boolean)
134
138
  .join(" + ");
@@ -141,6 +145,7 @@ export function prepareWiringManifest(input) {
141
145
  shapes.add(what);
142
146
  deps.onError?.(new Error(`write-capable ${what} are present but UNGATED โ€” wire an effect-aware tool policy (or a PreToolUse hook). ` +
143
147
  `bash/MCP irreversible actions are otherwise unadjudicated; the deployment sandbox/egress boundary is the only protection (design/77 ยง4, design/53 ยง2.H). ` +
148
+ (peerWriteUngated.length > 0 ? `Under ${permissionMode.current} a server's tools run with nobody stating them; a deny or ask rule naming them still gates them. ` : "") +
144
149
  `(This warning fires once per deployment per tool-set shape.)`), { phase: "config", sessionId });
145
150
  }
146
151
  }
@@ -49,9 +49,9 @@ export async function resumeApply(input) {
49
49
  ...(screenedMessage.actor !== undefined ? { actor: snapshotActorAssertion(screenedMessage.actor) } : {}),
50
50
  }));
51
51
  if (seat.expired) {
52
- throw new CheckpointError("steering.blocked_by_hook", seat.cause === "timeout"
53
- ? `the deployment's userPromptSubmit hook did not answer within its ${screenTimeoutMs}ms bound while screening this wake message; the resume was refused (fail-closed) and the checkpoint stays pending`
54
- : `the resume was cancelled while the deployment's userPromptSubmit hook was still screening this wake message; the checkpoint stays pending`);
52
+ if (seat.cause === "aborted")
53
+ throw new CheckpointError("checkpoint.resume_aborted", "the resume was aborted while the deployment's userPromptSubmit hook was still screening this wake message โ€” the checkpoint stays pending and is resumable with a live signal");
54
+ throw new CheckpointError("steering.blocked_by_hook", `the deployment's userPromptSubmit hook did not answer within its ${screenTimeoutMs}ms bound while screening this wake message; the resume was refused (fail-closed) and the checkpoint stays pending`);
55
55
  }
56
56
  decision = seat.value;
57
57
  }
@@ -49,9 +49,9 @@ export async function resumePreflight(input) {
49
49
  owner: runner.deps,
50
50
  }, (sig) => preflight(info, sig));
51
51
  if (seat.expired) {
52
- throw new CheckpointError("resume.preflight_rejected", seat.cause === "timeout"
53
- ? `the deployment's resumePreflight did not answer within its ${preflightTimeoutMs}ms deadline while screening this resume โ€” refused fail-closed (retry-later arm)${refusalTail}`
54
- : `the resume was cancelled while the deployment's resumePreflight was still screening it โ€” refused (retry-later arm)${refusalTail}`);
52
+ if (seat.cause === "aborted")
53
+ throw new CheckpointError("checkpoint.resume_aborted", "the resume was aborted while the deployment's resumePreflight was still screening it โ€” the checkpoint stays pending and is resumable with a live signal");
54
+ throw new CheckpointError("resume.preflight_rejected", `the deployment's resumePreflight did not answer within its ${preflightTimeoutMs}ms deadline while screening this resume โ€” refused fail-closed (retry-later arm)${refusalTail}`);
55
55
  }
56
56
  verdict = seat.value;
57
57
  }
@@ -3,6 +3,7 @@ const RETIRED_RUNNER_DEPS_KEYS = {
3
3
  legacyRewindBoundaryProbe: "the rewind refusal always names both possibilities (never existed / retired whole-tree era); a deployment that holds that era's table answers the question on its own surface",
4
4
  peerAdmission: "the peer lane's knob table is `RunnerDeps.peerLane` (the same fields: admission gate, delivery budgets, listing window)",
5
5
  graderEnvFactory: "the grader env reaches the repair loop as `RepairLoopConfig.graderEnv`, minted by the trusted control plane (`assertOracleIsolation` / `GraderEnv` unchanged)",
6
+ "hands.readCyberReminder": "a Read result carries only the reminders about that read (an empty file, an offset past the end, a file already in context) โ€” no standing content-safety tail, so there is nothing to switch; a deployment that wants that sentence states it in its own `appendSystemPrompt`",
6
7
  };
7
8
  export function assertNoRetiredRunnerDeps(deps) {
8
9
  assertNoRetiredKeys(deps, RETIRED_RUNNER_DEPS_KEYS, "RunnerDeps");
@@ -497,7 +497,9 @@ export interface RunnerDeps {
497
497
  * design/380 O2 โ€” deployment-supplied resume preflight, called INSIDE the pre-CAS ladder (after the
498
498
  * row-integrity rungs, immediately before the CAS) with a bounded deadline
499
499
  * ({@link resumePreflightTimeoutMs}). Refusal / throw / timeout โ‡’ typed `resume.preflight_rejected`
500
- * (CheckpointError closed-set addition); the DEFAULT/absent disposition is `retry_later` (the safe
500
+ * (CheckpointError closed-set addition); a wait ended by the RESUME SIGNAL is the caller's own
501
+ * cancellation and answers `checkpoint.resume_aborted` instead (one code at the resume entrance);
502
+ * the DEFAULT/absent disposition is `retry_later` (the safe
501
503
  * arm โ€” fail-closed hooks that just throw can never accidentally terminalize a row) โ€” the
502
504
  * entrance-screen posture of the wake-message hook (design/373 D2), generalized from "screen the
503
505
  * wake message" to "screen the resume". A deployment binding runs to targets (a device lane) checks
@@ -850,6 +850,13 @@ export declare function createAllowDenyPolicy(opts: {
850
850
  list: "allow" | "deny";
851
851
  message: string;
852
852
  }) => void;
853
+ /**
854
+ * What an `allow` list HIT means. `"grant"` (default): the deployment named this tool as permitted โ€” a statement
855
+ * (`decisionReason: "rule"`) that satisfies the fold's unstated rule for a passthrough-seat tool. `"narrowing"`: the
856
+ * list only bounds WHICH tools a lane may see (an agent definition's `allowTools`); a hit is scope, not consent, and
857
+ * answers a bare allow โ€” the mode's unstated question still goes to a person. The deny list is unaffected.
858
+ */
859
+ scope?: "grant" | "narrowing";
853
860
  }): NamedToolPolicy;
854
861
  /**
855
862
  * What `createApprovalPolicy`'s `approve` may resolve to: a boolean, or the `AskOutcome` OBJECT minus the
@@ -918,6 +925,13 @@ export declare function createApprovalPolicy(opts: {
918
925
  * short-circuit โ€” scanning continues, because a later policy may still `deny` (which outranks ask).
919
926
  * With no deny, the first `ask` wins; with neither, `allow`.
920
927
  *
928
+ * **An allow is STATED or not.** A layer that names the call (an allow rule, a list entry, an approver's yes, a mode's
929
+ * release) says WHY on its allow (`decisionReason`); a bare allow is "no objection" โ€” an empty table, a list the call is
930
+ * not on, a layer that does not know the tool. The combined allow carries the FIRST stated reason in layer order, and a
931
+ * statement binds the arguments its layer judged: a layer that rewrites the arguments voids every statement before it (its
932
+ * own allow, if it states one, speaks for the new arguments). For a tool whose permission seat is `passthrough` an
933
+ * unstated allow is not a permission โ€” the gate puts the mode's question to it; for every other tool the two forms run alike.
934
+ *
921
935
  * **Idempotency requirement:** because the fold continues past an `ask` to look for a later `deny`, a
922
936
  * policy's `check()` can be invoked even after an earlier policy returned `ask`. `check()` MUST be
923
937
  * idempotent โ€” repeated calls with the same {@link ToolCallRequest} return the same decision and
@@ -253,6 +253,7 @@ export function constraintChainDigest(chain) {
253
253
  return `cpv1:${createHash("sha256").update(stableJson(chain)).digest("hex")}`;
254
254
  }
255
255
  const ALLOW = Object.freeze({ action: "allow" });
256
+ const ALLOW_BY_RULE = Object.freeze({ action: "allow", decisionReason: "rule" });
256
257
  const RETIRED_TEXT_FIELD = "reason";
257
258
  export const DECISION_MEMBERS = Object.keys({
258
259
  action: 0, updatedInput: 0, message: 0, decisionReason: 0, requiresRealApproval: 0, denialLimitFallback: 0, persistedRuleShadowed: 0, probeReason: 0,
@@ -414,7 +415,7 @@ export function createAllowDenyPolicy(opts) {
414
415
  if (allow && !nameListHolds(allow, req) && !namespacedCoveringHit(allowCovering, req)) {
415
416
  return { action: "deny", message: `tool "${req.toolName}" is not in the allowlist` };
416
417
  }
417
- return ALLOW;
418
+ return allow && opts.scope !== "narrowing" ? ALLOW_BY_RULE : ALLOW;
418
419
  },
419
420
  };
420
421
  }
@@ -486,11 +487,11 @@ export function createApprovalPolicy(opts) {
486
487
  : `approval denied for "${req.toolName}"${reading.note !== undefined ? `\nReviewer note:\n${delimitUntrusted("reviewer note", reading.note, REVIEWER_NOTE_MAX_BODY)}` : ""}`,
487
488
  }, reading.settlement);
488
489
  }
489
- return attest({ action: "allow" }, reading.settlement);
490
+ return attest({ action: "allow", decisionReason: "mode" }, reading.settlement);
490
491
  }
491
492
  const okRaw = ok;
492
493
  if (okRaw === true)
493
- return attest({ action: "allow" }, settledByDecider("person", "allowed", undefined));
494
+ return attest({ action: "allow", decisionReason: "mode" }, settledByDecider("person", "allowed", undefined));
494
495
  if (okRaw !== false) {
495
496
  return attest({
496
497
  action: "deny",
@@ -500,7 +501,9 @@ export function createApprovalPolicy(opts) {
500
501
  }
501
502
  return attest({ action: "deny", message: `approval denied for "${req.toolName}"` }, settledByDecider("person", "refused", undefined));
502
503
  }
503
- if (opts.denyByDefault && !nameListHolds(auto, req) && !namespacedCoveringHit(autoCovering, req)) {
504
+ if (nameListHolds(auto, req) || namespacedCoveringHit(autoCovering, req))
505
+ return ALLOW_BY_RULE;
506
+ if (opts.denyByDefault) {
504
507
  return { action: "deny", message: `tool "${req.toolName}" requires explicit allow` };
505
508
  }
506
509
  return ALLOW;
@@ -540,6 +543,7 @@ export function combinePolicies(...policies) {
540
543
  let asked;
541
544
  let current = req;
542
545
  let rewrite;
546
+ let stated;
543
547
  const records = {};
544
548
  let anyRecord = false;
545
549
  let ruleAskText;
@@ -565,6 +569,10 @@ export function combinePolicies(...policies) {
565
569
  if (d.updatedInput !== undefined) {
566
570
  current = { ...current, args: d.updatedInput };
567
571
  rewrite = d;
572
+ stated = d.action === "allow" ? d.decisionReason : undefined;
573
+ }
574
+ else if (d.action === "allow" && stated === undefined && d.decisionReason !== undefined) {
575
+ stated = d.decisionReason;
568
576
  }
569
577
  if (d.action === "allow" && hasEngineAttestation(d)) {
570
578
  transferEngineAttestations(d, records);
@@ -605,7 +613,8 @@ export function combinePolicies(...policies) {
605
613
  FOLDED_CLASSES_SEAT.set(out, Object.freeze([...folded]));
606
614
  return out;
607
615
  }
608
- const allowed = rewrite ?? ALLOW;
616
+ const base = rewrite ?? ALLOW;
617
+ const allowed = stated !== undefined && base.decisionReason !== stated ? { ...base, decisionReason: stated } : base;
609
618
  if (!anyRecord)
610
619
  return allowed;
611
620
  const out = { ...allowed };
@@ -659,7 +668,7 @@ export function createCoarseCommandNamePolicy(opts) {
659
668
  if (allow && !allow.has(parsed.name)) {
660
669
  return fallback(`command "${parsed.name}" is not in the allowlist`);
661
670
  }
662
- return ALLOW;
671
+ return allow ? ALLOW_BY_RULE : ALLOW;
663
672
  },
664
673
  }, "sandbox_local");
665
674
  }
@@ -274,6 +274,16 @@ export interface ToolSpec<TParams extends TSchema = TSchema> {
274
274
  * prepare) โ€” omitting `effect` is fine (the default IS `write`).
275
275
  */
276
276
  egress?: true;
277
+ /**
278
+ * The tool's PERMISSION SEAT. `"passthrough"`: the tool states no permission of its own โ€” under an asking permission mode
279
+ * (`default` / `acceptEdits` / `plan` / `auto` / `dontAsk`) a call to it runs only when some layer STATES an allow (an allow
280
+ * rule or allow-list entry naming it, an approver's yes, a PreToolUse hook's own `allow`); a policy fold in which nobody
281
+ * stated it ("no objection") gets the mode's question (class `unstated`, origin `policy`), which dontAsk refuses, auto's
282
+ * classifier may answer, and `bypassPermissions` never raises. The MCP and A2A materializers set it on a remote peer's
283
+ * tool. Absent: the tool answers for itself (the engine's own stations and the deployment's declarations). Tightening only โ€”
284
+ * it can make a tool ask, never let one run.
285
+ */
286
+ permissionSeat?: "passthrough";
277
287
  /**
278
288
  * design/77 ยง4 (Gate 4 โ€” irreversibility axis): how reversible this tool's effect is. A THIRD axis,
279
289
  * orthogonal to {@link effect} (repeat-safety) and {@link egress} (blast-radius). For office/finance
@@ -81,6 +81,7 @@ export function defineTool(spec, options) {
81
81
  ...(spec.effect ? { effect: spec.effect } : {}),
82
82
  ...(spec.contentOrigin !== undefined ? { contentOrigin: spec.contentOrigin } : {}),
83
83
  ...(spec.egress ? { egress: true } : {}),
84
+ ...(spec.permissionSeat === "passthrough" ? { permissionSeat: "passthrough" } : {}),
84
85
  ...(spec.irreversibility !== undefined ? { irreversibility: spec.irreversibility } : {}),
85
86
  ...(spec.reversibilityProbe ? { reversibilityProbe: spec.reversibilityProbe } : {}),
86
87
  ...(spec.offload !== undefined ? { offload: spec.offload } : {}),
@@ -772,6 +772,12 @@ export interface AgentTool<TParameters extends TSchema = TSchema, TDetails = unk
772
772
  * repeat-safety effect below its fail-closed `write` default (only the caller's `toolAxes` does). Absent on non-MCP tools.
773
773
  */
774
774
  mcpReadOnlyHint?: true;
775
+ /**
776
+ * The tool's permission seat โ€” see `ToolSpec.permissionSeat` (src/core/tool-spec.ts). `"passthrough"` = the tool states no
777
+ * permission of its own; set by the MCP and A2A materializers on a remote peer's tool and carried by `defineTool` from a
778
+ * spec. Read per call off the leg's live mount array (`mountedToolPermissionSeat`). Absent โ‡’ the tool answers for itself.
779
+ */
780
+ permissionSeat?: "passthrough";
775
781
  /**
776
782
  * design/120 P1.5 (CC `isConcurrencySafe(parsedInput)` parity): INPUT-level concurrency refinement
777
783
  * for a tool whose safety depends on its arguments โ€” CC's only real consumer is
@@ -118,7 +118,7 @@ export function applyWorkflowAgentType(spec, agentType, registry) {
118
118
  if (out.memoryPersistenceCapable === undefined && def.memoryPersistenceCapable !== undefined)
119
119
  out.memoryPersistenceCapable = def.memoryPersistenceCapable;
120
120
  if (def.allowTools !== undefined || def.denyTools !== undefined) {
121
- const perType = createAllowDenyPolicy({ ...(def.allowTools ? { allow: def.allowTools } : {}), ...(def.denyTools ? { deny: def.denyTools } : {}) });
121
+ const perType = createAllowDenyPolicy({ ...(def.allowTools ? { allow: def.allowTools } : {}), ...(def.denyTools ? { deny: def.denyTools } : {}), scope: "narrowing" });
122
122
  out.toolPolicy = out.toolPolicy ? combinePolicies(out.toolPolicy, perType) : perType;
123
123
  }
124
124
  return out;