@sema-agent/core 7.11.0 → 7.11.1

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.
@@ -0,0 +1,185 @@
1
+ import { uuidv7 } from "../../internal/harness.js";
2
+ import { snapshotActorAssertion } from "../../internal/llm.js";
3
+ import { LEGACY_PENDING_STEER_INPUT_ID, MAX_STEER_INPUT_ID_CHARS } from "../checkpoint-store.js";
4
+ import { formatHookFeedback, hookSeatExpiredError, runHookSeat } from "../hooks.js";
5
+ import { buildHumanInputEvent, frameMidTurnUserInput, projectHumanInput } from "../human-input-projection.js";
6
+ import { isSystemInjectionPriority, SYSTEM_INJECTION_PRIORITIES } from "../task-notification.js";
7
+ import { deliverEngineNotice } from "../types.js";
8
+ import { inlineUntrusted, sanitizeUntrustedText } from "../untrusted-text.js";
9
+ import { nextHumanInputSeq, sameAcceptedSteerInput } from "./steer-admission.js";
10
+ export function streamSteerVerb(input) {
11
+ const { spec, internals, queue, acceptedSteerInputs, live, ready, orTimeout, readyTimeoutMs: READY_TIMEOUT_MS, steeringError, runner } = input;
12
+ let steerChain = Promise.resolve();
13
+ const steer = async (text, options) => {
14
+ const trusted = options?.trusted ? true : false;
15
+ if (trusted && sanitizeUntrustedText(text) !== text) {
16
+ throw steeringError("trusted steering text must not contain a </system-reminder> tag", "steering.invalid_content");
17
+ }
18
+ const inputId = options?.inputId;
19
+ if (inputId !== undefined) {
20
+ if (typeof inputId !== "string") {
21
+ throw steeringError("inputId must be a string when supplied", "steering.invalid_content");
22
+ }
23
+ if (inputId === "" || inputId.length > MAX_STEER_INPUT_ID_CHARS) {
24
+ throw steeringError(`inputId must be a non-empty string of at most ${MAX_STEER_INPUT_ID_CHARS} characters`, "steering.invalid_content");
25
+ }
26
+ if (inputId === LEGACY_PENDING_STEER_INPUT_ID) {
27
+ throw steeringError(`inputId "${LEGACY_PENDING_STEER_INPUT_ID}" is reserved for a pre-queue parked steer and cannot be supplied by a caller`, "steering.invalid_content");
28
+ }
29
+ }
30
+ const priorityIn = options?.priority;
31
+ if (priorityIn !== undefined && !isSystemInjectionPriority(priorityIn)) {
32
+ throw steeringError(`priority must be one of ${SYSTEM_INJECTION_PRIORITIES.join("/")} when supplied`, "steering.invalid_content");
33
+ }
34
+ const priority = priorityIn ?? "next";
35
+ const actorIn = options?.actor;
36
+ const actor = actorIn === undefined ? undefined : snapshotActorAssertion(actorIn);
37
+ const projected = projectHumanInput({ text, actor, source: "steer" });
38
+ const effectiveInputId = typeof inputId === "string" ? inputId : uuidv7();
39
+ const parkRecord = {
40
+ text,
41
+ trusted,
42
+ inputId: effectiveInputId,
43
+ ...(priorityIn !== undefined ? { priority } : {}),
44
+ ...(actor !== undefined ? { actor } : {}),
45
+ };
46
+ let payload;
47
+ let mintsAFrame;
48
+ let replay;
49
+ const noteAccepted = (h) => {
50
+ if (!mintsAFrame)
51
+ return;
52
+ if (typeof inputId === "string")
53
+ acceptedSteerInputs.set(inputId, replay);
54
+ queue.push({
55
+ ...buildHumanInputEvent({
56
+ carrier: "steer",
57
+ source: "steer",
58
+ delivery: "queued",
59
+ sessionSeq: nextHumanInputSeq(h.harness),
60
+ inputId: effectiveInputId,
61
+ ...(actor !== undefined ? { actor } : {}),
62
+ ...(actor?.issuer !== undefined ? { issuer: actor.issuer } : {}),
63
+ ...(spec.principal !== undefined ? { principal: spec.principal } : {}),
64
+ }),
65
+ eventId: uuidv7(),
66
+ ...(internals?.parentToolCallId !== undefined
67
+ ? { parentToolCallId: internals.parentToolCallId, ...(spec.taskId !== undefined ? { sourceTaskId: spec.taskId } : {}) }
68
+ : {}),
69
+ });
70
+ };
71
+ const deliver = async () => {
72
+ if (live.resultValue)
73
+ throw steeringError("the task has already finished");
74
+ const h = live.handle ?? (await orTimeout(ready));
75
+ if (!h)
76
+ throw steeringError("the task is not running");
77
+ payload = trusted ? formatHookFeedback(projected, h.reminderMark) : frameMidTurnUserInput(projected);
78
+ mintsAFrame = payload.trim().length !== 0;
79
+ replay = { payload, trusted, priority, ...(actor !== undefined ? { actor } : {}) };
80
+ if (typeof inputId === "string") {
81
+ const prior = acceptedSteerInputs.get(inputId);
82
+ if (prior !== undefined) {
83
+ if (live.resultValue !== undefined || h.loop.ended)
84
+ throw steeringError("the task is no longer running");
85
+ if (!sameAcceptedSteerInput(prior, replay)) {
86
+ throw steeringError("a different steering instruction was already accepted under this inputId — re-issue this one with a fresh inputId " +
87
+ "(an identical payload would have been an idempotent retry)", "steering.duplicate_input_id");
88
+ }
89
+ return;
90
+ }
91
+ }
92
+ if (live.resultValue !== undefined || h.loop.ended)
93
+ throw steeringError("the task is no longer running");
94
+ if (mintsAFrame) {
95
+ const screen = (spec.hooks ?? runner.deps.hooks)?.userPromptSubmit;
96
+ if (screen !== undefined) {
97
+ let decision;
98
+ try {
99
+ const seat = await runHookSeat("userPromptSubmit", { timeoutMs: h.hookTimeoutMs, signal: h.abortController.signal, abortEnds: true }, (sig) => screen(text, { identity: h.hookIdentity, signal: sig, source: "steer", inputId: effectiveInputId, ...(actor !== undefined ? { actor: snapshotActorAssertion(actor) } : {}) }));
100
+ if (seat.expired) {
101
+ if (seat.cause === "timeout") {
102
+ try {
103
+ runner.deps.onError?.(hookSeatExpiredError("userPromptSubmit", h.hookTimeoutMs, seat.cause, "the steering input was NOT accepted (fail-closed) and the caller was refused typed"), { phase: "hook", sessionId: h.sessionId });
104
+ }
105
+ catch {
106
+ }
107
+ }
108
+ throw steeringError(seat.cause === "timeout"
109
+ ? `the deployment's userPromptSubmit hook did not answer within its ${h.hookTimeoutMs}ms bound while screening this steering input; the input was NOT accepted (fail-closed)`
110
+ : `the task was cancelled while the deployment's userPromptSubmit hook was still screening this steering input; the input was NOT accepted (fail-closed)`, "steering.blocked_by_hook");
111
+ }
112
+ decision = seat.value;
113
+ }
114
+ catch (hookErr) {
115
+ if (hookErr instanceof Error && hookErr.code === "steering.blocked_by_hook")
116
+ throw hookErr;
117
+ const err = hookErr instanceof Error ? hookErr : new Error(String(hookErr));
118
+ try {
119
+ runner.deps.onError?.(err, { phase: "hook", sessionId: h.sessionId });
120
+ }
121
+ catch {
122
+ }
123
+ throw steeringError(`the deployment's userPromptSubmit hook crashed while screening this steering input (${inlineUntrusted(err.message)}); the input was NOT accepted (fail-closed)`, "steering.blocked_by_hook");
124
+ }
125
+ if (decision?.block !== undefined && decision.block !== "") {
126
+ throw steeringError(`the deployment's userPromptSubmit hook blocked this steering input: ${inlineUntrusted(decision.block)}`, "steering.blocked_by_hook");
127
+ }
128
+ if (decision?.additionalContext !== undefined && decision.additionalContext !== "") {
129
+ payload = `${formatHookFeedback(decision.additionalContext, h.reminderMark)}\n\n${payload}`;
130
+ }
131
+ }
132
+ }
133
+ const injectFramed = async () => {
134
+ const noteOptions = { provenance: "engine-note", callerAuthored: true, parkRecord, ...(actor !== undefined ? { actor } : {}) };
135
+ if (priority === "later") {
136
+ await h.harness.followUp(payload, noteOptions);
137
+ noteAccepted(h);
138
+ return;
139
+ }
140
+ const frame = await h.harness.steer(payload, { ...noteOptions, ...(priority === "now" ? { immediate: true } : {}) });
141
+ noteAccepted(h);
142
+ if (priority === "now" && frame !== undefined && h.harness.interruptTurn(frame)) {
143
+ deliverEngineNotice(runner.deps.onNotice, {
144
+ code: "task.turn_interrupted",
145
+ message: "a caller-provenance steer with priority \"now\" interrupted the running turn: in-flight work was cut at " +
146
+ "a manufactured boundary (finished tool calls keep their real results; never-started ones settle as " +
147
+ "interrupted) and the run continues with the steer at the queue head.",
148
+ detail: {
149
+ inputId: effectiveInputId,
150
+ sessionId: h.sessionId,
151
+ runId: h.runId,
152
+ ...(actor?.id !== undefined ? { actorId: actor.id } : {}),
153
+ ...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}),
154
+ },
155
+ });
156
+ }
157
+ };
158
+ try {
159
+ await injectFramed();
160
+ return;
161
+ }
162
+ catch (e) {
163
+ if (!(e instanceof Error && e.code === "invalid_state"))
164
+ throw e;
165
+ }
166
+ const birthDeadline = Date.now() + READY_TIMEOUT_MS;
167
+ while (live.resultValue === undefined && !h.loop.ended && Date.now() < birthDeadline) {
168
+ try {
169
+ await injectFramed();
170
+ return;
171
+ }
172
+ catch (e2) {
173
+ if (!(e2 instanceof Error && e2.code === "invalid_state"))
174
+ throw e2;
175
+ }
176
+ await new Promise((r) => setTimeout(r, 10));
177
+ }
178
+ throw steeringError("the task is no longer running");
179
+ };
180
+ const p = steerChain.then(deliver);
181
+ steerChain = p.then(() => undefined, () => undefined);
182
+ return p;
183
+ };
184
+ return { steer };
185
+ }
@@ -256,6 +256,11 @@ export interface CompoundReadonlyVerdict {
256
256
  outOfRootRead?: true;
257
257
  /** The resolved out-of-root paths, de-duplicated, in first-seen order. Present iff `outOfRootRead`. */
258
258
  outOfRootPaths?: readonly string[];
259
+ /** B-057 — an operand matched the deployment's read deny judge (`BashReadonlyRootBoundary.denyMatch`):
260
+ * the demotion was raised by a DECLARED read boundary, not by the classifier's own caution. The probe
261
+ * reads it to mint `mandated` — a stored allow rule or the read-only shell arm may retire a classify-
262
+ * tier question, never one the read face itself asked. Structural sibling of `outOfRootRead`. */
263
+ readDenied?: true;
259
264
  /**
260
265
  * RB-451 — the paths this scan resolved INSIDE the roots (de-duplicated, first-seen order), i.e. the
261
266
  * candidates a caller holding a filesystem should re-check with symlinks resolved. Absent when there
@@ -867,6 +867,7 @@ function evaluateReadBoundary(foldedSegments, boundary) {
867
867
  if (denied != null) {
868
868
  return {
869
869
  reason: `a command operand resolves to "${finding.path}", which matches the sensitive-path read deny list (pattern "${denied}") ${NOT_AUTO_ALLOWED}`,
870
+ readDenied: true,
870
871
  };
871
872
  }
872
873
  if (finding.kind === "inside") {
@@ -31,6 +31,7 @@ export function bashReversibilityProbe(allow, boundary) {
31
31
  return { reversible: false };
32
32
  const resolved = typeof boundary === "function" ? boundary() : boundary;
33
33
  const outOfRootGate = () => resolved !== undefined && classifyOutOfRootReadGate(command, allowSet, resolved).gated ? { mandated: true } : {};
34
+ const boundaryGate = (verdict) => verdict.readDenied === true || (verdict.recursiveReadPaths !== undefined && verdict.recursiveReadPaths.length > 0) ? { mandated: true } : outOfRootGate();
34
35
  if (a?.run_in_background === true)
35
36
  return { reversible: false, ...outOfRootGate() };
36
37
  const detailed = classifyCompoundReadonlyDetailed(command, allowSet, resolved);
@@ -42,7 +43,7 @@ export function bashReversibilityProbe(allow, boundary) {
42
43
  const others = detailed.undecidedPaths.filter((p) => !recursiveSet.has(p));
43
44
  return {
44
45
  reversible: false,
45
- ...outOfRootGate(),
46
+ ...boundaryGate(detailed),
46
47
  cause: {
47
48
  code: RECURSIVE_READ_CAUSE_CODE,
48
49
  roots: operandFamily(recursive),
@@ -56,7 +57,7 @@ export function bashReversibilityProbe(allow, boundary) {
56
57
  }
57
58
  return classifyBoundedReadonlyPollLoop(command, allowSet, resolved) === undefined
58
59
  ? { reversible: true }
59
- : { reversible: false, ...outOfRootGate() };
60
+ : { reversible: false, ...boundaryGate(detailed) };
60
61
  };
61
62
  }
62
63
  export { FULL_SHELL_CONTRACT_ID } from "../../core/tool-catalog-entries.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "7.11.0",
3
+ "version": "7.11.1",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",