@sema-agent/core 7.39.2 → 7.40.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 (42) hide show
  1. package/CHANGELOG.md +60 -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/gate-fold.js +5 -0
  6. package/dist/core/gate-lanes.js +8 -4
  7. package/dist/core/hands-band.d.ts +0 -12
  8. package/dist/core/hooks.d.ts +3 -0
  9. package/dist/core/hooks.js +4 -1
  10. package/dist/core/mcp.js +1 -1
  11. package/dist/core/mode-policy.d.ts +17 -2
  12. package/dist/core/mode-policy.js +13 -6
  13. package/dist/core/permission-rule-model.d.ts +29 -0
  14. package/dist/core/permission-rule-model.js +15 -2
  15. package/dist/core/permission-rules.d.ts +2 -10
  16. package/dist/core/permission-rules.js +22 -19
  17. package/dist/core/runner/denial-limit-arms.js +1 -1
  18. package/dist/core/runner/gate-exit.d.ts +5 -1
  19. package/dist/core/runner/permission-rule-lanes.js +7 -5
  20. package/dist/core/runner/persisted-path-rows.js +3 -3
  21. package/dist/core/runner/prepare-gate-stations.d.ts +1 -1
  22. package/dist/core/runner/prepare-gate-stations.js +2 -0
  23. package/dist/core/runner/prepare-hands-readface.d.ts +1 -1
  24. package/dist/core/runner/prepare-hands-readface.js +0 -1
  25. package/dist/core/runner/prepare-policy-chain.js +11 -7
  26. package/dist/core/runner/prepare-wiring-manifest.d.ts +1 -1
  27. package/dist/core/runner/prepare-wiring-manifest.js +10 -5
  28. package/dist/core/runner/retired-runner-deps.js +1 -0
  29. package/dist/core/tool-policy.d.ts +14 -0
  30. package/dist/core/tool-policy.js +15 -6
  31. package/dist/core/tool-spec.d.ts +10 -0
  32. package/dist/core/tools.js +1 -0
  33. package/dist/engine/loop/types.d.ts +6 -0
  34. package/dist/orchestration/workflow.js +1 -1
  35. package/dist/tools/fs/fs-read.d.ts +1 -1
  36. package/dist/tools/fs/fs-read.js +3 -5
  37. package/dist/tools/fs/fs-shared.d.ts +0 -27
  38. package/dist/tools/fs/fs-shared.js +0 -3
  39. package/dist/tools/fs/index.d.ts +1 -9
  40. package/dist/tools/fs/index.js +1 -1
  41. package/package.json +1 -1
  42. package/wire-changes/core@7.40.0.json +376 -0
@@ -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
  }
@@ -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");
@@ -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;
@@ -7,4 +7,4 @@ export declare function createReadFileTool(env: ExecutionEnv, state: ReadFileSta
7
7
  taskId?: string;
8
8
  principal?: string;
9
9
  toolCallId?: string;
10
- }) => boolean, readCyberReminder?: boolean, readDeny?: import("./read-deny.js").ReadDenyMatcher, readFace?: import("./containment-face.js").ContainmentFace, reminderMark?: string, reminderDisclosureCounts?: ReminderDisclosureCounts): AgentTool;
10
+ }) => boolean, readDeny?: import("./read-deny.js").ReadDenyMatcher, readFace?: import("./containment-face.js").ContainmentFace, reminderMark?: string, reminderDisclosureCounts?: ReminderDisclosureCounts): AgentTool;
@@ -5,13 +5,12 @@ import { decodeTextBytes } from "./encoding.js";
5
5
  import { isNotebookPath, parseNotebookCells, renderNotebookCells, stripNotebookImageData, NOTEBOOK_IMAGE_BASE64_BUDGET } from "./notebook.js";
6
6
  import { MCP_IMAGE_MAX_BASE64, IMAGE_TARGET_RAW_SIZE } from "../../core/mcp.js";
7
7
  import { PDF_MAX_PAGES_PER_READ, pdfMagicMatches } from "./pdf.js";
8
- import { MAX_READ_BYTES, SLICED_READ_MAX_BYTES, MAX_IMAGE_READ_BYTES, MAX_IMAGE_DOWNSAMPLE_INPUT_BYTES, NO_DOWNSAMPLER_IMAGE_CAP_HINT, resolveAutoDownsampler, MAX_READ_OUTPUT_CHARS, readCyberReminderText, FILE_PATH_PARAMS, countLines, seededFileUnchangedReminder, enoentMessage, } from "./fs-shared.js";
8
+ import { MAX_READ_BYTES, SLICED_READ_MAX_BYTES, MAX_IMAGE_READ_BYTES, MAX_IMAGE_DOWNSAMPLE_INPUT_BYTES, NO_DOWNSAMPLER_IMAGE_CAP_HINT, resolveAutoDownsampler, MAX_READ_OUTPUT_CHARS, FILE_PATH_PARAMS, countLines, seededFileUnchangedReminder, enoentMessage, } from "./fs-shared.js";
9
9
  import { readPdfFile, pdfResultToToolReturn } from "./fs-pdf.js";
10
10
  import { openSystemReminder } from "../../core/reminder-mint.js";
11
11
  import { discloseReminderShaped } from "../../core/reminder-disclosure.js";
12
12
  import { toolFace } from "../../core/tool-catalog-entries.js";
13
- export function createReadFileTool(env, state, rootCanonical, cwdRef, additionalRoots, imageDownsampler, pdfCapabilities, bgOutputReadExemption, readCyberReminder, readDeny, readFace, reminderMark, reminderDisclosureCounts) {
14
- const cyberReminder = readCyberReminder === false ? "" : readCyberReminderText(reminderMark);
13
+ export function createReadFileTool(env, state, rootCanonical, cwdRef, additionalRoots, imageDownsampler, pdfCapabilities, bgOutputReadExemption, readDeny, readFace, reminderMark, reminderDisclosureCounts) {
15
14
  const reminderDisclosureWindows = new Map();
16
15
  const pdfReminderDisclosure = (key) => ({
17
16
  mark: reminderMark,
@@ -244,7 +243,6 @@ export function createReadFileTool(env, state, rootCanonical, cwdRef, additional
244
243
  return {
245
244
  content: [
246
245
  ...bodyBlocks,
247
- ...(cyberReminder ? [{ type: "text", text: cyberReminder }] : []),
248
246
  ...(nbDisclosure.trailer !== undefined ? [{ type: "text", text: nbDisclosure.trailer }] : []),
249
247
  ],
250
248
  details: { type: "notebook", file: { filePath: path, cells: parsed.cells.map(stripNotebookImageData) } },
@@ -321,7 +319,7 @@ export function createReadFileTool(env, state, rootCanonical, cwdRef, additional
321
319
  counts: reminderDisclosureCounts,
322
320
  });
323
321
  return {
324
- content: `${nbFallbackPrefix}${header}${body}${cyberReminder}${disclosure.trailer !== undefined ? `\n${disclosure.trailer}` : ""}`,
322
+ content: `${nbFallbackPrefix}${header}${body}${disclosure.trailer !== undefined ? `\n${disclosure.trailer}` : ""}`,
325
323
  details: {
326
324
  type: "text",
327
325
  file: {
@@ -145,33 +145,6 @@ export declare function resolveAutoDownsampler(): Promise<ImageDownsampler | und
145
145
  * divergence is a fidelity delta, not a correctness one (the partial-view marker always states the
146
146
  * served window truthfully). */
147
147
  export declare const MAX_READ_OUTPUT_CHARS = 100000;
148
- /** Per-read content-safety reminder appended after a text read.
149
- *
150
- * Attribution, stated precisely because either half alone is wrong. The upstream 88 source DOES carry
151
- * the same mechanism — `FileReadTool`'s `CYBER_RISK_MITIGATION_REMINDER`, appended after every text
152
- * read, behind a PER-MODEL exemption gate. The 220 baseline does NOT: the text is gone, retired once
153
- * the mitigation moved inside the serving model.
154
- *
155
- * This engine keeps it because a BYOM deployment cannot assume its serving model carries the
156
- * mitigation, and adjacency is the whole mechanism (a reminder in a system prompt hundreds of turns
157
- * back is not what makes a model treat the bytes in front of it as data). It costs ≈50 tokens per text
158
- * read, which is why the exemption gate 88 had is restored rather than the append being unconditional:
159
- * see `HandsToolkitOptions.readCyberReminder`. Default ON — a deployment that knows its model is
160
- * covered turns it off explicitly.
161
- *
162
- * COVERAGE, stated because the switch is easy to over-read: the reminder rides the plain-text read and
163
- * the notebook projection. PDF text extraction returns through its own result builder and has never
164
- * carried it, so extracted PDF text reaches the model without this mitigation whatever the switch says.
165
- * That gap predates the switch; its IMPERSONATION half is closed — the PDF text legs carry the same
166
- * detect-and-disclose trailer as this lane (reminder-shaped bytes in extracted PDF text are disclosed
167
- * at the outlet, fs-pdf.ts), and their bodies are fenced. The CYBER half (this malware-alertness
168
- * sentence) still does not ride PDF text and stays an open item rather than closed silently —
169
- * widening it to a third surface is a change to what every PDF read costs, not a wiring fix.
170
- *
171
- * A FUNCTION over the session mark, not a constant: the open tag is rendered by the mint home so it
172
- * carries the run's provenance mark; the sentence bytes are unchanged, and a markless call renders
173
- * the bare form verbatim. */
174
- export declare function readCyberReminderText(mark: string | undefined): string;
175
148
  export declare const BASH_DEFAULT_TIMEOUT_SEC = 120;
176
149
  export declare const BASH_MAX_TIMEOUT_SEC = 600;
177
150
  export declare const BASH_DEFAULT_TIMEOUT_MS: number;