@sema-agent/core 5.28.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 (122) hide show
  1. package/CHANGELOG.md +99 -3
  2. package/dist/agents/cascade.d.ts +1 -1
  3. package/dist/agents/observer.d.ts +3 -3
  4. package/dist/agents/peer-admission.d.ts +1 -1
  5. package/dist/agents/send-message-tool.js +2 -0
  6. package/dist/agents/subagent.d.ts +2 -0
  7. package/dist/agents/subagent.js +6 -0
  8. package/dist/agents/teacher.d.ts +1 -1
  9. package/dist/agents/teacher.js +2 -0
  10. package/dist/agents/verify.d.ts +6 -6
  11. package/dist/agents/verify.js +2 -0
  12. package/dist/bench/metrics.d.ts +1 -1
  13. package/dist/brain/retry.d.ts +1 -1
  14. package/dist/core/arg-summary.d.ts +1 -1
  15. package/dist/core/ask-class.d.ts +2 -2
  16. package/dist/core/ask-question.d.ts +1 -1
  17. package/dist/core/ask-question.js +14 -1
  18. package/dist/core/auto-compaction.d.ts +5 -1
  19. package/dist/core/auto-compaction.js +10 -1
  20. package/dist/core/background-shell.d.ts +5 -5
  21. package/dist/core/checkpoint-store.d.ts +65 -19
  22. package/dist/core/checkpoint-store.js +2 -1
  23. package/dist/core/file-snapshot-store.d.ts +1 -1
  24. package/dist/core/hooks.d.ts +15 -4
  25. package/dist/core/hooks.js +8 -2
  26. package/dist/core/human-input-projection.d.ts +2 -2
  27. package/dist/core/memory-admission.d.ts +2 -2
  28. package/dist/core/memory-engine/engine.d.ts +1 -1
  29. package/dist/core/memory-engine/file-backend.d.ts +2 -2
  30. package/dist/core/memory-engine/header-hints.d.ts +1 -1
  31. package/dist/core/memory-engine/layout.d.ts +2 -2
  32. package/dist/core/memory-recall.d.ts +1 -1
  33. package/dist/core/memory.d.ts +2 -2
  34. package/dist/core/oracle-isolation.d.ts +2 -2
  35. package/dist/core/permission-rule-consent.d.ts +1 -1
  36. package/dist/core/permission-rule-model.d.ts +1 -1
  37. package/dist/core/permission-rule-org.d.ts +1 -1
  38. package/dist/core/permission-rule-store.d.ts +1 -1
  39. package/dist/core/permission-rules.js +2 -2
  40. package/dist/core/runner/active-skill-scope.d.ts +1 -1
  41. package/dist/core/runner/memory-consolidation.d.ts +1 -1
  42. package/dist/core/runner/prepare-task.d.ts +29 -8
  43. package/dist/core/runner/prepare-task.js +118 -22
  44. package/dist/core/runner/runtask.d.ts +1 -1
  45. package/dist/core/runner/runtask.js +34 -8
  46. package/dist/core/runner/session-rule-policy.d.ts +4 -3
  47. package/dist/core/runner/teardown-bounded.d.ts +1 -1
  48. package/dist/core/runner/tool-disclosure.d.ts +1 -1
  49. package/dist/core/runner/tool-output-projection.js +1 -1
  50. package/dist/core/scheduler.d.ts +4 -4
  51. package/dist/core/sensitive-path-policy.js +5 -16
  52. package/dist/core/session-policy-store.d.ts +3 -3
  53. package/dist/core/shared-memory/normalize.d.ts +1 -1
  54. package/dist/core/skill-tool-specifier.d.ts +1 -1
  55. package/dist/core/store-contracts/tool-result-store-contract.js +23 -0
  56. package/dist/core/task-notification.d.ts +2 -2
  57. package/dist/core/task-registry-agent.d.ts +9 -9
  58. package/dist/core/task-registry-shared.d.ts +4 -4
  59. package/dist/core/tighten-task-spec.js +18 -0
  60. package/dist/core/tool-errors.d.ts +2 -2
  61. package/dist/core/tool-policy.d.ts +64 -12
  62. package/dist/core/tool-policy.js +68 -8
  63. package/dist/core/tool-result-store.d.ts +2 -2
  64. package/dist/core/tool-result-store.js +3 -1
  65. package/dist/core/trace.d.ts +1 -1
  66. package/dist/core/types.d.ts +68 -13
  67. package/dist/core/untrusted-text.d.ts +6 -0
  68. package/dist/core/untrusted-text.js +1 -0
  69. package/dist/engine/compaction/compaction.d.ts +1 -1
  70. package/dist/engine/harness/messages.d.ts +1 -1
  71. package/dist/engine/harness/types.d.ts +12 -2
  72. package/dist/engine/llm/types.d.ts +1 -1
  73. package/dist/engine/loop/types.d.ts +2 -2
  74. package/dist/engine/session/import-validate.d.ts +1 -1
  75. package/dist/engine/session/log-digest.d.ts +1 -1
  76. package/dist/fixtures/index.d.ts +18 -2
  77. package/dist/fixtures/index.js +11 -0
  78. package/dist/index.d.ts +3 -1
  79. package/dist/index.js +3 -1
  80. package/dist/orchestration/goal.d.ts +1 -1
  81. package/dist/orchestration/run-workflow-tool.d.ts +21 -0
  82. package/dist/orchestration/run-workflow-tool.js +6 -3
  83. package/dist/orchestration/workflow-primitives.d.ts +10 -1
  84. package/dist/orchestration/workflow-primitives.js +12 -1
  85. package/dist/orchestration/workflow-types.d.ts +5 -4
  86. package/dist/orchestration/workflow.js +11 -3
  87. package/dist/prompt-assembly/artifact.d.ts +1 -1
  88. package/dist/prompt-assembly/epoch.js +2 -0
  89. package/dist/prompt-assembly/packs/sema-default.js +2 -2
  90. package/dist/prompt-assembly/types.d.ts +4 -0
  91. package/dist/prompts/default.d.ts +14 -9
  92. package/dist/prompts/default.js +14 -4
  93. package/dist/scenarios/scenario-registry.d.ts +3 -3
  94. package/dist/stores/file/tool-result-store.d.ts +2 -2
  95. package/dist/tools/fs/bash-readonly-classifier.d.ts +23 -2
  96. package/dist/tools/fs/bash-readonly-classifier.js +11 -0
  97. package/dist/tools/fs/fs-bash.d.ts +8 -1
  98. package/dist/tools/fs/fs-bash.js +8 -3
  99. package/dist/tools/fs/fs-pdf.d.ts +1 -1
  100. package/dist/tools/fs/fs-pdf.js +2 -2
  101. package/dist/tools/fs/fs-read.d.ts +1 -1
  102. package/dist/tools/fs/fs-read.js +11 -7
  103. package/dist/tools/fs/fs-search-tools.d.ts +5 -3
  104. package/dist/tools/fs/fs-search-tools.js +15 -8
  105. package/dist/tools/fs/fs-shared.d.ts +6 -2
  106. package/dist/tools/fs/fs-shared.js +8 -3
  107. package/dist/tools/fs/index.d.ts +18 -0
  108. package/dist/tools/fs/index.js +13 -2
  109. package/dist/tools/fs/read-deny.d.ts +105 -0
  110. package/dist/tools/fs/read-deny.js +151 -0
  111. package/dist/tools/fs/read-face.d.ts +43 -0
  112. package/dist/tools/fs/read-face.js +38 -0
  113. package/dist/tools/fs/repo-map.d.ts +3 -1
  114. package/dist/tools/fs/repo-map.js +11 -5
  115. package/dist/tools/fs/safety.d.ts +34 -12
  116. package/dist/tools/fs/safety.js +88 -7
  117. package/dist/tools/fs/search.d.ts +54 -5
  118. package/dist/tools/fs/search.js +103 -21
  119. package/dist/tools/scheduler-tools.d.ts +1 -1
  120. package/dist/tools/task-list.d.ts +1 -1
  121. package/dist/tools/web.d.ts +3 -3
  122. package/package.json +1 -1
@@ -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,
@@ -864,7 +871,7 @@ export interface InheritedGate {
864
871
  * Org-memory admission freeze (ruled 2026-08-05): the spawning chain's FROZEN admitted org-scope
865
872
  * set — every org memory scope the parent actually mounted (deployment-origin + admitted request).
866
873
  * A delegated child's REQUEST-origin org scopes must be a subset (intersection ≠ request ⇒ the
867
- * child's prepare refuses, `memory.admission_denied`); deployment-origin scopes bypass the freeze
874
+ * child's prepare refuses, `memory.admission_denied`); deployment-origin scopes circumvent the freeze
868
875
  * (operator authority is deployment-wide). ALWAYS emitted by chain assembly (empty array = parent
869
876
  * mounted no org plane), so an ABSENT field discriminates an older-shape chain — which the child
870
877
  * side reads fail-closed as the empty set. Serializable (plain strings): persists on the
@@ -872,7 +879,7 @@ export interface InheritedGate {
872
879
  */
873
880
  admittedOrgScopes?: readonly string[];
874
881
  /**
875
- * The MONOTONIC org-governance provenance bit (adversarial round 5): `true` when any leg of this
882
+ * The MONOTONIC org-governance provenance bit (falsification-style round 5): `true` when any leg of this
876
883
  * tree ran with an org-admission surface configured (resolver / deployment scope list) or
877
884
  * inherited the bit. It never clears once set (a governed tree stays governed), and it counts as
878
885
  * governance evidence at the admission door EVEN when every admitted set en route is empty — a
@@ -1055,7 +1062,7 @@ export interface RunInternals {
1055
1062
  * arbitration is decided) and BEFORE the resumed leg starts. The parked-resume caller uses it to
1056
1063
  * flip the reserved row `parked→running` (guarded CAS) + adopt the live handle/lane — the ONLY
1057
1064
  * legal site for that flip (a pre-consume flip would let TaskStop hit the plain running arm and
1058
- * bypass the checkpoint arbitration entirely). A THROW here aborts the resume (propagates out of
1065
+ * circumvent the checkpoint arbitration entirely). A THROW here aborts the resume (propagates out of
1059
1066
  * `resumeStream`): the checkpoint is already consumed, so the caller's compensation
1060
1067
  * (`rollbackParkedClaim`) reads `resolved` and settles the honest failed/outcome-unknown terminal
1061
1068
  * — never a silent run without an adopted row. Deployment/trusted-caller channel only, mirroring
@@ -1442,6 +1449,11 @@ export interface ResolvedWorkspace {
1442
1449
  cwd: string;
1443
1450
  /** True only for an accepted `isolation: "worktree"` request (⇒ `cwd` is this agent's own worktree). */
1444
1451
  isolated: boolean;
1452
+ /** True when the task's execution env is REMOTE (#197 BGW-7): `cwd` then names a path INSIDE the remote
1453
+ * target (a per-task sandbox on the shape-(d) exemption leg, e.g. `/workspace`), which does not exist —
1454
+ * or names an unrelated directory — on the host. `isolated && !remote` is the only combination under
1455
+ * which `cwd` is a host path a consumer may diff / merge / remove. */
1456
+ remote: boolean;
1445
1457
  }
1446
1458
  /**
1447
1459
  * From the resumed/active transcript, the batch position of `currentId` (design/45 §4.ter): the tool-call
@@ -1478,6 +1490,15 @@ export declare function rebaseWorkspacePath(p: string, fromRaw: string, toRaw: s
1478
1490
  * which case the earlier — more specific — spelling is the intended one. Exported for direct unit pinning.
1479
1491
  */
1480
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
+ */
1481
1502
  export declare function prepareTask(spec: TaskSpec, deps: RunnerDeps, sessions: SessionStore, resume?: PrepareResume, internals?: RunInternals,
1482
1503
  /** design/98 §3.1 (S8c): a TRUSTED self-reference to the Runner, passed by the Runner itself (never a
1483
1504
  * TaskSpec field) so the `run_workflow` tool can execute child tasks via `runner.runTask`. Undefined when
@@ -57,7 +57,7 @@ import { foldAdmissionFreeze } from "../memory-admission.js";
57
57
  import { prepareMemory } from "./prepare-memory.js";
58
58
  import { defaultPromptProvider, buildEnvironmentContext, buildGitSnapshot, formatLocalDate, isValidTimeZone, PROJECT_CONTEXT_FRAMING } from "../../prompts/default.js";
59
59
  import { assemblePrompt } from "../../prompt-assembly/assemble.js";
60
- import { auditToolCollisions, projectToolManifest } from "../../prompt-assembly/tool-catalog.js";
60
+ import { auditToolCollisions, getToolContract, projectToolManifest } from "../../prompt-assembly/tool-catalog.js";
61
61
  import { resolveEpochAgainstBundled } from "../../prompt-assembly/epoch.js";
62
62
  import { artifactDeclarations } from "../../prompt-assembly/artifact.js";
63
63
  import { buildTurnPromptSnapshot } from "../../prompt-assembly/turn-snapshot.js";
@@ -74,7 +74,7 @@ import { hasBackgroundShell, sweepBackgroundShells } from "../background-shell.j
74
74
  import { createTaskOutputTool, createTaskStopTool, defaultTaskRegistry } from "../task-registry.js";
75
75
  import { createMonitorTool } from "../../tools/monitor.js";
76
76
  import { createWorktreeTools } from "../../tools/worktree.js";
77
- import { applyCompactionToReadFileState, bashReversibilityProbe, createHandsToolkit, isReadDedupStubResult, seedReadFileStateFromContext, seedReadFileStateFromTranscript, HAND_TOOL_EFFECTS, pdfModelCapabilitiesOf } from "../../tools/fs/index.js";
77
+ import { applyCompactionToReadFileState, bashReversibilityProbe, compileReadDeny, createHandsToolkit, isReadDedupStubResult, resolveReadFace, seedReadFileStateFromContext, seedReadFileStateFromTranscript, FULL_SHELL_CONTRACT_ID, HAND_TOOL_EFFECTS, pdfModelCapabilitiesOf } from "../../tools/fs/index.js";
78
78
  import { decodeTextBytes } from "../../tools/fs/encoding.js";
79
79
  import { ASK_USER_QUESTION_TOOL_NAME, createAskUserQuestionTool, classifyQuestionOutcome, isLiveQuestionFace, validateAskQuestions, } from "../ask-question.js";
80
80
  import { createSchedulerTools } from "../../tools/scheduler-tools.js";
@@ -85,7 +85,7 @@ import { resolveWorkflowSizeGuideline } from "../../orchestration/workflow-size-
85
85
  import { createLspTool, gitCheckIgnoreFilter, resolveLspPath } from "../lsp.js";
86
86
  import { resolveKey } from "../../tools/fs/safety.js";
87
87
  import { wholeFileRecordsFromTranscript } from "./session-file-state-replay.js";
88
- import { BINDING_CHECKPOINT_VERSION, mintCheckpointToken, ORG_ADMISSION_CHECKPOINT_VERSION, F012_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, buildRiskDescriptor, debitLedger, encodeAtFidelity, remainingBudgetMicroUsd, resolveCheckpointStore, resolveDeclaredFidelity, samePlainValue, } from "../checkpoint-store.js";
88
+ import { BINDING_CHECKPOINT_VERSION, mintCheckpointToken, ORG_ADMISSION_CHECKPOINT_VERSION, F012_CHECKPOINT_VERSION, FACE_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, buildRiskDescriptor, debitLedger, encodeAtFidelity, remainingBudgetMicroUsd, resolveCheckpointStore, resolveDeclaredFidelity, samePlainValue, } from "../checkpoint-store.js";
89
89
  import { boundInputHashOf } from "../canonical-json.js";
90
90
  import { countElicitOptIns, deriveAskEffective, deriveWiringManifest, resolveAskSeamForm, resolveDeclaredDurability, resolveElicitSeam, resolveQuestionSeam } from "../wiring-manifest.js";
91
91
  import { GLOBAL_USAGE_KEY, resolveUsageWindows, usageRetryAfterMs } from "../usage-window-store.js";
@@ -219,6 +219,9 @@ export function resolveModelPromptTraits(model, spec, internals) {
219
219
  fableMitigations: isFableFamilyModelId(model.id),
220
220
  };
221
221
  }
222
+ function hasConversationContent(branch) {
223
+ return branch.some((e) => e.type === "message" || e.type === "custom_message" || e.type === "compaction");
224
+ }
222
225
  function isDelegatedNonForkChild(internals) {
223
226
  return internals?.isDelegatedChild === true && internals?.insideFork !== true;
224
227
  }
@@ -910,7 +913,13 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
910
913
  }
911
914
  if (internals?.onWorkspaceResolved !== undefined) {
912
915
  try {
913
- internals.onWorkspaceResolved({ cwd: taskRootPath, isolated: internals.isolation === "worktree" });
916
+ const workspaceEnv = ownedEnv ?? deps.executionEnv;
917
+ internals.onWorkspaceResolved({
918
+ cwd: taskRootPath,
919
+ isolated: internals.isolation === "worktree",
920
+ remote: workspaceEnv !== undefined &&
921
+ (isRemoteExecutionEnv(workspaceEnv) || workspaceEnv.hostLocalPaths === false),
922
+ });
914
923
  }
915
924
  catch {
916
925
  }
@@ -1073,6 +1082,17 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1073
1082
  const memoryAdmittedOrgScopesRef = { current: [] };
1074
1083
  const ownOrgVerdictRef = { current: undefined };
1075
1084
  const orgAdmissionCheckpointState = () => inheritedAdmittedOrgScopes !== undefined || ownOrgVerdictRef.current !== undefined || orgGovernedProvenance;
1085
+ const faceCheckpointSection = () => {
1086
+ if (resolvedReadFace === undefined) {
1087
+ const seed = resume?.seed.readFace;
1088
+ return seed !== undefined ? { face: seed.face, ...(seed.denyEntries !== undefined ? { denyEntries: seed.denyEntries.map((e) => ({ ...e })) } : {}) } : undefined;
1089
+ }
1090
+ if (resolvedReadFace === "open" || readDenyAdditionsNormalized.length > 0) {
1091
+ return { face: resolvedReadFace, ...(readDenyAdditionsNormalized.length > 0 ? { denyEntries: readDenyAdditionsNormalized.map((e) => ({ ...e })) } : {}) };
1092
+ }
1093
+ return undefined;
1094
+ };
1095
+ const faceCheckpointState = () => faceCheckpointSection() !== undefined;
1076
1096
  const f012CheckpointState = () => (inheritedParentConstraints?.length ?? 0) > 0 ||
1077
1097
  seedInheritedGate?.constraintChain !== undefined ||
1078
1098
  internals?.delegationProvenance !== undefined;
@@ -1143,6 +1163,12 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1143
1163
  };
1144
1164
  let agentForkDenial;
1145
1165
  let observersActive = false;
1166
+ let resolvedReadFace;
1167
+ let readDenyAdditionsNormalized = [];
1168
+ const fullShellReachable = handsEnabled &&
1169
+ !(executionEnv instanceof StubExecutionEnv) &&
1170
+ spec.handsReadOnly !== true &&
1171
+ !(toolFaceSnapshot.exclude?.includes("Bash") ?? false);
1146
1172
  let autoModeDecider;
1147
1173
  const enrichSpecToolCtx = (ctx) => ({
1148
1174
  ...ctx,
@@ -1153,6 +1179,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1153
1179
  ...(frozenOnAsk !== undefined ? { onAsk: frozenOnAsk } : {}),
1154
1180
  ...(frozenOnQuestion !== undefined ? { onQuestion: frozenOnQuestion } : {}),
1155
1181
  ...(spec.handsReadOnly === true ? { handsReadOnly: true } : {}),
1182
+ ...(resolvedReadFace === "roots" ? { readFace: "roots" } : {}),
1183
+ ...(readDenyAdditionsNormalized.length > 0 ? { readDenyPatterns: Object.freeze(readDenyAdditionsNormalized.map((e) => ({ ...e }))) } : {}),
1156
1184
  ...(spec.interactiveTools === false ? { interactiveTools: false } : {}),
1157
1185
  oneShot: spec.oneShot,
1158
1186
  clientContext: spec.clientContext,
@@ -1358,6 +1386,11 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1358
1386
  workflowDepth: internals?.workflowDepth,
1359
1387
  parentCwd: taskRootPath,
1360
1388
  parentThinking: () => harnessRef.current?.getThinkingLevel() ?? thinking,
1389
+ parentReadFace: () => resolvedReadFace,
1390
+ parentReadDenyPatterns: () => (readDenyAdditionsNormalized.length > 0 ? readDenyAdditionsNormalized : undefined),
1391
+ parentCheckpointStoreDisabled: spec.checkpointStore === null,
1392
+ parentCenterArtifactDigest: () => centerAdoption?.artifact.artifactDigest,
1393
+ parentCenterSourceRevision: () => centerAdoption?.sourceRevision,
1361
1394
  ...(forwardEvent ? { forwardEvent } : {}),
1362
1395
  inheritedGateForChildren,
1363
1396
  }));
@@ -1570,6 +1603,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1570
1603
  const additionalRootsCanonical = [];
1571
1604
  const additionalReadRootsCanonical = [];
1572
1605
  let attachmentRootCanonical;
1606
+ let readDenyMatcher;
1573
1607
  if (handsEnabled) {
1574
1608
  const rootRaw = taskRootPath;
1575
1609
  const canon = await executionEnv.canonicalPath(rootRaw);
@@ -1621,14 +1655,14 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1621
1655
  if (resume === undefined && spec.sessionId !== undefined) {
1622
1656
  const prior = await session.buildContext().catch(() => undefined);
1623
1657
  for (const rec of wholeFileRecordsFromTranscript(prior?.messages ?? [])) {
1624
- const rk = await resolveKey(executionEnv, rootCanonical, rec.path, undefined, undefined, additionalRootsCanonical);
1658
+ const rk = await resolveKey(executionEnv, rootCanonical, rec.path, undefined, undefined, [...additionalRootsCanonical, ...additionalReadRootsCanonical]);
1625
1659
  if (rk.ok)
1626
1660
  seedReadFileStateFromTranscript(readFileState, rk.key, rec.content, rec.at);
1627
1661
  }
1628
1662
  }
1629
1663
  seedContextFiles = async (files) => {
1630
1664
  for (const f of files) {
1631
- const rk = await resolveKey(executionEnv, rootCanonical, f.path, undefined, undefined, additionalRootsCanonical);
1665
+ const rk = await resolveKey(executionEnv, rootCanonical, f.path, undefined, undefined, [...additionalRootsCanonical, ...additionalReadRootsCanonical]);
1632
1666
  if (rk.ok)
1633
1667
  seedReadFileStateFromContext(readFileState, rk.key, f.content);
1634
1668
  }
@@ -1650,9 +1684,28 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1650
1684
  ...(resume !== undefined ? { baselineUnknown: true } : {}),
1651
1685
  };
1652
1686
  }
1687
+ const seedReadFaceSection = resume?.seed.readFace;
1688
+ const readDenyAdditions = [...(deps.readDenyPatterns ?? []), ...(spec.readDenyPatterns ?? []), ...(seedReadFaceSection?.denyEntries ?? [])];
1689
+ readDenyMatcher = compileReadDeny(readDenyAdditions, "readDenyPatterns");
1690
+ {
1691
+ const builtinCount = compileReadDeny([], "built-in").entries.length;
1692
+ readDenyAdditionsNormalized = readDenyMatcher.entries.slice(builtinCount);
1693
+ }
1694
+ let liveReadFace = resolveReadFace({
1695
+ specReadFace: spec.readFace,
1696
+ depsReadFace: deps.readFace,
1697
+ readOnlyMount: handsReadOnly,
1698
+ orgGoverned: deps.permissionRuleOrg !== undefined,
1699
+ fullShellReachable,
1700
+ });
1701
+ if (resume !== undefined && (seedReadFaceSection === undefined || seedReadFaceSection.face === "roots"))
1702
+ liveReadFace = "roots";
1703
+ resolvedReadFace = liveReadFace;
1653
1704
  const band = createHandsToolkit(executionEnv, readFileState, rootCanonical, {
1654
1705
  ...(additionalRootsCanonical.length > 0 ? { additionalRoots: additionalRootsCanonical } : {}),
1655
1706
  ...(additionalReadRootsCanonical.length > 0 ? { additionalReadRoots: additionalReadRootsCanonical } : {}),
1707
+ ...(readDenyAdditions.length > 0 ? { readDenyPatterns: readDenyAdditions } : {}),
1708
+ readFace: liveReadFace,
1656
1709
  includeShell: handsIncludeShell,
1657
1710
  readOnly: handsReadOnly,
1658
1711
  ...(handsCwdRef ? { cwdRef: handsCwdRef } : {}),
@@ -1706,6 +1759,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1706
1759
  const shellReadBoundary = () => ({
1707
1760
  roots: [rootCanonical, ...additionalRootsCanonical, ...additionalReadRootsCanonical],
1708
1761
  ...(handsCwdRef?.current !== undefined ? { cwd: handsCwdRef.current } : {}),
1762
+ denyMatch: (p) => readDenyMatcher?.matchPath(p)?.pattern ?? null,
1763
+ ...(resolvedReadFace !== undefined ? { face: resolvedReadFace } : {}),
1709
1764
  });
1710
1765
  if (shellGate === "classify" && shellGatedBash)
1711
1766
  reversibilityProbes.set("Bash", bashReversibilityProbe(undefined, shellReadBoundary));
@@ -2017,7 +2072,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2017
2072
  if (spec.sessionId) {
2018
2073
  try {
2019
2074
  const branch = await session.getBranch();
2020
- const hasConversation = branch.some((e) => e.type === "message" || e.type === "custom_message" || e.type === "compaction");
2075
+ const hasConversation = hasConversationContent(branch);
2021
2076
  if (!hasConversation)
2022
2077
  projectMemoryPhase = "fresh";
2023
2078
  else {
@@ -2156,6 +2211,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2156
2211
  policyEnabled: promptPolicyEnabled,
2157
2212
  hooksEnabled: promptHooksEnabled,
2158
2213
  isolationEnabled: isIsolated(executionEnv),
2214
+ readFaceOpen: resolvedReadFace === "open",
2159
2215
  orchestrationEnabled: selfOrchestrationActive,
2160
2216
  orchestrationDeferred: selfOrchestrationActive && (toolFaceSnapshot.defer?.includes("Workflow") ?? false),
2161
2217
  promptProfile,
@@ -2208,6 +2264,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2208
2264
  if (additionalRootsCanonical.length > 0) {
2209
2265
  envFacts.additionalDirectories = [...additionalRootsCanonical];
2210
2266
  }
2267
+ if (resolvedReadFace !== undefined)
2268
+ envFacts.readFace = resolvedReadFace;
2211
2269
  if (additionalReadRootsCanonical.length > 0) {
2212
2270
  envFacts.additionalReadDirectories = [...additionalReadRootsCanonical];
2213
2271
  }
@@ -2301,6 +2359,16 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2301
2359
  if (excluded.has(tools[i].name))
2302
2360
  tools.splice(i, 1);
2303
2361
  }
2362
+ if (handsEnabled) {
2363
+ const fullShellOnRoster = tools.some((t) => t.name === "Bash" && getToolContract(t, "core").contractId === FULL_SHELL_CONTRACT_ID);
2364
+ if (fullShellOnRoster !== fullShellReachable) {
2365
+ const e = new Error(`internal invariant: fullShellReachable=${String(fullShellReachable)} but the post-exclusion roster ` +
2366
+ `${fullShellOnRoster ? "carries" : "does not carry"} the full shell (${FULL_SHELL_CONTRACT_ID}). ` +
2367
+ `A new roster-affecting mechanism was added without updating the predicate.`);
2368
+ e.code = "internal.full_shell_reachable_mismatch";
2369
+ throw e;
2370
+ }
2371
+ }
2304
2372
  if (promptProfile === "classic") {
2305
2373
  for (let i = 0; i < tools.length; i++) {
2306
2374
  const t = tools[i];
@@ -2498,7 +2566,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2498
2566
  }
2499
2567
  else if (resume === undefined && internals?.parentTaskId === undefined && internals?.parentSessionId === undefined) {
2500
2568
  const candidate = deps.promptSource?.current();
2501
- if (candidate !== undefined && (await session.getBranch()).length === 0) {
2569
+ if (candidate !== undefined && !hasConversationContent(await session.getBranch())) {
2502
2570
  centerAdoption = { artifact: candidate.artifact, sourceRevision: candidate.sourceRevision };
2503
2571
  centerAdoptionFresh = true;
2504
2572
  }
@@ -2577,7 +2645,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2577
2645
  const resolved = resolveEpochAgainstBundled(pinned, SEMA_DEFAULT_PACK, epochDeclaredSections);
2578
2646
  epochArtifactDigestForSnapshot = resolved.descriptor.artifactDigest;
2579
2647
  if (resolved.migrated) {
2580
- const activatedBy = pinned === undefined && !spec.sessionId ? "session_start" : "legacy_migration";
2648
+ const priorConversation = pinned === undefined ? hasConversationContent(await session.getBranch()) : true;
2649
+ const activatedBy = pinned === undefined && !priorConversation ? "session_start" : "legacy_migration";
2581
2650
  await session.appendPromptEpoch({
2582
2651
  ...resolved.descriptor,
2583
2652
  activatedBy,
@@ -3185,6 +3254,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3185
3254
  ...((internals?.explicitAgentName ?? internals?.agentName) !== undefined
3186
3255
  ? { sourceAgentName: internals?.explicitAgentName ?? internals?.agentName }
3187
3256
  : {}),
3257
+ ...(internals?.isDelegatedChild === true ? { isDelegatedChild: true } : {}),
3188
3258
  });
3189
3259
  const riskAxesOf = (toolName) => {
3190
3260
  const tier = irreversibilityTier.get(toolName);
@@ -3290,6 +3360,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3290
3360
  const suggestions = suggestRulesForCommand(command);
3291
3361
  return suggestions.length > 0 ? { ruleSuggestions: suggestions } : {};
3292
3362
  };
3363
+ const frozenClassifierExcluded = (d) => d.decisionReason === "hook" || d.matchedAskRule !== undefined;
3293
3364
  const recheckApprovedEdit = async (pol, onAskOf, creq, edit, csignal, ancestorDecider) => {
3294
3365
  let editArgs = edit;
3295
3366
  for (let round = 0;; round++) {
@@ -3316,7 +3387,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3316
3387
  }
3317
3388
  if (re.updatedInput !== undefined)
3318
3389
  editArgs = re.updatedInput;
3319
- if (ancestorDecider !== undefined) {
3390
+ if (ancestorDecider !== undefined && !frozenClassifierExcluded(re)) {
3320
3391
  const verdict = await ancestorDecider
3321
3392
  .decide({ req: { ...creq, args: editArgs }, ...(re.message !== undefined ? { askMessage: re.message } : {}) }, csignal ?? abortController.signal)
3322
3393
  .catch(() => ({ kind: "unavailable", cause: "error" }));
@@ -3395,7 +3466,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3395
3466
  `ask cannot be reconstructed in a delegated child; denied fail-closed (tighten-only)`,
3396
3467
  };
3397
3468
  }
3398
- if (pc.autoMode !== undefined) {
3469
+ if (pc.autoMode !== undefined && !frozenClassifierExcluded(first)) {
3399
3470
  const verdict = await pc.autoMode.decider
3400
3471
  .decide({ req: creq, ...(first.message !== undefined ? { askMessage: first.message } : {}) }, csignal ?? abortController.signal)
3401
3472
  .catch(() => ({ kind: "unavailable", cause: "error" }));
@@ -3482,7 +3553,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3482
3553
  };
3483
3554
  }
3484
3555
  const presentedArgs = decision.updatedInput !== undefined ? decision.updatedInput : creq.args;
3485
- if (pc.autoMode !== undefined) {
3556
+ if (pc.autoMode !== undefined && !frozenClassifierExcluded(decision)) {
3486
3557
  const verdict = await pc.autoMode.decider
3487
3558
  .decide({ req: { ...creq, args: presentedArgs }, ...(decision.message !== undefined ? { askMessage: decision.message } : {}) }, csignal ?? abortController.signal)
3488
3559
  .catch(() => ({ kind: "unavailable", cause: "error" }));
@@ -3892,6 +3963,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3892
3963
  nestedStats: { ...nestedStats },
3893
3964
  consolidationNotes: undefined,
3894
3965
  readFileState: readFileStateForCheckpoint ? [...readFileStateForCheckpoint.entries()] : undefined,
3966
+ readFace: faceCheckpointSection(),
3895
3967
  repairBundle: repairBundleForCheckpoint(parkedSpendMicroUsd),
3896
3968
  workspaceHandle,
3897
3969
  handsCwd: handsCwdRef?.current,
@@ -4111,7 +4183,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4111
4183
  const mintedAt = Date.now();
4112
4184
  const cp = {
4113
4185
  token,
4114
- version: f012CheckpointState() ? F012_CHECKPOINT_VERSION : orgAdmissionCheckpointState() ? ORG_ADMISSION_CHECKPOINT_VERSION : resourceLedgerOut.totalTokens !== undefined ? TOKEN_CHECKPOINT_VERSION : RESOURCE_CHECKPOINT_VERSION,
4186
+ version: faceCheckpointState() ? FACE_CHECKPOINT_VERSION : f012CheckpointState() ? F012_CHECKPOINT_VERSION : orgAdmissionCheckpointState() ? ORG_ADMISSION_CHECKPOINT_VERSION : resourceLedgerOut.totalTokens !== undefined ? TOKEN_CHECKPOINT_VERSION : RESOURCE_CHECKPOINT_VERSION,
4115
4187
  scope,
4116
4188
  sessionId,
4117
4189
  leafId,
@@ -4193,7 +4265,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4193
4265
  const mintedAt = Date.now();
4194
4266
  const cp = {
4195
4267
  token,
4196
- version: f012CheckpointState() ? F012_CHECKPOINT_VERSION : orgAdmissionCheckpointState() ? ORG_ADMISSION_CHECKPOINT_VERSION : reviewLedger.totalTokens !== undefined ? TOKEN_CHECKPOINT_VERSION : BINDING_CHECKPOINT_VERSION,
4268
+ version: faceCheckpointState() ? FACE_CHECKPOINT_VERSION : f012CheckpointState() ? F012_CHECKPOINT_VERSION : orgAdmissionCheckpointState() ? ORG_ADMISSION_CHECKPOINT_VERSION : reviewLedger.totalTokens !== undefined ? TOKEN_CHECKPOINT_VERSION : BINDING_CHECKPOINT_VERSION,
4197
4269
  scope,
4198
4270
  sessionId,
4199
4271
  leafId,
@@ -4445,11 +4517,14 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4445
4517
  const scope = durableApproval?.scope || checkpointScopeOf({ principal: spec.principal });
4446
4518
  const ttlMs = sanitizedTtlMs(durableApproval?.ttlMs);
4447
4519
  const checkpointState = serializeCheckpointState(remoteHandle, inFlightSpendMicroUsd());
4520
+ if (realApproval !== undefined && checkpointState.readFace !== undefined) {
4521
+ checkpointState.readFace = { ...checkpointState.readFace, realApproval: true };
4522
+ }
4448
4523
  const approvalLedger = debitLedger(priorLedger, liveSpendRef.get?.() ?? { costMicroUsd: 0, tokens: 0, turns: 0, walltimeMs: 0 }, resourceTotal, { countSlice: false });
4449
4524
  const mintedAt = Date.now();
4450
4525
  cp = {
4451
4526
  token,
4452
- version: realApproval !== undefined ? REAL_APPROVAL_CHECKPOINT_VERSION : f012CheckpointState() ? F012_CHECKPOINT_VERSION : orgAdmissionCheckpointState() ? ORG_ADMISSION_CHECKPOINT_VERSION : approvalLedger.totalTokens !== undefined ? TOKEN_CHECKPOINT_VERSION : BINDING_CHECKPOINT_VERSION,
4527
+ version: faceCheckpointState() ? FACE_CHECKPOINT_VERSION : realApproval !== undefined ? REAL_APPROVAL_CHECKPOINT_VERSION : f012CheckpointState() ? F012_CHECKPOINT_VERSION : orgAdmissionCheckpointState() ? ORG_ADMISSION_CHECKPOINT_VERSION : approvalLedger.totalTokens !== undefined ? TOKEN_CHECKPOINT_VERSION : BINDING_CHECKPOINT_VERSION,
4453
4528
  scope,
4454
4529
  sessionId,
4455
4530
  leafId,
@@ -4799,21 +4874,42 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4799
4874
  promptManifest.lowering = turnSnapshot.lowering;
4800
4875
  const promptOverheadTokens = Math.ceil((systemPrompt.length +
4801
4876
  harnessTools.reduce((a, t) => a + t.name.length + (t.description?.length ?? 0) + JSON.stringify(t.parameters ?? {}).length, 0)) / charsPerToken);
4877
+ const announceAttachmentSkip = (path, reason) => {
4878
+ deps.onError?.(new Error(`working-file attachment skipped — ${path}: ${reason}`), {
4879
+ phase: "degraded",
4880
+ sessionId,
4881
+ classification: "attachment-read",
4882
+ });
4883
+ };
4802
4884
  const readTaskFile = handsEnabled
4803
4885
  ? async (path) => {
4804
4886
  try {
4887
+ let key = path;
4805
4888
  if (attachmentRootCanonical !== undefined) {
4806
- const rk = await resolveKey(executionEnv, attachmentRootCanonical, path, undefined, undefined, additionalRootsCanonical);
4807
- if (!rk.ok)
4889
+ const rk = await resolveKey(executionEnv, attachmentRootCanonical, path, undefined, undefined, [...additionalRootsCanonical, ...additionalReadRootsCanonical], undefined, readDenyMatcher, resolvedReadFace);
4890
+ if (!rk.ok) {
4891
+ announceAttachmentSkip(path, rk.violation.message);
4892
+ if (rk.violation.code === "read_path_denied" && rk.violation.pattern !== undefined) {
4893
+ return { withheld: { pattern: rk.violation.pattern } };
4894
+ }
4808
4895
  return null;
4896
+ }
4897
+ key = rk.key;
4809
4898
  }
4810
- const r = await executionEnv.readBinaryFile(path);
4811
- if (!r.ok)
4899
+ const r = await executionEnv.readBinaryFile(key);
4900
+ if (!r.ok) {
4901
+ announceAttachmentSkip(path, r.error.message);
4812
4902
  return null;
4903
+ }
4813
4904
  const d = decodeTextBytes(r.value);
4814
- return d.malformed ? null : d.text;
4905
+ if (d.malformed) {
4906
+ announceAttachmentSkip(path, "malformed text encoding (truncated UTF-16 body)");
4907
+ return null;
4908
+ }
4909
+ return d.text;
4815
4910
  }
4816
- catch {
4911
+ catch (err) {
4912
+ announceAttachmentSkip(path, err instanceof Error ? err.message : String(err));
4817
4913
  return null;
4818
4914
  }
4819
4915
  }
@@ -4823,7 +4919,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4823
4919
  if (attachmentRootCanonical === undefined)
4824
4920
  return path;
4825
4921
  try {
4826
- const rk = await resolveKey(executionEnv, attachmentRootCanonical, path, undefined, handsCwdRef?.current, additionalRootsCanonical);
4922
+ const rk = await resolveKey(executionEnv, attachmentRootCanonical, path, undefined, handsCwdRef?.current, [...additionalRootsCanonical, ...additionalReadRootsCanonical], undefined, undefined, resolvedReadFace);
4827
4923
  return rk.ok ? rk.key : path;
4828
4924
  }
4829
4925
  catch {
@@ -300,7 +300,7 @@ export declare class Runner {
300
300
  * `inheritedManifestScope` and every other RunInternals field) — the library caller is trusted to
301
301
  * re-supply the genuine chain; a caller able to invoke resumeStream already holds full Runner
302
302
  * authority, so constraint substitution here is outside the enforced threat model (the count check
303
- * catches shape MISTAKES, not a malicious trusted caller). */
303
+ * catches shape MISTAKES, not a untrusted trusted caller). */
304
304
  internals?: RunInternals): Promise<TaskStream>;
305
305
  /**
306
306
  * design/45 resume step: with the harness idle and the branch rewound to the suspension leaf, resolve
@@ -1,6 +1,7 @@
1
+ import { persistedReadDenyEntryProblem } from "../../tools/fs/read-deny.js";
1
2
  import { AgentHarness, DEFAULT_COMPACTION_SETTINGS, uuidv7 } from "../../internal/harness.js";
2
3
  import { snapshotActorAssertion } from "../../internal/llm.js";
3
- import { CheckpointError, BINDING_CHECKPOINT_VERSION, checkpointVersionOf, F012_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, remainingBudgetMicroUsd, readPendingSteerQueue, remainingTokens, validatePendingSteer, winnerFromOutcome, } from "../checkpoint-store.js";
4
+ import { CheckpointError, BINDING_CHECKPOINT_VERSION, checkpointVersionOf, F012_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, FACE_CHECKPOINT_VERSION, remainingBudgetMicroUsd, readPendingSteerQueue, remainingTokens, validatePendingSteer, winnerFromOutcome, } from "../checkpoint-store.js";
4
5
  import { engineVersion } from "../version.js";
5
6
  import { CONFIG_CATALOG_VERSION, declarationReasons, resolveEffectiveConfig } from "../../config/catalog.js";
6
7
  import { eventDefaultOn } from "../../prompt-assembly/event-registry.js";
@@ -35,7 +36,7 @@ import { hasDestroy, isIsolated } from "../remote-env.js";
35
36
  import { hasBackgroundShell, sweepBackgroundShells } from "../background-shell.js";
36
37
  import { cloneObserverInput, formatHookFeedback } from "../hooks.js";
37
38
  import { buildHumanInputEvent, projectHumanInput } from "../human-input-projection.js";
38
- import { delimitUntrusted, inlineUntrusted, sanitizeUntrustedText } from "../untrusted-text.js";
39
+ import { delimitUntrusted, inlineUntrusted, REVIEWER_NOTE_MAX_BODY, sanitizeUntrustedText } from "../untrusted-text.js";
39
40
  import { reconcileInterruptedSession } from "../session-reconcile.js";
40
41
  import { RunnerSharedToolResultStore } from "../tool-result-store.js";
41
42
  import { formatDiagnosticsBlock } from "../lsp-diagnostics.js";
@@ -245,7 +246,7 @@ function resumeContinuation(resume) {
245
246
  if (resume.outcome.gate === "dry_run_review") {
246
247
  const verdict = resume.outcome.decision === "approve"
247
248
  ? "Your predicted change was REVIEWED and APPROVED; it has been applied"
248
- : `Your predicted change was REVIEWED and REJECTED${resume.outcome.reason ? `: ${delimitUntrusted("reviewer note", resume.outcome.reason)}` : ""}; it was NOT applied`;
249
+ : `Your predicted change was REVIEWED and REJECTED${resume.outcome.reason ? `: ${delimitUntrusted("reviewer note", resume.outcome.reason, REVIEWER_NOTE_MAX_BODY)}` : ""}; it was NOT applied`;
249
250
  return formatHookFeedback(`You were resumed after a dry-run review. ${verdict}. Before continuing, re-orient from the workspace ` +
250
251
  "(run `git status` and review your recent changes) to confirm the current state, then continue the " +
251
252
  "remaining work. Do NOT restart the task or re-run work that is already done.");
@@ -265,7 +266,7 @@ function resumeContinuation(resume) {
265
266
  "or restart from scratch, just carry out the revised plan.\n\nThe REVISED plan to follow is:\n" +
266
267
  delimitUntrusted("revised plan", resume.outcome.editedPlan));
267
268
  }
268
- const why = resume.outcome.reason ? ` Reviewer note: ${delimitUntrusted("reviewer note", resume.outcome.reason)}` : "";
269
+ const why = resume.outcome.reason ? ` Reviewer note: ${delimitUntrusted("reviewer note", resume.outcome.reason, REVIEWER_NOTE_MAX_BODY)}` : "";
269
270
  return formatHookFeedback(`Your proposed PLAN was REVIEWED and REJECTED; it was NOT executed.${why} Produce a NEW plan that ` +
270
271
  "addresses the concern, then continue. This is a RESUMED task — re-plan from the current state; do " +
271
272
  "NOT execute the rejected plan.");
@@ -276,7 +277,7 @@ function resumeContinuation(resume) {
276
277
  }
277
278
  const decided = resume.outcome.decision === "allow"
278
279
  ? "was APPROVED and has now been executed — its result is in the tool results above"
279
- : `was DENIED${resume.outcome.reason ? `: ${delimitUntrusted("reviewer note", resume.outcome.reason)}` : ""}`;
280
+ : `was DENIED${resume.outcome.reason ? `: ${delimitUntrusted("reviewer note", resume.outcome.reason, REVIEWER_NOTE_MAX_BODY)}` : ""}`;
280
281
  const remaining = pendingAction.batchToolCallIds.filter((id) => id !== pendingAction.toolCallId && !pendingAction.completedCallIds.includes(id)).length;
281
282
  const batchNote = remaining > 0
282
283
  ? ` ${remaining} other tool call(s) from that batch were returned as deferred and not run — re-issue any you still need.`
@@ -3718,11 +3719,36 @@ export class Runner {
3718
3719
  (preCasGateBit.origin === "org_rule" ||
3719
3720
  preCasGateBit.origin === "org_unavailable" ||
3720
3721
  preCasGateBit.origin === "policy");
3721
- if (checkpointVersionOf(cp) >= REAL_APPROVAL_CHECKPOINT_VERSION ? !preCasBitWellFormed : preCasGateBit !== undefined) {
3722
+ const faceSaysGoverned = (cp.state.readFace?.realApproval) === true;
3723
+ const preCasBitViolation = checkpointVersionOf(cp) === REAL_APPROVAL_CHECKPOINT_VERSION
3724
+ ? !preCasBitWellFormed
3725
+ : checkpointVersionOf(cp) < REAL_APPROVAL_CHECKPOINT_VERSION
3726
+ ? preCasGateBit !== undefined
3727
+ : faceSaysGoverned
3728
+ ? !preCasBitWellFormed
3729
+ : preCasGateBit !== undefined && !preCasBitWellFormed;
3730
+ if (preCasBitViolation) {
3722
3731
  throw new CheckpointError("checkpoint.invalid_outcome", checkpointVersionOf(cp) >= REAL_APPROVAL_CHECKPOINT_VERSION
3723
- ? `a v${checkpointVersionOf(cp)} checkpoint must carry a well-formed non-budgetable realApproval gate bit (origin org_rule/org_unavailable/policy) on an irreversible_ask gate — this row does not; refusing to resume a damaged real-approval row (corruption / downgrade guard), the checkpoint stays pending`
3732
+ ? `a v${checkpointVersionOf(cp)} checkpoint's realApproval gate bit must be well-formed (origin org_rule/org_unavailable/policy)${checkpointVersionOf(cp) === REAL_APPROVAL_CHECKPOINT_VERSION ? " and present on an irreversible_ask gate" : ""} — this row's is not; refusing to resume a damaged real-approval row (corruption / downgrade guard), the checkpoint stays pending`
3724
3733
  : `a v${checkpointVersionOf(cp)} checkpoint carries a realApproval gate bit no release of that version ever minted — refusing to honor a fabricated origin (corruption / forgery guard), the checkpoint stays pending`, { reason: checkpointVersionOf(cp) >= REAL_APPROVAL_CHECKPOINT_VERSION ? "real_approval_damaged" : "real_approval_forged" });
3725
3734
  }
3735
+ {
3736
+ const faceSectionRaw = cp.state.readFace;
3737
+ const faceCandidate = faceSectionRaw !== undefined && typeof faceSectionRaw === "object" && faceSectionRaw !== null
3738
+ ? faceSectionRaw
3739
+ : undefined;
3740
+ const faceWellFormed = faceCandidate !== undefined &&
3741
+ (faceCandidate.face === "open" || faceCandidate.face === "roots") &&
3742
+ (faceCandidate.realApproval === undefined || faceCandidate.realApproval === true) &&
3743
+ (faceCandidate.denyEntries === undefined ||
3744
+ (Array.isArray(faceCandidate.denyEntries) &&
3745
+ faceCandidate.denyEntries.every((e) => persistedReadDenyEntryProblem(e) === null)));
3746
+ if (checkpointVersionOf(cp) >= FACE_CHECKPOINT_VERSION ? !faceWellFormed : faceSectionRaw !== undefined) {
3747
+ throw new CheckpointError("checkpoint.invalid_outcome", checkpointVersionOf(cp) >= FACE_CHECKPOINT_VERSION
3748
+ ? `a v${checkpointVersionOf(cp)} checkpoint must carry a well-formed read-face section (state.readFace) — this row's is missing or malformed; refusing to resume a damaged read-face row (corruption / downgrade guard), the checkpoint stays pending`
3749
+ : `a v${checkpointVersionOf(cp)} checkpoint carries a read-face section no release of that version ever minted — refusing to honor a fabricated read-face state (corruption / forgery guard), the checkpoint stays pending`, { reason: checkpointVersionOf(cp) >= FACE_CHECKPOINT_VERSION ? "read_face_damaged" : "read_face_forged" });
3750
+ }
3751
+ }
3726
3752
  if ((preCasGateBit?.origin === "org_rule" || preCasGateBit?.origin === "org_unavailable") &&
3727
3753
  this.deps.permissionRuleOrg === undefined) {
3728
3754
  throw new CheckpointError("checkpoint.unsupported_version", `this checkpoint's approval was minted under organization governance (${preCasGateBit.origin}) and this worker has no org adjudication wiring (permissionRuleOrg) — a governed approval may only be redeemed where governance can be enforced; the checkpoint stays pending, resume it on an org-wired worker`, { reason: "governed_unwired" });
@@ -4004,7 +4030,7 @@ export class Runner {
4004
4030
  : outcome.settledBy === "aborted"
4005
4031
  ? `The approval for the pending tool call "${pendingAction.toolName}" ended without anyone deciding it (it was cancelled or could not be delivered), so it was not executed.`
4006
4032
  : `The pending tool call "${pendingAction.toolName}" was denied by an approver.`;
4007
- const reason = outcome.reason ? delimitUntrusted("reviewer note", outcome.reason) : defaultDenial;
4033
+ const reason = outcome.reason ? delimitUntrusted("reviewer note", outcome.reason, REVIEWER_NOTE_MAX_BODY) : defaultDenial;
4008
4034
  emitEnd(true, { content: formatHookFeedback(reason) });
4009
4035
  const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, formatHookFeedback(reason), true));
4010
4036
  emitCommitted(eid, "toolResult", pendingAction.toolCallId);
@@ -53,10 +53,11 @@ export declare function isWithin(root: string, p: string): boolean;
53
53
  * - `toolDeny` → deny the listed tools.
54
54
  * - `toolAllow` (if set) → deny any tool NOT listed (a narrowing allowlist).
55
55
  * - `commandAllow`/`commandDeny` → delegate to {@link createCoarseCommandNamePolicy} (argv[0] names; only
56
- * speaks about shell tools). allowlist mode denies the un-listed/un-parseable; blocklist mode asks on the
57
- * un-parseable bypass surface (the gate decides).
56
+ * speaks about shell tools) with `defaultAction:"deny"` in BOTH modes so this policy structurally
57
+ * emits no ask. Allowlist mode denies the un-listed and the un-parseable; blocklist mode denies the
58
+ * listed and the un-parseable (a parseable command absent from the deny list stays allowed).
58
59
  * - `allowDirs` (if set) → a WRITE tool must land inside one of the dirs (resolved via `canonicalizeTarget`,
59
- * so a symlink can't smuggle a write out); a write-capable tool that cannot be path-confined (e.g. `bash`)
60
+ * so a symlink can't carry across a write out); a write-capable tool that cannot be path-confined (e.g. `bash`)
60
61
  * is DENIED while `allowDirs` is set (fail-closed, mirrors active-skill-scope MAJOR-2).
61
62
  * Returns `allow` when nothing constrains the call. `env`/`rootPath` are the task's real exec env + root, so
62
63
  * matching sees the actual filesystem (a remote/E2B task resolves in its container). RB-108: a relative
@@ -9,7 +9,7 @@
9
9
  * mask the original error") — but a promise that never settles is not a throw, and one wedged
10
10
  * adapter turned into: for-await done, `result()` never settles (②型, process alive), or — when the
11
11
  * wedge happened before the queue closed — a zero-handle pure-logic hang (the 2.12.0 exit-13 shape).
12
- * The session lock releases in the run IIFE's finally, so one wedged run also poisoned every later
12
+ * The session lock releases in the run IIFE's finally, so one wedged run also contaminated every later
13
13
  * same-session resume.
14
14
  *
15
15
  * The remedy is the same doctrine as the workflow journal drain (batch-4 F2): a best-effort leg gets
@@ -223,7 +223,7 @@ export declare function resolveToolSearch(args: ToolSearchArgs, registry: Readon
223
223
  * direct call (ruled 2026-08-03) activates too and is flagged as an error, so it lands in that same
224
224
  * under-approximated class by construction: within the process the in-memory `active` set carries it, and
225
225
  * a replay that loses it costs the corrected call — which succeeds, and is itself carrier ②. The one way to
226
- * forge a positive is a deployment `tool_result` hook that rewrites a rejection's error flag to false,
226
+ * self-declare a positive is a deployment `tool_result` hook that rewrites a rejection's error flag to false,
227
227
  * which is the deployment editing its own transcript.
228
228
  *
229
229
  * design/116 W1-2 note: pre-116 checkpoints whose KEYWORD queries activated up to 25 tools re-derive at
@@ -62,7 +62,7 @@ const CC_DETAIL_TYPES = new Set([
62
62
  "agent", "task", "task-list", "task-output", "workflow-run",
63
63
  "web-fetch", "web-search", "todo", "cron-create", "cron-delete", "cron-list", "image",
64
64
  "task-stop", "tool-search", "repo-map", "fork", "enter-plan-mode", "exit-plan-mode",
65
- "monitor-start", "path_not_in_root", "readonly_out_of_root", "bash_invalid_timeout",
65
+ "monitor-start", "path_not_in_root", "read_path_denied", "readonly_out_of_root", "bash_invalid_timeout",
66
66
  "report-findings", "schedule-wakeup", "send-message", "agent-transcript", "a2a", "document",
67
67
  ]);
68
68
  export const structuredFrom = (result) => {