@sema-agent/core 5.29.0 → 5.30.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 (62) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/dist/agents/send-message-tool.js +2 -0
  3. package/dist/agents/subagent.d.ts +2 -0
  4. package/dist/agents/subagent.js +6 -0
  5. package/dist/agents/teacher.js +2 -0
  6. package/dist/agents/verify.js +2 -0
  7. package/dist/core/auto-compaction.d.ts +5 -1
  8. package/dist/core/auto-compaction.js +10 -1
  9. package/dist/core/checkpoint-store.d.ts +51 -5
  10. package/dist/core/checkpoint-store.js +2 -1
  11. package/dist/core/hooks.d.ts +12 -1
  12. package/dist/core/hooks.js +8 -2
  13. package/dist/core/permission-rules.js +2 -2
  14. package/dist/core/runner/prepare-task.d.ts +21 -5
  15. package/dist/core/runner/prepare-task.js +105 -19
  16. package/dist/core/runner/runtask.js +29 -3
  17. package/dist/core/runner/session-rule-policy.d.ts +3 -2
  18. package/dist/core/runner/tool-output-projection.js +1 -1
  19. package/dist/core/sensitive-path-policy.js +5 -16
  20. package/dist/core/store-contracts/tool-result-store-contract.js +23 -0
  21. package/dist/core/tighten-task-spec.js +18 -0
  22. package/dist/core/tool-policy.d.ts +20 -1
  23. package/dist/core/tool-policy.js +31 -4
  24. package/dist/core/tool-result-store.js +3 -1
  25. package/dist/core/types.d.ts +55 -0
  26. package/dist/engine/harness/types.d.ts +10 -0
  27. package/dist/index.d.ts +3 -1
  28. package/dist/index.js +3 -1
  29. package/dist/orchestration/run-workflow-tool.d.ts +21 -0
  30. package/dist/orchestration/run-workflow-tool.js +6 -3
  31. package/dist/orchestration/workflow-primitives.d.ts +10 -1
  32. package/dist/orchestration/workflow-primitives.js +12 -1
  33. package/dist/prompt-assembly/epoch.js +2 -0
  34. package/dist/prompt-assembly/packs/sema-default.js +2 -2
  35. package/dist/prompt-assembly/types.d.ts +4 -0
  36. package/dist/prompts/default.d.ts +14 -9
  37. package/dist/prompts/default.js +13 -3
  38. package/dist/tools/fs/bash-readonly-classifier.d.ts +21 -0
  39. package/dist/tools/fs/bash-readonly-classifier.js +11 -0
  40. package/dist/tools/fs/fs-bash.d.ts +7 -0
  41. package/dist/tools/fs/fs-bash.js +8 -3
  42. package/dist/tools/fs/fs-pdf.d.ts +1 -1
  43. package/dist/tools/fs/fs-pdf.js +2 -2
  44. package/dist/tools/fs/fs-read.d.ts +1 -1
  45. package/dist/tools/fs/fs-read.js +11 -7
  46. package/dist/tools/fs/fs-search-tools.d.ts +4 -2
  47. package/dist/tools/fs/fs-search-tools.js +15 -8
  48. package/dist/tools/fs/fs-shared.d.ts +5 -1
  49. package/dist/tools/fs/fs-shared.js +8 -3
  50. package/dist/tools/fs/index.d.ts +18 -0
  51. package/dist/tools/fs/index.js +13 -2
  52. package/dist/tools/fs/read-deny.d.ts +105 -0
  53. package/dist/tools/fs/read-deny.js +151 -0
  54. package/dist/tools/fs/read-face.d.ts +43 -0
  55. package/dist/tools/fs/read-face.js +38 -0
  56. package/dist/tools/fs/repo-map.d.ts +3 -1
  57. package/dist/tools/fs/repo-map.js +11 -5
  58. package/dist/tools/fs/safety.d.ts +33 -11
  59. package/dist/tools/fs/safety.js +88 -7
  60. package/dist/tools/fs/search.d.ts +54 -5
  61. package/dist/tools/fs/search.js +103 -21
  62. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,68 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.30.0 — 2026-08-13
4
+
5
+ No API-BREAKING changes (exports grow only: `resolveReadFace`, `ReadFace`, `ReadFaceInputs`,
6
+ `READ_FACE_DEFAULT_DENY_ENTRIES`, `compileReadDeny`, `ReadDenyEntry`, `ReadDenyMatcher`,
7
+ `NormalizedReadDenyEntry`; `TaskSpec`/`RunnerDeps`/`HandsToolkitOptions` gain `readFace`/
8
+ `readDenyPatterns`). Narrowings are tighten-direction and named below.
9
+
10
+ ### Added
11
+
12
+ - **A read-face containment knob (`readFace: "open" | "roots"`, default `"roots"` — byte-compatible
13
+ with prior behavior).** `TaskSpec`/`RunnerDeps`/`HandsToolkitOptions` each gain the seat; task-layer
14
+ may only tighten under governance, deployment-layer sets the default. Beside a read-only (verifier)
15
+ mount, a task-seat `"open"` is a genuine contradiction and refuses loudly; a deployment-seat
16
+ `"open"` there silently clamps to `"roots"` (a deployment default cannot override a load-bearing
17
+ containment wall). Every workflow/subagent/SendMessage delegation lane inherits the resolved face
18
+ tighten-only (stricter-wins), including `run_workflow`'s script-spawned children, which previously
19
+ did not inherit it at all. Checkpoint schema bumps to v9 (`FACE_CHECKPOINT_VERSION`) to carry the
20
+ resolved face across suspend/resume; an absent v9 section on an older checkpoint row is the default
21
+ posture (roots) — no existing row's behavior changes.
22
+ - **A built-in sensitive-path READ deny set**, exported as `READ_FACE_DEFAULT_DENY_ENTRIES` (SSH
23
+ keys, cloud/VCS credential files, browser profile directories, crypto wallet files, shell history,
24
+ and similar). Enforced under BOTH containment modes — an `"open"` face does not exempt it — and not
25
+ shrinkable by any deployment configuration, including the `readFace: "roots"` compatibility seat
26
+ (the two axes are orthogonal). `.env`-family files are deliberately excluded (write-side guards
27
+ already cover them). Deployments may extend the table (`readDenyPatterns` on the same three seats,
28
+ additive-only; `compileReadDeny` lets a host pre-validate additions with the engine's own rules).
29
+
30
+ ### Narrowed (behavior)
31
+
32
+ - **A UNC-rooted deployment's `..` can no longer walk out of its declared root.** Lexical folding now
33
+ normalizes `..` segments and Win32 device-namespace prefixes (`\\?\`, `\\.\`) before they reach the
34
+ containment check; this is a security repair with no compatibility escape hatch (unaffected by any
35
+ "keep old behavior" knob).
36
+ - **`bashReversibilityProbe` no longer auto-executes an operand with an unexpanded shell glob
37
+ metacharacter it can't verify against the deny/boundary check.** A command whose read target is
38
+ undecided at the classifier layer now routes to the ask flow instead of running unattended.
39
+ - **A hands-less resume no longer clones the `realApproval` governance marker onto an ungoverned
40
+ re-park.** The seed-carry now picks only `{face, denyEntries}`; previously, a worker resuming into a
41
+ plain (non-governed) ask could inherit a stale `realApproval: true` and permanently wedge the row on
42
+ its next resume (`real_approval_damaged`, fail-closed by design).
43
+ - **The auto-mode classifier can no longer resolve an ask that originated from an explicit
44
+ user-written permission rule or a hook-raised promotion.** Both are now excluded from classifier
45
+ judgment and the persisted-rule-clearing lane, closing two asymmetric gaps against the existing
46
+ `decisionReason === "hook"` exclusion.
47
+ - **`resolveAsk`'s object-arm reads share one exception boundary.** A throwing getter on `settledBy`/
48
+ `allow`/`updatedInput` now fails closed (deny) instead of propagating an unhandled rejection; a
49
+ non-boolean `allow` is refused loudly by name instead of silently rendering as a human refusal.
50
+
51
+ ### Fixed
52
+
53
+ - `run_workflow`'s durable-store off switch (`spec.checkpointStore === null`) and design/148's
54
+ center-artifact inheritance (`centerArtifactDigest`/`centerSourceRevision`) were dead reads on the
55
+ tool's auto-mounted execute context (which never carries them) — neither ever reached a
56
+ script-spawned child. Both now fall back to a mount-time dependency, matching the tool's other two
57
+ ctx-or-deps fields.
58
+ - Under `readFace: "open"`, compaction's attachment-reader legs (`readTaskFile`/
59
+ `normalizeAttachmentPath`) refused files the Read tool had already legitimately served out-of-root;
60
+ they now thread the resolved face like the structured read tools do.
61
+ - `deleteBySession` joins the `ToolResultStore` contract kit as an honest optional member: when
62
+ implemented, the four-state semantics (same-session rows deleted, other-session rows untouched,
63
+ unowned rows counted not deleted, idempotent re-delete) are asserted; when absent, the contract
64
+ probe reports it by name instead of silently passing.
65
+
3
66
  ## 5.29.0 — 2026-08-13
4
67
 
5
68
  No API-BREAKING changes (exports grow only: `AskOutcome.reason`, `AskRequest.isDelegatedChild`).
@@ -680,6 +680,8 @@ export function createSendMessageTool(opts) {
680
680
  ...(ctx.onQuestion !== undefined && row.sessionScoped !== true ? { currentOnQuestion: ctx.onQuestion } : {}),
681
681
  currentClamps: {
682
682
  ...(ctx.handsReadOnly === true ? { handsReadOnly: true } : {}),
683
+ ...(ctx.readFace === "roots" ? { readFaceRoots: true } : {}),
684
+ ...(ctx.readDenyPatterns !== undefined ? { readDenyPatterns: ctx.readDenyPatterns } : {}),
683
685
  ...(ctx.interactiveTools === false ? { interactiveTools: false } : {}),
684
686
  ...(ctx.oneShot === true ? { oneShot: true } : {}),
685
687
  ...(ctx.memoryPersistenceCapable === false ? { memoryPersistenceCapable: false } : {}),
@@ -404,6 +404,8 @@ export declare function createSubagentResume(deps: {
404
404
  interactiveTools?: false;
405
405
  oneShot?: true;
406
406
  memoryPersistenceCapable?: false;
407
+ readFaceRoots?: true;
408
+ readDenyPatterns?: readonly import("../tools/fs/read-deny.js").ReadDenyEntry[];
407
409
  };
408
410
  /** The RESUMING caller's own handback-review seat (its trusted `ToolExecuteContext.autoModeReview`),
409
411
  * same turn-bound reasoning as `currentOnQuestion` above: a resumed cycle is a completion like any
@@ -520,6 +520,10 @@ export function createSubagentResume(deps) {
520
520
  requireExistingSession: true,
521
521
  ...(deps.currentOnQuestion !== undefined ? { onQuestion: deps.currentOnQuestion } : {}),
522
522
  ...(entry.specSnapshot.handsReadOnly === true || deps.currentClamps?.handsReadOnly === true ? { handsReadOnly: true } : {}),
523
+ ...(entry.specSnapshot.readFace === "roots" || deps.currentClamps?.readFaceRoots === true ? { readFace: "roots" } : {}),
524
+ ...(entry.specSnapshot.readDenyPatterns !== undefined || deps.currentClamps?.readDenyPatterns !== undefined
525
+ ? { readDenyPatterns: [...(entry.specSnapshot.readDenyPatterns ?? []), ...(deps.currentClamps?.readDenyPatterns ?? [])] }
526
+ : {}),
523
527
  ...(entry.specSnapshot.interactiveTools === false || deps.currentClamps?.interactiveTools === false ? { interactiveTools: false } : {}),
524
528
  ...(entry.specSnapshot.oneShot === true || deps.currentClamps?.oneShot === true ? { oneShot: true } : {}),
525
529
  ...(entry.specSnapshot.memoryPersistenceCapable === false || deps.currentClamps?.memoryPersistenceCapable === false
@@ -1693,6 +1697,8 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1693
1697
  ...(ctx.onQuestion !== undefined ? { onQuestion: ctx.onQuestion } : {}),
1694
1698
  ...(ctx.interactiveTools === false ? { interactiveTools: false } : {}),
1695
1699
  ...(ctx.handsReadOnly === true ? { handsReadOnly: true } : {}),
1700
+ ...(ctx.readFace === "roots" ? { readFace: "roots" } : {}),
1701
+ ...(ctx.readDenyPatterns !== undefined ? { readDenyPatterns: [...ctx.readDenyPatterns] } : {}),
1696
1702
  ...(childMemoryPersistenceCapable !== undefined ? { memoryPersistenceCapable: childMemoryPersistenceCapable } : {}),
1697
1703
  ...(ctx.oneShot === true ? { oneShot: true } : {}),
1698
1704
  ...(ctx.clientContext !== undefined ? { clientContext: ctx.clientContext } : {}),
@@ -180,6 +180,8 @@ async function runTeacherCore(runner, studentSpec, teacher) {
180
180
  ...(studentSpec.clientContext !== undefined ? { clientContext: studentSpec.clientContext } : {}),
181
181
  ...(studentSpec.promptProfile !== undefined ? { promptProfile: studentSpec.promptProfile } : {}),
182
182
  ...(studentSpec.handsReadOnly === true ? { handsReadOnly: true } : {}),
183
+ ...(studentSpec.readFace === "roots" ? { readFace: "roots" } : {}),
184
+ ...(studentSpec.readDenyPatterns !== undefined ? { readDenyPatterns: [...studentSpec.readDenyPatterns] } : {}),
183
185
  ...(studentSpec.interactiveTools === false ? { interactiveTools: false } : {}),
184
186
  ...(studentSpec.excludeTools !== undefined ? { excludeTools: [...studentSpec.excludeTools] } : {}),
185
187
  ...(studentSpec.deferTools !== undefined ? { deferTools: [...studentSpec.deferTools] } : {}),
@@ -99,6 +99,8 @@ export async function verifyCompleted(runner, result, specBase, objective, confi
99
99
  roles: specBase.roles,
100
100
  tools: verifierTools,
101
101
  handsReadOnly: config.verifierHandsReadOnly ?? true,
102
+ ...(specBase.readFace === "roots" ? { readFace: "roots" } : {}),
103
+ ...(specBase.readDenyPatterns !== undefined ? { readDenyPatterns: [...specBase.readDenyPatterns] } : {}),
102
104
  interactiveTools: false,
103
105
  outputSchema: VerdictSchema,
104
106
  enableBlockedReport: false,
@@ -244,7 +244,11 @@ export interface MaybeCompactOptions {
244
244
  */
245
245
  workingFileAttachments?: {
246
246
  /** Read a task workspace file; null/throw = skip it. Wired by the Runner from the task's ExecutionEnv. */
247
- readFile: (path: string) => Promise<string | null>;
247
+ readFile: (path: string) => Promise<string | null | {
248
+ withheld: {
249
+ pattern: string;
250
+ };
251
+ }>;
248
252
  /** Max modified files to attach. Default 3. */
249
253
  maxFiles?: number;
250
254
  /** Per-file char cap (≈4 chars/token). Default 16_000 (~4k tokens). */
@@ -397,12 +397,21 @@ export async function maybeCompact(opts) {
397
397
  if (remaining <= 0)
398
398
  break;
399
399
  let content = null;
400
+ let withheldPattern;
400
401
  try {
401
- content = await att.readFile(path);
402
+ const r = await att.readFile(path);
403
+ if (typeof r === "string")
404
+ content = r;
405
+ else if (r !== null && typeof r === "object")
406
+ withheldPattern = r.withheld.pattern;
402
407
  }
403
408
  catch {
404
409
  content = null;
405
410
  }
411
+ if (withheldPattern !== undefined) {
412
+ blocks.push(`<working-file path="${escapeWorkingFilePathAttr(path)}" withheld="content withheld: matches the sensitive-path read deny list (pattern ${escapeWorkingFilePathAttr(withheldPattern)})"/>`);
413
+ continue;
414
+ }
406
415
  if (content === null)
407
416
  continue;
408
417
  const cap = Math.min(perFileCap, remaining);
@@ -647,6 +647,37 @@ export interface CheckpointState {
647
647
  /** design/44: the hand's read-file state (content hashes), serialized from the hands-toolkit closure
648
648
  * (NOT part of Prepared — §15.2 net-add #8). Without it a resumed `edit_file` is rejected "not read". */
649
649
  readFileState?: Array<[string, ReadEntry]>;
650
+ /**
651
+ * design/199 — the READ-face containment state at suspend (v9,
652
+ * {@link FACE_CHECKPOINT_VERSION}): the RESOLVED face plus the NORMALIZED deny-set ADDITIONS in
653
+ * force (deployment ∪ task layers; the built-in table is engine code, never frozen here — a newer
654
+ * engine's built-ins apply on resume as everywhere else). Written ONLY when the posture is
655
+ * non-default (face "open", or additions present) — an absent section IS the default posture and
656
+ * resumes as roots + built-ins on any worker. Resume folds STRICTER-WINS: face = roots unless the
657
+ * section says open AND the live resolution says open; deny additions = frozen ∪ live (add-only,
658
+ * monotone — a resume can never widen what the cut leg could read).
659
+ *
660
+ * The design's r3/r4 rounds carried a third key (the workspace domain at cut) to evaluate
661
+ * domain-scoped deny entries under their frozen domain; the D-4 ruling (2026-08-13) removed the
662
+ * scoped tier entirely, leaving that key without a consumer — it is deliberately NOT persisted
663
+ * (dead fields in a durable format are the drift surface the liveness gate exists to catch).
664
+ */
665
+ readFace?: {
666
+ face: "open" | "roots";
667
+ denyEntries?: Array<{
668
+ pattern: string;
669
+ caseSensitive: boolean;
670
+ }>;
671
+ /** codex r2 — REDUNDANT governance marker: `true` iff the row's gate was minted WITH a
672
+ * {@link RealApprovalGateBit}. v8's bit-required invariant was version-keyed and v9 (face)
673
+ * outranks v8 in the stamp, so without this a v9 GOVERNED row whose `gate.realApproval` member
674
+ * was dropped in storage would pass pre-CAS and be redeemable on an unwired worker — the exact
675
+ * #130/#120 corruption/downgrade hole, reopened one version up. The resume pre-CAS ladder
676
+ * refuses a v9+ row whose section says `realApproval: true` while the gate carries no
677
+ * well-formed bit (same protection level as v8: a coherent rewrite of BOTH sites remains out of
678
+ * scope for any in-row invariant). Absent = the row was minted ungoverned. */
679
+ realApproval?: true;
680
+ };
650
681
  /**
651
682
  * design/78 Slice-1: the SAFE-tier self-repair loop's durable state (`failureTrace`/`diagnostics`/
652
683
  * `rejectedHypotheses`/`attemptCount`/`oracleTier` — all JSON/`structuredClone`-safe, no fn/Date). Set ONLY
@@ -963,14 +994,29 @@ export declare const F012_CHECKPOINT_VERSION = 7;
963
994
  * stays true: rows whose gate carries no realApproval bit keep their historic stamps everywhere.
964
995
  */
965
996
  export declare const REAL_APPROVAL_CHECKPOINT_VERSION = 8;
997
+ /**
998
+ * design/199 (2026-08-13 — the precedent replayed a seventh time): the schema version a suspend
999
+ * stamps when its state carries the READ-FACE section ({@link CheckpointState.readFace} — the
1000
+ * resolved open face and/or deny-set additions). The enforcement lives ENTIRELY in the resuming
1001
+ * worker (the stricter-wins face fold + the deny-entry union at prepare): a pre-199 worker
1002
+ * (MAX_SUPPORTED=8) would accept the row, ignore the section, and resume the leg under its own
1003
+ * defaults — silently DROPPING the frozen deny additions (a widening), and re-deriving the face
1004
+ * from live config alone. Stamping v9 forces it to reject PRE-CAS (`unsupported_version`, stays
1005
+ * `pending`, retried on an enforcing worker). A row whose task ran the default posture (face roots,
1006
+ * no additions) carries NO section and keeps the historic stamps — such a resume on ANY worker
1007
+ * folds to roots + built-ins, which is exactly the semantics it was cut under.
1008
+ */
1009
+ export declare const FACE_CHECKPOINT_VERSION = 9;
966
1010
  /** The highest {@link Checkpoint.version} `runner.resume` will act on; a higher one is rejected pre-CAS with
967
1011
  * {@link CheckpointError} `unsupported_version` (the checkpoint stays `pending`, retryable on a newer worker).
968
1012
  * Raised to 8 for realApproval-bearing gates — this worker reads v1 (legacy human), v2 (resource), v3
969
1013
  * (binding human/irreversible_ask), v4 (a pre-164 row, refused only when it carries the retired
970
1014
  * wall-clock allocation), v5 (token-allocation-bearing resource/approval), v6 (org-admission freeze
971
- * state), v7 (F-012 constraint-chain / delegation-provenance enforcement state) and v8 (a
972
- * non-budgetable `realApproval` gate bit with its org-origin resume semantics). */
973
- export declare const MAX_SUPPORTED_CHECKPOINT_VERSION = 8;
1015
+ * state), v7 (F-012 constraint-chain / delegation-provenance enforcement state), v8 (a
1016
+ * non-budgetable `realApproval` gate bit with its org-origin resume semantics) and v9 (the
1017
+ * design/199 read-face section: resolved face + deny-set additions, folded stricter-wins on
1018
+ * resume). */
1019
+ export declare const MAX_SUPPORTED_CHECKPOINT_VERSION = 9;
974
1020
  /**
975
1021
  * Read a checkpoint's schema version, defaulting an absent field to **legacy `0`** (a 1.67-era checkpoint
976
1022
  * written before the field existed — it carries no `workspaceHandle`, so resuming it the v1 way is safe).
@@ -1507,7 +1553,7 @@ export declare class CheckpointError extends Error {
1507
1553
  * · `invalid_outcome` pre-CAS row-integrity arms: `"real_approval_damaged"`,
1508
1554
  * `"real_approval_forged"`, `"constraint_chain_missing"` (all terminal for the row's
1509
1555
  * current bytes — no worker version redeems a damaged row). */
1510
- reason?: "version_newer" | "env_factory_missing" | "governed_unwired" | "real_approval_damaged" | "real_approval_forged" | "constraint_chain_missing";
1556
+ reason?: "version_newer" | "env_factory_missing" | "governed_unwired" | "real_approval_damaged" | "real_approval_forged" | "read_face_damaged" | "read_face_forged" | "constraint_chain_missing";
1511
1557
  } | undefined;
1512
1558
  constructor(code: "checkpoint.already_exists" | "checkpoint.already_resolved" | "checkpoint.not_found"
1513
1559
  /** `runner.resume` was handed an {@link ResumeOutcome} whose `gate` arm does not match the
@@ -1628,7 +1674,7 @@ export declare class CheckpointError extends Error {
1628
1674
  * · `invalid_outcome` pre-CAS row-integrity arms: `"real_approval_damaged"`,
1629
1675
  * `"real_approval_forged"`, `"constraint_chain_missing"` (all terminal for the row's
1630
1676
  * current bytes — no worker version redeems a damaged row). */
1631
- reason?: "version_newer" | "env_factory_missing" | "governed_unwired" | "real_approval_damaged" | "real_approval_forged" | "constraint_chain_missing";
1677
+ reason?: "version_newer" | "env_factory_missing" | "governed_unwired" | "real_approval_damaged" | "real_approval_forged" | "read_face_damaged" | "read_face_forged" | "constraint_chain_missing";
1632
1678
  } | undefined);
1633
1679
  }
1634
1680
  /**
@@ -136,7 +136,8 @@ export const TOKEN_CHECKPOINT_VERSION = 5;
136
136
  export const ORG_ADMISSION_CHECKPOINT_VERSION = 6;
137
137
  export const F012_CHECKPOINT_VERSION = 7;
138
138
  export const REAL_APPROVAL_CHECKPOINT_VERSION = 8;
139
- export const MAX_SUPPORTED_CHECKPOINT_VERSION = 8;
139
+ export const FACE_CHECKPOINT_VERSION = 9;
140
+ export const MAX_SUPPORTED_CHECKPOINT_VERSION = 9;
140
141
  export function checkpointVersionOf(cp) {
141
142
  return cp.version ?? 0;
142
143
  }
@@ -724,13 +724,20 @@ export interface ToolGateInput {
724
724
  * - `unavailable`/`parse_error` → FAIL-CLOSED: the ask continues down the ORIGINAL chain
725
725
  * (durable suspend / onAsk / headless auto-deny) exactly as if auto mode were absent
726
726
  * (CC `automode-unavailable`/`automode-parsing-error` — "NOT a policy decision").
727
- * SCOPE (declared semantic): this covers EVERY surviving ask, INCLUDING the deterministic egress/
727
+ * SCOPE (declared semantic): this covers every surviving ask INCLUDING the deterministic egress/
728
728
  * irreversibility safety tightens — auto mode is the deployment's explicit choice to let the
729
729
  * classifier be the "explicit ask-resolution" for this session (its rule set covers exactly those
730
730
  * action classes: outbound writes, irreversible operations, and sensitive-value handling). A
731
731
  * deployment that wants human-only
732
732
  * resolution for safety asks simply does not arm auto mode. The classifier can never AUTO-ALLOW by
733
733
  * failing — only an affirmative `<block>no</block>` allows.
734
+ * EXCLUDED from that scope (the ask flows the ORIGINAL chain untouched, exactly as if this field
735
+ * were absent for that one call): an org-constrained ask (design/182 §7.4, either org arm), the
736
+ * reserved question tool, a call MARKED unresolvable (design/153), a hook-raised or hook-promoted
737
+ * ask (`decisionReason:"hook"`), and an ask carrying `matchedAskRule` (an explicit `ask` permission
738
+ * rule matched — #213: a person's standing "ask me each time" is not classifier hesitation). The
739
+ * last two are the same pair the persisted-rule lane refuses, so the gate's two ask→allow seams
740
+ * answer provenance questions identically.
734
741
  */
735
742
  autoMode?: {
736
743
  decider: import("./auto-mode.js").AutoModeDecider;
@@ -769,6 +776,10 @@ export interface ToolGateInput {
769
776
  * tool's own egress/irreversibility marks are structural requirements, not classifier hesitation
770
777
  * ("allow rules silence the classifier's questions, never a mandated one"). When a rule matches
771
778
  * but cannot clear, the surviving ask discloses it (message + `persistedRuleShadowed`).
779
+ * · #213: an ask carrying `matchedAskRule` (an explicit `ask` permission rule matched this call)
780
+ * is never consumed — a person's standing "ask me each time" outranks the memory of one past
781
+ * yes, across lanes exactly as the DSL's own deny > ask > allow order says within one. Same
782
+ * disclosure as the mandated family when a rule matches anyway.
772
783
  * What it DOES consume is the classify-DOCTRINE shell ask (the coarse tier "maybe" — the
773
784
  * don't-ask-again main case this feature exists for) and unmarked bare asks. Consuming such an ask
774
785
  * also skips the park it would have minted; that IS what a standing approval means, and the
@@ -354,7 +354,7 @@ export async function runToolGate(input) {
354
354
  req.toolName !== ASK_USER_QUESTION_TOOL_NAME &&
355
355
  input.isMarkedUnresolvable?.(input.event.toolCallId) !== true) {
356
356
  const hit = await input.persistedRules.admits(req).catch(() => undefined);
357
- if (hit !== undefined && persistedRuleMandate === undefined) {
357
+ if (hit !== undefined && persistedRuleMandate === undefined && decision.matchedAskRule === undefined) {
358
358
  decision = {
359
359
  action: "allow",
360
360
  message: `a persisted allow rule (${hit}) covers this call`,
@@ -365,7 +365,11 @@ export async function runToolGate(input) {
365
365
  }
366
366
  else if (hit !== undefined) {
367
367
  const shownRule = inlineUntrusted(hit, 200);
368
- const mandateNoun = persistedRuleMandate === "operator_always" ? "this deployment mandates per-call confirmation for shell commands (shellGate: always)" : "this tool carries egress/irreversibility marks (a mandated confirmation a rule cannot clear)";
368
+ const mandateNoun = persistedRuleMandate === "operator_always"
369
+ ? "this deployment mandates per-call confirmation for shell commands (shellGate: always)"
370
+ : persistedRuleMandate !== undefined
371
+ ? "this tool carries egress/irreversibility marks (a mandated confirmation a rule cannot clear)"
372
+ : "an explicit ask rule matched this call (a person's ask-me-each-time outranks a standing allow rule)";
369
373
  decision = {
370
374
  ...decision,
371
375
  persistedRuleShadowed: shownRule,
@@ -376,6 +380,8 @@ export async function runToolGate(input) {
376
380
  if (input.autoMode &&
377
381
  !orgRealApprovalRequired &&
378
382
  decision.action === "ask" &&
383
+ decision.decisionReason !== "hook" &&
384
+ decision.matchedAskRule === undefined &&
379
385
  req.toolName !== ASK_USER_QUESTION_TOOL_NAME &&
380
386
  input.isMarkedUnresolvable?.(input.event.toolCallId) !== true) {
381
387
  const verdict = await input.autoMode.decider
@@ -287,11 +287,11 @@ export function createPermissionRulePolicy(rules, opts) {
287
287
  return { action: "deny", message: ruleMessage("denied", paramDeny.ruleText, paramDeny.source) };
288
288
  }
289
289
  if (entry.bare.ask) {
290
- return { action: "ask", message: ruleMessage("flagged", entry.bare.ask.ruleText, entry.bare.ask.source) };
290
+ return { action: "ask", message: ruleMessage("flagged", entry.bare.ask.ruleText, entry.bare.ask.source), matchedAskRule: entry.bare.ask.ruleText };
291
291
  }
292
292
  const paramAsk = matchParamRules(entry.param.ask, req.args, caps.maxScalarValueChars);
293
293
  if (paramAsk) {
294
- return { action: "ask", message: ruleMessage("flagged", paramAsk.ruleText, paramAsk.source) };
294
+ return { action: "ask", message: ruleMessage("flagged", paramAsk.ruleText, paramAsk.source), matchedAskRule: paramAsk.ruleText };
295
295
  }
296
296
  if (entry.bare.allow) {
297
297
  return { action: "allow" };
@@ -307,14 +307,14 @@ export interface Prepared {
307
307
  * threaded to every compaction-boundary epoch selection so boundary re-pins hash the same
308
308
  * artifact identity as the prepare-time pin. Empty on declaration-free runs. */
309
309
  epochDeclaredSections: import("../../prompt-assembly/epoch.js").EpochDeclaredSections;
310
+ /** design/148 S3 — the LIVE nine-element identity face (refreshed by deferred materialization and
311
+ * the RB-31 adoption swap); the manifest holds the prepare-time initial copy. */
312
+ turnSnapshot?: import("../../prompt-assembly/turn-snapshot.js").TurnPromptSnapshot;
310
313
  /** RB-31 (design/148 S2) — the compaction-boundary center-adoption seam (undefined = no source
311
314
  * wired or session not center-pinned). Returns the maybeCompact `centerAdoption` fragment or
312
315
  * undefined; `apply` runs post-CAS only (auto-compaction owns the ordering). Both
313
316
  * `epochDeclaredSections` and `promptOverheadTokens` on THIS object are mutated by a successful
314
317
  * adoption (read them at call time, never capture early). */
315
- /** design/148 S3 — the LIVE nine-element identity face (refreshed by deferred materialization and
316
- * the RB-31 adoption swap); the manifest holds the prepare-time initial copy. */
317
- turnSnapshot?: import("../../prompt-assembly/turn-snapshot.js").TurnPromptSnapshot;
318
318
  centerCompactionCandidate?: () => {
319
319
  centerArtifactDigest?: string;
320
320
  sourceRevision?: string;
@@ -573,8 +573,15 @@ export interface Prepared {
573
573
  /** Narrow workspace reader for compaction working-file attachments (LONGRUN-2): reads a task file
574
574
  * via the SAME env the hands ran against (so remote/k8s/E2B tasks read the container's tree, not
575
575
  * the control plane's). Present only when the hands are enabled — without an env there is no
576
- * workspace to re-read. null = unreadable (deleted/binary/transport error); callers skip it. */
577
- readTaskFile?: (path: string) => Promise<string | null>;
576
+ * workspace to re-read. null = unreadable (deleted/binary/transport error); callers skip it.
577
+ * design/199 件B: `{ withheld }` = the target matches the sensitive-path read deny list — the
578
+ * attachment is deliberately withheld and the consumer must SAY so (an annotation, never a silent
579
+ * skip: a silently missing attachment reads as "file gone", which is a different fact). */
580
+ readTaskFile?: (path: string) => Promise<string | null | {
581
+ withheld: {
582
+ pattern: string;
583
+ };
584
+ }>;
578
585
  /** CC post-compact restore parity (2026-07-03): the task's READ files, most recent
579
586
  * first (from the hands' readFileState `lastReadAt` stamps). The compaction working-file
580
587
  * attachment prefers this over the modified set — CC restores what the model RECENTLY READ,
@@ -1483,6 +1490,15 @@ export declare function rebaseWorkspacePath(p: string, fromRaw: string, toRaw: s
1483
1490
  * which case the earlier — more specific — spelling is the intended one. Exported for direct unit pinning.
1484
1491
  */
1485
1492
  export declare function rebaseWorkspacePathAcross(p: string, froms: readonly string[], to: string): string;
1493
+ /**
1494
+ * Build everything a task run needs (council design/34 ②: a free function with EXPLICIT deps, not a
1495
+ * Runner method — testable and decoupled). Resolves the model/role/thinking, acquires + reconciles the
1496
+ * session (with the conflict-detecting Proxy + bounded re-wake on a lost CAS), assembles the tool set
1497
+ * (caller tools + offload wrap + report_blocked + submit_output + MCP + memory + read_tool_result),
1498
+ * builds the system prompt, constructs the harness, wires the tool-policy gate + context-edit/guard
1499
+ * hooks + external-signal cancellation, and sets up the cache-break detector. **No run-loop control
1500
+ * flow** lives here — that stays in the Runner loop.
1501
+ */
1486
1502
  export declare function prepareTask(spec: TaskSpec, deps: RunnerDeps, sessions: SessionStore, resume?: PrepareResume, internals?: RunInternals,
1487
1503
  /** design/98 §3.1 (S8c): a TRUSTED self-reference to the Runner, passed by the Runner itself (never a
1488
1504
  * TaskSpec field) so the `run_workflow` tool can execute child tasks via `runner.runTask`. Undefined when