@sema-agent/core 5.31.0 → 5.33.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 +96 -0
  2. package/dist/agents/cascade.d.ts +49 -1
  3. package/dist/agents/cascade.js +2 -2
  4. package/dist/agents/verify.d.ts +70 -4
  5. package/dist/agents/verify.js +62 -16
  6. package/dist/core/checkpoint-store.d.ts +95 -0
  7. package/dist/core/checkpoint-store.js +40 -0
  8. package/dist/core/hooks.d.ts +14 -6
  9. package/dist/core/hooks.js +14 -3
  10. package/dist/core/memory-engine/file-backend.d.ts +172 -22
  11. package/dist/core/memory-engine/file-backend.js +877 -79
  12. package/dist/core/memory-engine/memory-backend-contract.js +33 -0
  13. package/dist/core/runner/assemble-result.d.ts +7 -0
  14. package/dist/core/runner/assemble-result.js +1 -1
  15. package/dist/core/runner/prepare-acquire-reconcile.d.ts +72 -0
  16. package/dist/core/runner/prepare-acquire-reconcile.js +126 -0
  17. package/dist/core/runner/prepare-config-doors.d.ts +140 -0
  18. package/dist/core/runner/prepare-config-doors.js +250 -0
  19. package/dist/core/runner/prepare-safety-scan.d.ts +53 -0
  20. package/dist/core/runner/prepare-safety-scan.js +80 -0
  21. package/dist/core/runner/prepare-task.d.ts +28 -80
  22. package/dist/core/runner/prepare-task.js +102 -586
  23. package/dist/core/runner/prepare-workspace-restore.d.ts +102 -0
  24. package/dist/core/runner/prepare-workspace-restore.js +144 -0
  25. package/dist/core/runner/runtask.js +8 -2
  26. package/dist/core/tool-policy.d.ts +25 -0
  27. package/dist/core/types.d.ts +149 -13
  28. package/dist/index.d.ts +5 -4
  29. package/dist/index.js +2 -2
  30. package/dist/orchestration/workflow-governance.d.ts +6 -4
  31. package/dist/tools/fs/bash-readonly-classifier.d.ts +9 -3
  32. package/dist/tools/fs/bash-readonly-classifier.js +4 -1
  33. package/dist/tools/fs/fs-bash.d.ts +19 -3
  34. package/dist/tools/fs/fs-bash.js +26 -1
  35. package/dist/tools/fs/index.d.ts +27 -7
  36. package/dist/tools/fs/index.js +7 -2
  37. package/dist/tools/fs/read-deny.d.ts +66 -8
  38. package/dist/tools/fs/read-deny.js +75 -39
  39. package/dist/tools/fs/read-face.d.ts +24 -2
  40. package/dist/tools/fs/read-face.js +9 -0
  41. package/dist/tools/fs/search.js +2 -0
  42. package/package.json +1 -1
@@ -0,0 +1,250 @@
1
+ import { resolveBrainCallGuardrailMs } from "../../brain/timeout.js";
2
+ import { assertReadFaceValue } from "../../tools/fs/index.js";
3
+ import { resolveCheckpointStore } from "../checkpoint-store.js";
4
+ import { preflightLockedConfig } from "../locked-config.js";
5
+ import { assertRetentionCapability } from "../retention.js";
6
+ import { resolveModel, resolveTaskModel, roleModelIfSet } from "../roles.js";
7
+ import { resolveUsageWindows } from "../usage-window-store.js";
8
+ import { deriveAskEffective, resolveAskSeamForm, resolveQuestionSeam } from "../wiring-manifest.js";
9
+ const TASK_LIMIT_KEY_DICT = {
10
+ maxTokens: true,
11
+ maxCostUsd: true,
12
+ maxTurns: true,
13
+ maxWalltimeMs: true,
14
+ maxOutputTokens: true,
15
+ approachNotice: true,
16
+ budgetStreamCancel: true,
17
+ degrade: true,
18
+ brainCallGuardrailMs: true,
19
+ };
20
+ const TASK_LIMIT_KEYS = Object.keys(TASK_LIMIT_KEY_DICT);
21
+ const NUMERIC_TASK_LIMIT_KEYS = ["maxTokens", "maxCostUsd", "maxTurns", "maxWalltimeMs", "maxOutputTokens"];
22
+ const RETIRED_TASK_LIMIT_KEYS = {
23
+ timeoutSec: "maxWalltimeMs (milliseconds, not seconds)",
24
+ deadlineNudge: "approachNotice",
25
+ callCapByDeadline: "(retired — no replacement)",
26
+ gracefulFinalize: "(retired — the approach notice is the only end-of-budget prompt)",
27
+ };
28
+ export function limitConfigError(code, message) {
29
+ const e = new Error(message);
30
+ e.code = code;
31
+ return e;
32
+ }
33
+ export function resolveTaskLimits(limits) {
34
+ if (limits === undefined)
35
+ return undefined;
36
+ if (typeof limits !== "object" || Array.isArray(limits)) {
37
+ throw limitConfigError("config.limit_invalid", `TaskSpec.limits must be an object (got ${Array.isArray(limits) ? "an array" : typeof limits})`);
38
+ }
39
+ const raw = limits;
40
+ const legal = TASK_LIMIT_KEYS;
41
+ for (const key of Object.keys(raw)) {
42
+ if (legal.includes(key))
43
+ continue;
44
+ const replacement = RETIRED_TASK_LIMIT_KEYS[key];
45
+ throw limitConfigError("config.limit_unknown_key", `TaskSpec.limits.${key} is not a limit this engine reads` +
46
+ (replacement !== undefined ? ` — it was replaced by \`${replacement}\`` : "") +
47
+ `. Legal keys: ${TASK_LIMIT_KEYS.join(", ")}. Refused rather than ignored: a limit that is silently dropped reads to the caller as an armed ceiling.`);
48
+ }
49
+ for (const key of NUMERIC_TASK_LIMIT_KEYS) {
50
+ const value = raw[key];
51
+ if (value === undefined)
52
+ continue;
53
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
54
+ throw limitConfigError("config.limit_invalid", `TaskSpec.limits.${key} must be a finite, non-negative number (got ${String(value)}) — an unevaluable ceiling is not a ceiling, and folding it to a default would run the task under a limit nobody chose.`);
55
+ }
56
+ }
57
+ const notice = raw.approachNotice;
58
+ if (notice !== undefined && notice !== false) {
59
+ if (typeof notice !== "object" || notice === null || Array.isArray(notice)) {
60
+ throw limitConfigError("config.limit_invalid", `TaskSpec.limits.approachNotice must be \`false\` or { at?: [first, second] } (got ${String(notice)})`);
61
+ }
62
+ const at = notice.at;
63
+ if (at !== undefined) {
64
+ const ok = Array.isArray(at) &&
65
+ at.length === 2 &&
66
+ at.every((n) => typeof n === "number" && Number.isFinite(n) && n > 0 && n <= 1) &&
67
+ at[0] <= at[1];
68
+ if (!ok) {
69
+ throw limitConfigError("config.limit_invalid", `TaskSpec.limits.approachNotice.at must be two fractions in (0, 1] with first <= second (got ${JSON.stringify(at)})`);
70
+ }
71
+ }
72
+ }
73
+ return limits;
74
+ }
75
+ export function isFableFamilyModelId(id) {
76
+ const tail = id.toLowerCase().split("/").pop() ?? "";
77
+ return /^claude-fable-\d/.test(tail) || /^claude-mythos-5(?!\d)/.test(tail);
78
+ }
79
+ export function resolveModelPromptTraits(model, spec, internals) {
80
+ return {
81
+ promptProfile: spec.promptProfile ?? internals?.promptProfile ?? "simple",
82
+ fableMitigations: isFableFamilyModelId(model.id),
83
+ };
84
+ }
85
+ export function prepareConfigDoors(input) {
86
+ const { deps, sessions, resume, internals } = input;
87
+ let spec = input.spec;
88
+ const toolFaceSnapshot = {
89
+ exclude: spec.excludeTools ? Object.freeze([...spec.excludeTools]) : undefined,
90
+ defer: spec.deferTools ? Object.freeze([...spec.deferTools]) : undefined,
91
+ alwaysLoad: spec.alwaysLoadTools ? Object.freeze([...spec.alwaysLoadTools]) : undefined,
92
+ };
93
+ const promptProfile = resolveModelPromptTraits({ id: "" }, spec, internals).promptProfile;
94
+ if (spec.resumeAt !== undefined) {
95
+ if (resume) {
96
+ const e = new Error("resumeAt cannot be combined with a durable resume");
97
+ e.code = "resume_at.conflicts_resume";
98
+ throw e;
99
+ }
100
+ if (!spec.sessionId) {
101
+ const e = new Error("resumeAt requires a sessionId (the session to branch)");
102
+ e.code = "resume_at.no_session";
103
+ throw e;
104
+ }
105
+ }
106
+ assertReadFaceValue(spec.readFace, "TaskSpec.readFace");
107
+ assertReadFaceValue(deps.readFace, "readFace (deployment seat)");
108
+ if (spec.resumeAtMode !== undefined) {
109
+ if (spec.resumeAt === undefined) {
110
+ const e = new Error(`resumeAtMode "${spec.resumeAtMode}" requires resumeAt (there is no branch target to position against)`);
111
+ e.code = "resume_at.mode_without_target";
112
+ throw e;
113
+ }
114
+ if (spec.resumeAtMode !== "at" && spec.resumeAtMode !== "before") {
115
+ const e = new Error(`resumeAtMode "${String(spec.resumeAtMode)}" is invalid — expected "at" (inclusive branch, the default) or "before" (exclusive branch)`);
116
+ e.code = "resume_at.invalid_mode";
117
+ throw e;
118
+ }
119
+ }
120
+ if (spec.toolMaterializeStrategy !== undefined && spec.toolMaterializeStrategy !== "swap" && spec.toolMaterializeStrategy !== "static") {
121
+ const e = new Error(`toolMaterializeStrategy must be "swap" or "static" (got ${JSON.stringify(spec.toolMaterializeStrategy)}).`);
122
+ e.code = "config.tool_materialize_invalid";
123
+ throw e;
124
+ }
125
+ if (spec.memoryPersistenceCapable !== undefined && typeof spec.memoryPersistenceCapable !== "boolean") {
126
+ const e = new Error(`memoryPersistenceCapable must be a boolean when present (got ${JSON.stringify(spec.memoryPersistenceCapable)}) — a non-boolean would silently read as capable.`);
127
+ e.code = "config.memory_persistence_invalid";
128
+ throw e;
129
+ }
130
+ if (spec.toolMaterializeStrategy === "static" && spec.deferSelfResolve === false) {
131
+ const e = new Error(`toolMaterializeStrategy "static" cannot be combined with deferSelfResolve: false — with the direct-call ` +
132
+ `lane disabled a placeholder is never swapped and never self-resolves, so no deferred tool could ever be ` +
133
+ `called. Use "swap", or leave deferSelfResolve on.`);
134
+ e.code = "config.tool_materialize_unreachable";
135
+ throw e;
136
+ }
137
+ if (resume === undefined && spec.objective.trim().length === 0) {
138
+ const e = new Error("TaskSpec.objective is empty — a task needs an instruction (an empty user message is rejected by strict model endpoints and would fail every later request of the session).");
139
+ e.code = "config.empty_objective";
140
+ throw e;
141
+ }
142
+ const lockedPreflight = preflightLockedConfig(spec, deps);
143
+ assertRetentionCapability({
144
+ policy: deps.retentionPolicy,
145
+ locked: lockedPreflight.lockedKeys.has("retentionPolicy"),
146
+ stores: [
147
+ { name: "sessionStore", store: sessions },
148
+ { name: "checkpointStore", store: resolveCheckpointStore(spec, deps) },
149
+ { name: "toolResultStore", store: deps.toolResultStore },
150
+ ],
151
+ });
152
+ const resolvedInteractionPosture = spec.interactionPosture ?? internals?.parentInteractionPosture ?? deps.interactionPosture;
153
+ {
154
+ const interactionPosture = resolvedInteractionPosture;
155
+ const discloseInteractionPostureRefusal = (err) => {
156
+ try {
157
+ deps.onError?.(err, { phase: "config", sessionId: spec.sessionId ?? "(pre-session)", classification: "interaction-posture-refused" });
158
+ }
159
+ catch {
160
+ }
161
+ };
162
+ if (interactionPosture !== undefined && interactionPosture !== "interactive" && interactionPosture !== "headless") {
163
+ const e = new Error(`interactionPosture ${JSON.stringify(interactionPosture)} is not a recognized posture ("interactive" | "headless") — ` +
164
+ `an unevaluable declaration is refused loudly, never folded to either posture.`);
165
+ e.code = "config.interaction_posture";
166
+ discloseInteractionPostureRefusal(e);
167
+ throw e;
168
+ }
169
+ if (interactionPosture === "interactive") {
170
+ const askDoor = resolveAskSeamForm(spec, deps);
171
+ const humanReachable = deriveAskEffective(askDoor.form, "unresolved") === "human_reachable";
172
+ const questionDoor = resolveQuestionSeam(spec, deps);
173
+ const strippedByEngine = internals?.questionFaceStripped === true;
174
+ if (spec.interactiveTools === false) {
175
+ const e = new Error(`interaction posture "interactive" declared together with interactiveTools: false — the hard-headless ` +
176
+ `clamp removes the AskUserQuestion mount, so no content question can ever reach the human this posture ` +
177
+ `promises. Drop one of the two declarations.`);
178
+ e.code = "config.interaction_posture";
179
+ discloseInteractionPostureRefusal(e);
180
+ throw e;
181
+ }
182
+ if (!humanReachable || !(questionDoor.wired || strippedByEngine)) {
183
+ const missing = [];
184
+ if (!humanReachable) {
185
+ missing.push(askDoor.form === "absent"
186
+ ? "no onAsk approver is wired (spec.onAsk ?? deps.onAsk is absent — asks would auto-deny or park)"
187
+ : `the resolved onAsk seat is the blanket policy "${askDoor.form}" (${askDoor.provenance ?? "?"}) — a policy setting is not a reachable human`);
188
+ }
189
+ if (!questionDoor.wired && !strippedByEngine) {
190
+ missing.push("no content-question channel (spec.onQuestion ?? deps.onQuestion is absent, and this leg is not an engine-stripped background lane)");
191
+ }
192
+ const e = new Error(`interaction posture "interactive" declared, but this assembly cannot reach a human: ${missing.join("; ")}. ` +
193
+ `Wire the missing seam(s), or drop the posture declaration (absent = no check).`);
194
+ e.code = "config.interaction_posture";
195
+ discloseInteractionPostureRefusal(e);
196
+ throw e;
197
+ }
198
+ }
199
+ }
200
+ resolveTaskLimits(spec.limits);
201
+ if (spec.resourceSuspend !== undefined) {
202
+ const rsus = spec.resourceSuspend;
203
+ if (typeof rsus.scope !== "string" || rsus.scope === "") {
204
+ throw limitConfigError("config.limit_invalid", `TaskSpec.resourceSuspend.scope must be a non-empty string (got ${String(rsus.scope)}) — it is the multi-tenant isolation key every resource checkpoint is filed under.`);
205
+ }
206
+ for (const key of ["totalBudgetUsd", "totalTokens", "maxSlices", "ttlMs"]) {
207
+ const value = rsus[key];
208
+ if (value === undefined)
209
+ continue;
210
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
211
+ throw limitConfigError("config.limit_invalid", `TaskSpec.resourceSuspend.${key} must be a finite, non-negative number (got ${String(value)}) — an unevaluable allocation is not an allocation, and a NaN here blinds even the validated per-slice window.`);
212
+ }
213
+ }
214
+ }
215
+ const usageWindows = resolveUsageWindows(deps.usageWindows);
216
+ const brainCallGuardrailRef = {};
217
+ const brainCallGuardrailMs = resolveBrainCallGuardrailMs(spec.limits?.brainCallGuardrailMs ?? deps.brainCallGuardrailMs);
218
+ if (spec.agents !== undefined && spec.agents.length > 0) {
219
+ const pool = spec.tools ?? [];
220
+ if (!pool.some((t) => typeof t.withAgents === "function")) {
221
+ const e = new Error(`TaskSpec.agents was provided but no delegation tool (createSubagentTool) is mounted in spec.tools — the per-task agents could never be offered. Mount the delegation tool, or drop spec.agents.`);
222
+ e.code = "config.agents.no_delegation_tool";
223
+ throw e;
224
+ }
225
+ const perTask = spec.agents;
226
+ spec = { ...spec, tools: pool.map((t) => (typeof t.withAgents === "function" ? t.withAgents(perTask) : t)) };
227
+ }
228
+ const resolvedRole = resolveTaskModel(spec, deps);
229
+ const model = resolvedRole.model;
230
+ const fableMitigations = resolveModelPromptTraits(model, spec, internals).fableMitigations;
231
+ const thinking = spec.thinking ?? resolvedRole.thinking ?? model.defaultThinking;
232
+ const compModel = spec.compactionModel
233
+ ? resolveModel(spec.compactionModel, deps.models)
234
+ : roleModelIfSet("summarize", spec, deps);
235
+ return {
236
+ spec,
237
+ toolFaceSnapshot,
238
+ promptProfile,
239
+ lockedPreflight,
240
+ resolvedInteractionPosture,
241
+ resolvedRole,
242
+ model,
243
+ thinking,
244
+ compModel,
245
+ fableMitigations,
246
+ usageWindows,
247
+ brainCallGuardrailRef,
248
+ brainCallGuardrailMs,
249
+ };
250
+ }
@@ -0,0 +1,53 @@
1
+ import type { RunnerDeps, TaskSpec, ToolEffect } from "../types.js";
2
+ /** The namespaced-name shapes the protocol table currently owns, rendered for the two messages that have
3
+ * to name them (the caller-name reservation and the policy audit's unprefixed-name arm). Read from the
4
+ * table rather than written out: a message that hard-codes ONE protocol becomes wrong — while staying
5
+ * green — the moment a second one is appended, and both messages tell a deployment what to write. */
6
+ export declare const NAMESPACED_NAME_SHAPES: string;
7
+ export interface PrepareSafetyScanInput {
8
+ /** borrowed-readonly — the REBOUND spec (`spec′`, the config-doors result). The scan reads
9
+ * `tools`, `handsReadOnly` and `enablePlanMode`; it never mutates. */
10
+ spec: TaskSpec;
11
+ /** borrowed-readonly — read only for the hands-enablement fact (`executionEnvFactory`/
12
+ * `executionEnv` presence gates the hand-band effect preseed). */
13
+ deps: RunnerDeps;
14
+ }
15
+ /**
16
+ * The scan's outputs. Ownership annotations (design/238 §4.2 four-class form):
17
+ *
18
+ * `ownToolNames` is **borrowed-readonly** — a first-await snapshot with no mutation anywhere after
19
+ * the scan; its one later read is the sandbox-boundary predicate's `.has()`.
20
+ *
21
+ * The six collections are **borrowed-mutable**: the driver and later phases fold into them IN
22
+ * PLACE (identity is part of the contract — closures alias these exact objects). Mutation owners
23
+ * after this phase, in driver order:
24
+ * · the workflow mount (registers the run-workflow tool's write effect);
25
+ * · the MCP/A2A materialize fold + the runtime refresh seam (protocol-axis tighten registration;
26
+ * on refresh, `toolEffects.delete` resets a replaced tool's effect record for the fresh fold —
27
+ * the irreversible/egress sets deliberately KEEP their entries, the tighten-only residue);
28
+ * · the hands/read-face block's shellGate fold (Bash/Monitor tier escalation + probe seeding);
29
+ * · the background-task / Monitor / worktree mounts (fixed-name effect registration);
30
+ * · the prepare-final effect sync (fills MISSING entries from the final tool set — never
31
+ * overrides a scanned value).
32
+ */
33
+ export interface PrepareSafetyScanResult {
34
+ /** borrowed-mutable — per-tool effect registry (alias-complete), the reconcile/path-policy read. */
35
+ toolEffects: Map<string, ToolEffect>;
36
+ /** borrowed-mutable — egress-marked tools (external writes; the gate never auto-allows them). */
37
+ egressTools: Set<string>;
38
+ /** borrowed-mutable — tools whose effect is irreversible (`always`, or `maybe` incl. by probe). */
39
+ irreversibleTools: Set<string>;
40
+ /** borrowed-mutable — static per-tool irreversibility tier (prepare-time resolution). */
41
+ irreversibilityTier: Map<string, "never" | "maybe" | "always">;
42
+ /** borrowed-mutable — explicit NEGATIVE axis judgments (report-face only; `riskAxesOf` reads). */
43
+ axisExplicitNegatives: Map<string, {
44
+ irreversible?: false;
45
+ egress?: false;
46
+ }>;
47
+ /** borrowed-mutable — declared reversibility probes by tool name. */
48
+ reversibilityProbes: Map<string, NonNullable<TaskSpec["tools"]>[number]["reversibilityProbe"]>;
49
+ /** borrowed-readonly — the caller roster's own names, snapshotted before the first await. */
50
+ ownToolNames: ReadonlySet<string>;
51
+ }
52
+ /** The B-2 phase body — the safety-scan slice, verbatim (see the module header for the contract). */
53
+ export declare function prepareSafetyScan(input: PrepareSafetyScanInput): PrepareSafetyScanResult;
@@ -0,0 +1,80 @@
1
+ import { PROTOCOL_TABLE } from "../protocol-table.js";
2
+ import { HAND_TOOL_EFFECTS } from "../../tools/fs/index.js";
3
+ import { OFFLOAD_TOOL_NAME } from "../tool-result-store.js";
4
+ import { PRESENT_PLAN_TOOL_NAME, ENTER_PLAN_MODE_TOOL_NAME } from "../present-plan-tool.js";
5
+ export const NAMESPACED_NAME_SHAPES = PROTOCOL_TABLE.map((ns) => `${ns.prefix}<peer>__<tool>`).join(", ");
6
+ export function prepareSafetyScan(input) {
7
+ const { spec, deps } = input;
8
+ const toolEffects = new Map();
9
+ const ownToolNames = new Set((spec.tools ?? []).map((t) => t.name));
10
+ const claimedAliases = new Set();
11
+ const egressTools = new Set();
12
+ const irreversibleTools = new Set();
13
+ const irreversibilityTier = new Map();
14
+ const axisExplicitNegatives = new Map();
15
+ const reversibilityProbes = new Map();
16
+ for (const t of spec.tools ?? []) {
17
+ if (t.name.includes("__")) {
18
+ const e = new Error(`Tool name "${t.name}" is invalid: "__" is reserved for the engine's protocol tool namespaces (${NAMESPACED_NAME_SHAPES}) and must not appear in a caller tool name.`);
19
+ e.code = "config.tool_name_invalid";
20
+ throw e;
21
+ }
22
+ if (t.effect) {
23
+ toolEffects.set(t.name, t.effect);
24
+ }
25
+ const tier = t.irreversibility ?? (t.reversibilityProbe ? "maybe" : undefined);
26
+ if (tier !== undefined) {
27
+ irreversibilityTier.set(t.name, tier);
28
+ if (tier === "always" || tier === "maybe")
29
+ irreversibleTools.add(t.name);
30
+ }
31
+ if (t.reversibilityProbe)
32
+ reversibilityProbes.set(t.name, t.reversibilityProbe);
33
+ if (t.egress) {
34
+ if (t.effect !== undefined && t.effect !== "write") {
35
+ const e = new Error(`Tool "${t.name}" declares egress:true with effect:"${t.effect}" — an egress tool (external write) must have effect:"write" (or omit effect; write is the default).`);
36
+ e.code = "config.egress_requires_write_effect";
37
+ throw e;
38
+ }
39
+ egressTools.add(t.name);
40
+ }
41
+ for (const alias of t.aliases ?? []) {
42
+ if (alias === t.name || ownToolNames.has(alias))
43
+ continue;
44
+ if (claimedAliases.has(alias))
45
+ continue;
46
+ claimedAliases.add(alias);
47
+ if (t.effect)
48
+ toolEffects.set(alias, t.effect);
49
+ if (tier !== undefined) {
50
+ irreversibilityTier.set(alias, tier);
51
+ if (tier === "always" || tier === "maybe")
52
+ irreversibleTools.add(alias);
53
+ }
54
+ if (t.reversibilityProbe)
55
+ reversibilityProbes.set(alias, t.reversibilityProbe);
56
+ if (t.egress)
57
+ egressTools.add(alias);
58
+ }
59
+ }
60
+ toolEffects.set(OFFLOAD_TOOL_NAME, "read");
61
+ if (deps.executionEnvFactory || deps.executionEnv) {
62
+ for (const [name, effect] of Object.entries(HAND_TOOL_EFFECTS))
63
+ toolEffects.set(name, effect);
64
+ if (spec.handsReadOnly === true)
65
+ toolEffects.set("Bash", "read");
66
+ }
67
+ if (spec.enablePlanMode === true) {
68
+ toolEffects.set(PRESENT_PLAN_TOOL_NAME, "read");
69
+ toolEffects.set(ENTER_PLAN_MODE_TOOL_NAME, "read");
70
+ }
71
+ return {
72
+ toolEffects,
73
+ egressTools,
74
+ irreversibleTools,
75
+ irreversibilityTier,
76
+ axisExplicitNegatives,
77
+ reversibilityProbes,
78
+ ownToolNames,
79
+ };
80
+ }
@@ -5,14 +5,14 @@ import { type AutoModeDecider } from "../auto-mode.js";
5
5
  import { type MaterializedMcp } from "../mcp.js";
6
6
  import { type MaterializedA2a } from "../a2a.js";
7
7
  import type { HarvestReport, MemorySessionHandle } from "../memory-engine/types.js";
8
- import { StoredSession } from "../session.js";
8
+ import type { StoredSession } from "../session.js";
9
9
  import type { SessionStore } from "../session.js";
10
10
  import { SubagentRetainLedger } from "../../agents/retain-ledger.js";
11
11
  import type { OnAsk, ToolCallRequest, ToolPolicy } from "../tool-policy.js";
12
12
  import { type ActiveSkillFrame } from "./active-skill-scope.js";
13
13
  import type { SessionPermissionRules } from "../session-policy-store.js";
14
14
  import { type Hooks, type OrgGateVerdict } from "../hooks.js";
15
- import { type RecoveredOrphan } from "../session-reconcile.js";
15
+ import type { RecoveredOrphan } from "../session-reconcile.js";
16
16
  import { CacheBreakDetector, type ToolFingerprintInput } from "../cache-break-detector.js";
17
17
  import { type BrainCallGuardrailRef } from "../../brain/timeout.js";
18
18
  import { type OutputRef, type BlockedRef, type SkillListingEntry } from "./synthetic-tools.js";
@@ -20,30 +20,21 @@ import type { MemoryEngine } from "../memory-engine/engine.js";
20
20
  import { type ToolManifestRow } from "../../prompt-assembly/tool-catalog.js";
21
21
  import type { ToolDisclosureManifest } from "../trace.js";
22
22
  import type { TaskNotificationPayload } from "../task-notification.js";
23
- import { type CwdRef } from "../../tools/fs/index.js";
23
+ import { type CwdRef, type ReadFace } from "../../tools/fs/index.js";
24
24
  import { type WorkflowSizeGuideline } from "../../orchestration/workflow-size-guideline.js";
25
25
  import type { Runner } from "./runtask.js";
26
26
  import { type CheckpointGate, type CheckpointState, type CheckpointToken, type ResourceLedger, type PlatformLimitReason, type ResourceLimitReason } from "../checkpoint-store.js";
27
27
  import { type WiringManifest } from "../wiring-manifest.js";
28
28
  import type { ActiveWorktreeSession, AgentMessage, AgentTool, ExecutionEnv } from "../../internal/harness.js";
29
- import type { NestedUsageAccum, RunnerDeps, TaskEvent, TaskLimits, TaskResult, TaskSpec, ToolActivity, ToolEffect } from "../types.js";
29
+ import type { NestedUsageAccum, RunnerDeps, TaskEvent, TaskResult, TaskSpec, ToolActivity, ToolEffect } from "../types.js";
30
30
  import type { RepairBundle } from "../../agents/repair-loop.js";
31
31
  /** Test seam (mirrors `__resetBashTimeoutAnnouncements`): never called by production code. */
32
32
  export declare function __resetMaterializeEnvAnnouncements(): void;
33
- /**
34
- * design/164validate `TaskSpec.limits` at the door and return it unchanged.
35
- *
36
- * Two refusals, both fail-loud (the {@link resolveStallTimeoutMs} posture: a bound nobody can evaluate is
37
- * not a bound, and folding it to a default would silently run the task under limits nobody chose):
38
- * - **unknown key** ⇒ `config.limit_unknown_key`. TypeScript already rejects a stale key, but a wire /
39
- * plain-JS caller does not go through TypeScript, and the retired names (`timeoutSec` above all) used
40
- * to be honored — accepting them silently is how a caller keeps believing a limit is armed when the
41
- * engine has stopped reading it.
42
- * - **unevaluable numeric value** (non-number / non-finite / negative) ⇒ `config.limit_invalid`, naming
43
- * the key. `0` is legal on every numeric axis: on `maxTurns` it is the documented "unbounded"
44
- * sentinel, and on the budget axes it is an exhausted window (absurd but honest).
45
- */
46
- export declare function resolveTaskLimits(limits: TaskLimits | undefined): TaskLimits | undefined;
33
+ /** Test seam (mirrors `__resetMalformedNoticeSeatAnnouncement`): never called by production code.
34
+ * Deliberately asymmetricit resets only the console latch: the WeakSet arm needs no seam
35
+ * because a test resets it by minting a fresh sink function (identity IS the ledger key), while
36
+ * the console arm's key is the process itself, which only this seam can refresh. */
37
+ export declare function __resetReadFaceClampAnnouncement(): void;
47
38
  /**
48
39
  * design/164 件四 — how long before an execution environment's declared `lifetimeMs` expires the engine
49
40
  * stops the run and checkpoints it. The margin has to cover ONE suspend: pausing/snapshotting the
@@ -138,6 +129,8 @@ export declare function checkpointScopeOf(spec: {
138
129
  principal?: string;
139
130
  }): string;
140
131
  export { resolveCheckpointStore } from "../checkpoint-store.js";
132
+ export { isFableFamilyModelId, resolveModelPromptTraits, resolveTaskLimits } from "./prepare-config-doors.js";
133
+ export { rebaseWorkspacePath, rebaseWorkspacePathAcross } from "./prepare-workspace-restore.js";
141
134
  /** Everything the run loop needs, built once by {@link prepareTask} (task setup, isolated from the loop). */
142
135
  export interface Prepared {
143
136
  harness: AgentHarness;
@@ -193,6 +186,16 @@ export interface Prepared {
193
186
  /** RB-430-a: prepare-time rewind disclosures (conversation-only branch / no snapshot backend / no file
194
187
  * env), echoed verbatim onto `TaskResult.rewindNotes`. Present only when there is something to say. */
195
188
  rewindNotes?: NonNullable<TaskResult["rewindNotes"]>;
189
+ /** #240 (design/199 v1.1) + #242 — the run's RESOLVED read face for the result observation seat
190
+ * (`TaskResult.effectiveReadFace`): `carrierReadFace()`'s value at prepare completion — the hands
191
+ * block's single resolution, or the hands-less legs' resolver run (live spec-time facts, with the
192
+ * checkpoint seed folded stricter-wins where one exists). Every leg that completes prepare has a
193
+ * read posture now — this is what its delegation subtree is clamped by even where no faces mount. */
194
+ effectiveReadFace?: ReadFace;
195
+ /** #240 — the normalized deny ADDITIONS in force (deployment ∪ task ∪ checkpoint seed; built-ins
196
+ * excluded), echoed on `TaskResult.effectiveReadDenyPatterns`. Present iff non-empty; a defensive
197
+ * copy (the wide-scope working array stays the engine's own). */
198
+ effectiveReadDenyPatterns?: readonly import("../../tools/fs/read-deny.js").NormalizedReadDenyEntry[];
196
199
  /** design/99 §E13 — the per-task logical cwd ref when a real shell is mounted (else undefined). The Runner
197
200
  * reads `cwdRef.current` after each tool to detect a `cd` move and emit `workspace_changed`. */
198
201
  cwdRef?: CwdRef;
@@ -999,37 +1002,6 @@ export interface InheritedGate {
999
1002
  * MONOTONICALLY (design/76 §2.2#1 r4 MAJOR-A). Mirrors how `nestedStats`/`resume.seed` thread trusted
1000
1003
  * run-scoped internals through the Runner without touching `TaskSpec`.
1001
1004
  */
1002
- /**
1003
- * R2 双形轴(clay 追加令 2026-07-18): CC 2.1.212's fable-variant prompt gate (b9e —
1004
- * `fable_5_mitigations` capability / claude-mythos-5), ORTHOGONAL to the simple/classic profile.
1005
- * sema is BYOM, so the id may carry provider prefixes ("anthropic/claude-fable-5",
1006
- * "openrouter/anthropic/claude-fable-5"): BOUNDARY-AWARE family match on the last path segment
1007
- * (codex 统一复审 F3 — raw substring classified "vendor/not-claude-fable-5" and "claude-mythos-50"
1008
- * as fable), case-normalized. Recognition set = CC's _Nr (startsWith "claude-fable-") + b9e
1009
- * (mythos-5). R3 system sections fork on the resulting fact.
1010
- */
1011
- export declare function isFableFamilyModelId(id: string): boolean;
1012
- /**
1013
- * RB-50 (CC 2.1.220 启示①, clay 2026-07-25): the SINGLE decision point for the two prompt-shape axes.
1014
- * Both were resolved in separate places with different mechanisms — `promptProfile` off a TaskSpec field,
1015
- * `fableMitigations` off a raw model-id prefix test — so "which shape does this task speak" had no one
1016
- * place to read. CC 2.1.220's counterpart is a model-registry `capabilities` array (one table drives
1017
- * `lean_prompt` + `fable_5_mitigations` alike; anchors/2.1.220/CC-218-220-DIFF.md §2).
1018
- *
1019
- * sema stays BYOM: we cannot key off a capability table for arbitrary model ids, so the RESOLUTION RULES
1020
- * are unchanged — profile: spec > inherited internals > "simple"; mitigations: model family. This is a
1021
- * consolidation, not a behavior change (the axes stay ORTHOGONAL: neither rewrites the other).
1022
- */
1023
- export declare function resolveModelPromptTraits(model: {
1024
- id: string;
1025
- }, spec: {
1026
- promptProfile?: "simple" | "classic";
1027
- }, internals?: {
1028
- promptProfile?: "simple" | "classic";
1029
- }): {
1030
- promptProfile: "simple" | "classic";
1031
- fableMitigations: boolean;
1032
- };
1033
1005
  export interface RunInternals {
1034
1006
  /** The live repair bundle from a `runRepairLoop` attempt in flight (attemptCount>0). Serialized onto a
1035
1007
  * checkpoint minted MID-attempt so a resume re-seeds it; undefined for any non-repair run. */
@@ -1408,9 +1380,11 @@ export interface RunInternals {
1408
1380
  * end with structural data (name/phase/ids) — NEVER args/output (those carry untrusted/host data). NEVER a
1409
1381
  * {@link TaskSpec} field. Absent ⇒ no activity capture (default).
1410
1382
  *
1411
- * **v1 scope (audit MINOR)**: reaches activity on a FRESH run only — the durable-resume entry (`resumeStream`)
1412
- * does not thread `internals`, so a resumed leg emits no activity. The workflow display is unaffected (its
1413
- * agents fail-on-suspend rather than durably resume).
1383
+ * Reaches activity on FRESH and RESUMED runs alike — the durable-resume entry (`resumeStream`) threads
1384
+ * `internals` too (see its parent-constraint re-supply snapshot), so a resumed leg's SUBSEQUENT tool calls
1385
+ * hit this sink. One real boundary remains (#249): the resume's already-approved pending call itself is
1386
+ * executed by `applyResumeDecision`'s own callback, outside the frame-minting harness, so THAT one call
1387
+ * emits no activity.
1414
1388
  */
1415
1389
  onActivity?: (activity: ToolActivity) => void;
1416
1390
  /**
@@ -1429,8 +1403,8 @@ export interface RunInternals {
1429
1403
  * after the durable-resume restore may have re-rooted it. Observe-only: a throwing sink is swallowed (an
1430
1404
  * observation must never fault a prepare that already minted a workspace).
1431
1405
  *
1432
- * **v1 scope**: same as {@link onActivity} a FRESH run only; the durable-resume entry (`resumeStream`)
1433
- * threads no `internals`, so a resumed leg reports nothing.
1406
+ * Like {@link onActivity}, this seat rides `internals` on FRESH and RESUMED runs alike (`resumeStream`
1407
+ * threads it too) a resumed leg's restore re-fires the observation with the settled root.
1434
1408
  */
1435
1409
  onWorkspaceResolved?: (workspace: ResolvedWorkspace) => void;
1436
1410
  }
@@ -1464,32 +1438,6 @@ export declare function batchContextAt(messages: AgentMessage[], currentId: stri
1464
1438
  batchToolCallIds: string[];
1465
1439
  completedCallIds: string[];
1466
1440
  };
1467
- /**
1468
- * codex 1360 r2-r4 — rebase one checkpointed absolute path from the OLD workspace root onto the RESTORED
1469
- * one (divergent `resumeVM`). POSIX-ONLY by contract: every remote lane's `mountPath` is a container
1470
- * path (e2b/k8s/ssh/adb/local-docker are all Linux targets), so a backslash ANYWHERE in the inputs marks
1471
- * the value outside this function's domain and it returns `p` UNCHANGED — an un-rebased path is honestly
1472
- * observable (the divergence observation already fired) while a WRONGLY-rebased one silently corrupts
1473
- * cwd/read-state (r4: drive-root and cross-family recomposition are not implementable without a Windows
1474
- * path model no lane needs). Trailing slashes are tolerated (from="/app/" must not weld the suffix);
1475
- * bare "/" keeps filesystem-root semantics; a path outside `from` (incl. the prefix-sibling
1476
- * "/application" vs "/app") returns unchanged. Exported for direct unit pinning.
1477
- */
1478
- export declare function rebaseWorkspacePath(p: string, fromRaw: string, toRaw: string): string;
1479
- /**
1480
- * RB-439-c — {@link rebaseWorkspacePath} over a SET of accepted spellings of the old root: the first
1481
- * prefix that actually matches wins, and a path under none of them is returned unchanged.
1482
- *
1483
- * The set exists because "the old root" has no single spelling. `WorkspaceHandle.mountPath` is whatever the
1484
- * adapter called the mount, while the persisted paths being migrated were spelled by whoever produced them
1485
- * (a `cd` the env canonicalized, a Read key resolved through realpath). On a target where the root has an
1486
- * equivalent alias (`/var` ↔ `/private/var`) those disagree while naming the same directory, and a
1487
- * single-prefix rebase then matched nothing and silently left the resumed shell in the pre-suspend tree —
1488
- * under a disclosure that said the task follows the restored root. Order is caller-chosen (the checkpointed
1489
- * spelling first, its canonical form second) and only matters if one prefix is a prefix of another, in
1490
- * which case the earlier — more specific — spelling is the intended one. Exported for direct unit pinning.
1491
- */
1492
- export declare function rebaseWorkspacePathAcross(p: string, froms: readonly string[], to: string): string;
1493
1441
  /**
1494
1442
  * Build everything a task run needs (council design/34 ②: a free function with EXPLICIT deps, not a
1495
1443
  * Runner method — testable and decoupled). Resolves the model/role/thinking, acquires + reconciles the