@sema-agent/core 5.31.0 → 5.32.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.32.0 — 2026-08-14
4
+
5
+ No BREAKING changes. One additive disclosure surface; zero resolution-result changes (the read-face
6
+ clamp VERDICT is unchanged since 5.30.0 — only its occurrence is now announced).
7
+
8
+ ### Added
9
+
10
+ - `config.read_face_deployment_clamped` EngineNotice family (#237): a deployment-wide
11
+ `readFace: "open"` beside a read-only (verifier) mount clamps to `"roots"` (stricter-wins);
12
+ the clamp's occurrence is now announced instead of silent. The trigger fires only when the
13
+ deployment seat was the deciding one — an explicit task-level `"roots"` reaches the same value
14
+ with or without the mount (no false-cause claim), and the task-seat `"open"` contradiction still
15
+ refuses loudly. Delivery: the prepare station announces once per `onNotice` SINK (the listener,
16
+ not the deps object — the Runner rebuilds its deps and `runTask` builds a Runner per call, so a
17
+ deps-keyed ledger would repeat per task; unwired console arm latches once per process); a
18
+ library-direct `createHandsToolkit({ readFace: "open", readOnly: true })` announces at toolkit
19
+ creation through its own `HandsToolkitOptions.onNotice` seat (absent ⇒ the same line via
20
+ `console.warn`). `detail: { seat, declared, inForce, cause }`. New export:
21
+ `deploymentReadFaceClampNotice` (the one shared notice builder).
22
+
23
+ ### Fixed
24
+
25
+ - `HandsToolkitOptions.readFace` JSDoc (ships in the `.d.ts`) stopped promising a loud refusal
26
+ beside `readOnly: true` — stale since the 96ef89d seat distinction (5.30.0): the deps-shaped
27
+ seat clamps and announces; the loud refusal belongs to `TaskSpec.readFace`. Wording only, two
28
+ sites; the runtime behavior was already as now documented.
29
+
30
+ ### Docs
31
+
32
+ - The recursive-verb table (`RECURSIVE_READ_FORMS`) declares its KNOWN OPEN SET with the standing
33
+ sweep obligation, and `StripSurvey` states consumer guidance for truncated samples (the two
34
+ 5.31.0 review deductions; wording only).
35
+
3
36
  ## 5.31.0 — 2026-08-14
4
37
 
5
38
  No API-BREAKING changes. Behavior narrowings are all tighten-direction (auto-allow → ask, or
@@ -30,6 +30,8 @@ import type { NestedUsageAccum, RunnerDeps, TaskEvent, TaskLimits, TaskResult, T
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
+ /** Test seam (mirrors `__resetMalformedNoticeSeatAnnouncement`): never called by production code. */
34
+ export declare function __resetReadFaceClampAnnouncement(): void;
33
35
  /**
34
36
  * design/164 — validate `TaskSpec.limits` at the door and return it unchanged.
35
37
  *
@@ -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, assertReadFaceValue, bashReversibilityProbe, compileReadDeny, createHandsToolkit, isReadDedupStubResult, resolveReadFace, seedReadFileStateFromContext, seedReadFileStateFromTranscript, FULL_SHELL_CONTRACT_ID, HAND_TOOL_EFFECTS, pdfModelCapabilitiesOf } from "../../tools/fs/index.js";
77
+ import { applyCompactionToReadFileState, assertReadFaceValue, bashReversibilityProbe, compileReadDeny, createHandsToolkit, deploymentReadFaceClampNotice, 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";
@@ -97,6 +97,11 @@ export function __resetMaterializeEnvAnnouncements() {
97
97
  function emitMaterializeEnvNotice(onNotice, message, detail) {
98
98
  deliverEngineNotice(onNotice, { code: "config.materialize_env_discarded", message, detail });
99
99
  }
100
+ const readFaceClampAnnouncedSinks = new WeakSet();
101
+ let readFaceClampConsoleAnnounced = false;
102
+ export function __resetReadFaceClampAnnouncement() {
103
+ readFaceClampConsoleAnnounced = false;
104
+ }
100
105
  const RECONCILE_MAX_RETRIES = 3;
101
106
  const DEFAULT_MAX_SUSPENDS = 5;
102
107
  const TASK_LIMIT_KEY_DICT = {
@@ -1692,6 +1697,20 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1692
1697
  readOnlyMount: handsReadOnly,
1693
1698
  orgGoverned: deps.permissionRuleOrg !== undefined,
1694
1699
  fullShellReachable,
1700
+ onDeploymentClamp: () => {
1701
+ const sink = deps.onNotice;
1702
+ if (typeof sink === "function") {
1703
+ if (readFaceClampAnnouncedSinks.has(sink))
1704
+ return;
1705
+ readFaceClampAnnouncedSinks.add(sink);
1706
+ }
1707
+ else {
1708
+ if (readFaceClampConsoleAnnounced)
1709
+ return;
1710
+ readFaceClampConsoleAnnounced = true;
1711
+ }
1712
+ deliverEngineNotice(sink, deploymentReadFaceClampNotice());
1713
+ },
1695
1714
  });
1696
1715
  if (resume !== undefined && (seedReadFaceSection === undefined || seedReadFaceSection.face === "roots"))
1697
1716
  liveReadFace = "roots";
@@ -4230,7 +4230,16 @@ export interface EngineNotice {
4230
4230
  * baseline (the strip is not a refusal in this window), which is why the drop is announced;
4231
4231
  * `detail: { total, stripped: [{ key, reason }], omitted? }`, the rendered key list bounded in count
4232
4232
  * and length because the names come from the untrusted script. One aggregated notice per governed
4233
- * child build, not de-duplicated across builds: each spec is a distinct fact. */
4233
+ * child build, not de-duplicated across builds: each spec is a distinct fact.
4234
+ * - `"config.read_face_deployment_clamped"` (#237) — a deployment-wide `readFace: "open"` is not
4235
+ * in force beside a read-only (verifier) mount: it clamps to "roots" without throwing
4236
+ * (stricter-wins; the clamp verdict stands, only its occurrence was undisclosed). Announced
4237
+ * once per `onNotice` SINK at the prepare station (the verifier lane would otherwise repeat it
4238
+ * every spawn; the sink — not the deps object — is the unit because the Runner rebuilds its
4239
+ * deps and `runTask` builds a Runner per call), unwired console arm once per process;
4240
+ * a library-direct `createHandsToolkit` mount announces at toolkit creation (one per mount,
4241
+ * through the band-local `HandsToolkitOptions.onNotice` seat, absent ⇒ `console.warn`);
4242
+ * `detail: { seat, declared, inForce, cause }`. */
4234
4243
  code: string;
4235
4244
  /** The exact human-readable line the unwired build prints via `console.warn` — same words, one text. */
4236
4245
  message: string;
package/dist/index.d.ts CHANGED
@@ -88,7 +88,7 @@ export { HAND_TOOL_EFFECTS, bashReversibilityProbe, BASH_READONLY_DEFAULT_ALLOW,
88
88
  export { classifyCompoundReadonlyDetailed, formatOutOfRootReadApprovalOption, type BashReadonlyRootBoundary, type CompoundReadonlyVerdict, } from "./tools/fs/index.js";
89
89
  export { resolveBashTimeoutCaps } from "./tools/fs/index.js";
90
90
  export { READ_FACE_DEFAULT_DENY_ENTRIES, compileReadDeny, type ReadDenyEntry, type ReadDenyMatcher, type NormalizedReadDenyEntry, } from "./tools/fs/index.js";
91
- export { resolveReadFace, type ReadFace, type ReadFaceInputs } from "./tools/fs/index.js";
91
+ export { deploymentReadFaceClampNotice, resolveReadFace, type ReadFace, type ReadFaceInputs } from "./tools/fs/index.js";
92
92
  export { InMemoryToolResultStore, OFFLOAD_TOOL_NAME, DEFAULT_TOOL_RESULT_THRESHOLD_CHARS, assertSafeToolResultRef, buildToolResultRef, toolResultContentSegment, MAX_MINTED_TOOL_RESULT_REF_CHARS, type ToolResultProvenance, assertToolResultProvenanceMatch, normalizeToolResultProvenance, toolResultProvenanceOf, ToolResultRefConflictError, TOOL_RESULT_REF_CONFLICT_CODE, type ToolResultStore, type ToolResultSlice, type ToolResultDeletionReport, } from "./core/tool-result-store.js";
93
93
  export { InMemoryCheckpointStore, CheckpointError, mintCheckpointToken, checkpointVersionOf, CURRENT_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, ORG_ADMISSION_CHECKPOINT_VERSION, F012_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, debitLedger, remainingBudgetMicroUsd, remainingTokens, winnerFromOutcome, validatePendingSteer, readPendingSteerQueue, appendPendingSteer, MAX_PENDING_STEER_CHARS, MAX_PENDING_STEER_ENTRIES, PENDING_STEER_QUEUE_BYTE_BUDGET_BYTES, PENDING_STEER_FROZEN_FIELDS, ACTOR_ASSERTION_FROZEN_FIELDS, MAX_ACTOR_FIELD_CHARS, MAX_STEER_INPUT_ID_CHARS, LEGACY_PENDING_STEER_INPUT_ID, type ActorAssertion, type PendingSteerEntry, type PendingSteerInput, riskSeverity, buildRiskDescriptor, summarizeCheckpoint, type RiskDescriptor, type CheckpointStore, type CheckpointSummary, type Checkpoint, type CheckpointToken, type CheckpointGate, type CheckpointState, type SerializedCheckpointState, type CheckpointFaultMode, type PendingAction, type ResumeOutcome, type ResolvedOutcome, type ReopenReason, type ResolveExpectation, type SafetyAxis, type RealApprovalGateBit, type ResourceLedger, type ResourceLimitReason, type PlatformLimitReason, } from "./core/checkpoint-store.js";
94
94
  export { InMemoryUsageWindowStore, GLOBAL_USAGE_KEY, EMPTY_USAGE_WINDOW_RECORD, chargeUsageRecord, readUsageRecord, usageRetryAfterMs, resolveUsageWindows, type UsageWindow, type UsageWindowStore, type UsageWindowReading, type UsageWindowRecord, type UsageSlot, type UsageBucketRow, } from "./core/usage-window-store.js";
package/dist/index.js CHANGED
@@ -69,7 +69,7 @@ export { HAND_TOOL_EFFECTS, bashReversibilityProbe, BASH_READONLY_DEFAULT_ALLOW,
69
69
  export { classifyCompoundReadonlyDetailed, formatOutOfRootReadApprovalOption, } from "./tools/fs/index.js";
70
70
  export { resolveBashTimeoutCaps } from "./tools/fs/index.js";
71
71
  export { READ_FACE_DEFAULT_DENY_ENTRIES, compileReadDeny, } from "./tools/fs/index.js";
72
- export { resolveReadFace } from "./tools/fs/index.js";
72
+ export { deploymentReadFaceClampNotice, resolveReadFace } from "./tools/fs/index.js";
73
73
  export { InMemoryToolResultStore, OFFLOAD_TOOL_NAME, DEFAULT_TOOL_RESULT_THRESHOLD_CHARS, assertSafeToolResultRef, buildToolResultRef, toolResultContentSegment, MAX_MINTED_TOOL_RESULT_REF_CHARS, assertToolResultProvenanceMatch, normalizeToolResultProvenance, toolResultProvenanceOf, ToolResultRefConflictError, TOOL_RESULT_REF_CONFLICT_CODE, } from "./core/tool-result-store.js";
74
74
  export { InMemoryCheckpointStore, CheckpointError, mintCheckpointToken, checkpointVersionOf, CURRENT_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, ORG_ADMISSION_CHECKPOINT_VERSION, F012_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, debitLedger, remainingBudgetMicroUsd, remainingTokens, winnerFromOutcome, validatePendingSteer, readPendingSteerQueue, appendPendingSteer, MAX_PENDING_STEER_CHARS, MAX_PENDING_STEER_ENTRIES, PENDING_STEER_QUEUE_BYTE_BUDGET_BYTES, PENDING_STEER_FROZEN_FIELDS, ACTOR_ASSERTION_FROZEN_FIELDS, MAX_ACTOR_FIELD_CHARS, MAX_STEER_INPUT_ID_CHARS, LEGACY_PENDING_STEER_INPUT_ID, riskSeverity, buildRiskDescriptor, summarizeCheckpoint, } from "./core/checkpoint-store.js";
75
75
  export { InMemoryUsageWindowStore, GLOBAL_USAGE_KEY, EMPTY_USAGE_WINDOW_RECORD, chargeUsageRecord, readUsageRecord, usageRetryAfterMs, resolveUsageWindows, } from "./core/usage-window-store.js";
@@ -1,5 +1,5 @@
1
1
  import type { AgentTool, ExecutionEnv } from "../../internal/harness-types.js";
2
- import type { BeforeWriteHook } from "../../core/types.js";
2
+ import { type BeforeWriteHook } from "../../core/types.js";
3
3
  import { type TaskRegistry } from "../../core/task-registry.js";
4
4
  import { type ReadFileState } from "./safety.js";
5
5
  import type { PdfModelCapabilities } from "./pdf.js";
@@ -130,8 +130,12 @@ export interface HandsToolkitOptions {
130
130
  * {@link import("./read-face.js").resolveReadFace} order prepare-task uses). Absent ⇒ the
131
131
  * resolution order's default = "roots" (D-1b: the engine never opens implicitly). "open" skips
132
132
  * ONLY the roots containment judgment — the deny set, the UNC out-of-set refusal and the
133
- * special-file type gates run in both faces (§2.0). Refused loudly beside `readOnly: true`
134
- * (the verifier mount's containment is load-bearing). Never affects the write faces. */
133
+ * special-file type gates run in both faces (§2.0). Beside `readOnly: true` the mount wins:
134
+ * this seat is deps-shaped (a deployment default, not a per-call assertion), so its "open"
135
+ * CLAMPS to "roots" and the clamp is ANNOUNCED (`config.read_face_deployment_clamped`, through
136
+ * `onNotice` / `console.warn`) — the loud refusal belongs to the TASK seat (`TaskSpec.readFace`),
137
+ * which is the one genuine per-call contradiction (#237; stale "refused loudly" wording here
138
+ * predated the 96ef89d seat distinction). Never affects the write faces. */
135
139
  readFace?: "open" | "roots";
136
140
  }
137
141
  /**
@@ -1,3 +1,4 @@
1
+ import { deliverEngineNotice } from "../../core/types.js";
1
2
  import { createTaskOutputTool, createTaskStopTool } from "../../core/task-registry.js";
2
3
  import { hasBackgroundShell } from "../../core/background-shell.js";
3
4
  import {} from "./safety.js";
@@ -12,7 +13,7 @@ export * from "./fs-bash.js";
12
13
  export * from "./read-deny.js";
13
14
  import { BASH_READONLY_DEFAULT_ALLOW, } from "./bash-readonly-classifier.js";
14
15
  import { compileReadDeny } from "./read-deny.js";
15
- import { resolveReadFace } from "./read-face.js";
16
+ import { deploymentReadFaceClampNotice, resolveReadFace } from "./read-face.js";
16
17
  export * from "./read-face.js";
17
18
  import {} from "./fs-shared.js";
18
19
  import { createReadFileTool } from "./fs-read.js";
@@ -39,6 +40,7 @@ export function createHandsToolkit(env, readFileState, rootCanonical, opts = {})
39
40
  readOnlyMount: readOnly,
40
41
  orgGoverned: false,
41
42
  fullShellReachable: includeShell && !readOnly,
43
+ onDeploymentClamp: () => deliverEngineNotice(opts.onNotice, deploymentReadFaceClampNotice()),
42
44
  });
43
45
  const tools = [
44
46
  createReadFileTool(env, readFileState, rootCanonical, readOnly ? undefined : cwdRef, readFaceRoots, opts.readImageDownsampler, opts.pdfModelCapabilities, bgOutputReadExemption, opts.readCyberReminder, readDeny, readFace),
@@ -29,6 +29,13 @@ export interface ReadFaceInputs {
29
29
  * "open"… no — see the resolution order: an EXPLICIT open still wins (row 4 is a default, not a
30
30
  * clamp); what it changes is that NOTHING implicit opens a bash-less mount. */
31
31
  fullShellReachable: boolean;
32
+ /** #237 — observation hook for the ONE silent branch of the order: row 1's deployment-seat clamp
33
+ * (readOnly mount + deps "open" + NO task seat). Fired exactly when the deps declaration was the
34
+ * deciding seat and the mount overrode it — not when a task-level "roots" pinned the same value
35
+ * anyway (the mount changed nothing there, and announcing it would claim a false cause), and
36
+ * never on the throw rows (those are already loud). The resolver stays the single owner of the
37
+ * clamp predicate; stations own delivery/dedup ({@link deploymentReadFaceClampNotice}). */
38
+ onDeploymentClamp?: () => void;
32
39
  }
33
40
  /**
34
41
  * The ONE resolution order (§2.2), first hit wins. prepare-task AND createHandsToolkit both call
@@ -47,3 +54,17 @@ export interface ReadFaceInputs {
47
54
  * 5. default: ROOTS (D-1b — the engine never opens implicitly).
48
55
  */
49
56
  export declare function resolveReadFace(i: ReadFaceInputs): ReadFace;
57
+ /**
58
+ * #237 — the ONE text of the deployment-clamp announcement (`config.read_face_deployment_clamped`).
59
+ * Both stations deliver THIS object through their own `deliverEngineNotice` call (prepare-task with
60
+ * the `RunnerDeps.onNotice` seat, once per notice sink — the sink, not the deps object, because the
61
+ * Runner rebuilds its deps; `createHandsToolkit` with its band-local `HandsToolkitOptions.onNotice`
62
+ * seat — toolkit creation is itself the one-per-mount boot moment, and an absent seat prints the
63
+ * same line via `console.warn`). Built here, import-free, so the message can never fork between
64
+ * the stations.
65
+ */
66
+ export declare function deploymentReadFaceClampNotice(): {
67
+ code: string;
68
+ message: string;
69
+ detail: Record<string, unknown>;
70
+ };
@@ -16,6 +16,8 @@ export function resolveReadFace(i) {
16
16
  e.code = "config.read_face_readonly_conflict";
17
17
  throw e;
18
18
  }
19
+ if (deps === "open" && spec === undefined)
20
+ i.onDeploymentClamp?.();
19
21
  return "roots";
20
22
  }
21
23
  if (i.orgGoverned) {
@@ -36,3 +38,10 @@ export function resolveReadFace(i) {
36
38
  return "roots";
37
39
  return "roots";
38
40
  }
41
+ export function deploymentReadFaceClampNotice() {
42
+ return {
43
+ code: "config.read_face_deployment_clamped",
44
+ message: `readFace (deployment seat): the deployment-wide "open" declaration is not in force on this read-only (verifier) mount — clamped to "roots" (stricter-wins: the mount's containment is load-bearing and never openable). Other mounts still honor "open".`,
45
+ detail: { seat: "readFace (deployment seat)", declared: "open", inForce: "roots", cause: "read_only_mount" },
46
+ };
47
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "5.31.0",
3
+ "version": "5.32.0",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",