@sema-agent/core 7.10.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.
Files changed (66) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/dist/agents/child-model-seat.d.ts +45 -18
  3. package/dist/agents/child-model-seat.js +12 -8
  4. package/dist/agents/subagent.js +6 -5
  5. package/dist/agents/teacher.js +2 -2
  6. package/dist/core/auto-mode-defaults.d.ts +19 -3
  7. package/dist/core/auto-mode-defaults.js +1 -0
  8. package/dist/core/auto-mode.d.ts +24 -20
  9. package/dist/core/auto-mode.js +12 -12
  10. package/dist/core/gate-fold.js +1 -0
  11. package/dist/core/gate-lanes.d.ts +6 -1
  12. package/dist/core/gate-lanes.js +45 -18
  13. package/dist/core/governance-codes.d.ts +1 -0
  14. package/dist/core/governance-codes.js +1 -0
  15. package/dist/core/hooks.d.ts +13 -0
  16. package/dist/core/permission-rule-model.d.ts +5 -3
  17. package/dist/core/permission-rule-model.js +7 -3
  18. package/dist/core/persisted-rule-arms.js +4 -3
  19. package/dist/core/read-only-shell-table.d.ts +87 -0
  20. package/dist/core/read-only-shell-table.js +485 -0
  21. package/dist/core/read-only-shell.d.ts +42 -0
  22. package/dist/core/read-only-shell.js +316 -0
  23. package/dist/core/roles.d.ts +3 -2
  24. package/dist/core/runner/contracts.d.ts +70 -0
  25. package/dist/core/runner/gate-exit.d.ts +5 -0
  26. package/dist/core/runner/prepare-caps-and-workflow.js +38 -12
  27. package/dist/core/runner/prepare-gate-stations.js +9 -0
  28. package/dist/core/runner/prepare-task.js +1 -1
  29. package/dist/core/runner/prepare-turn-wiring.js +1 -1
  30. package/dist/core/runner/runtask.js +34 -510
  31. package/dist/core/runner/stream-halt-verbs.d.ts +38 -0
  32. package/dist/core/runner/stream-halt-verbs.js +82 -0
  33. package/dist/core/runner/stream-lifecycle-verbs.d.ts +34 -0
  34. package/dist/core/runner/stream-lifecycle-verbs.js +126 -0
  35. package/dist/core/runner/stream-reap.d.ts +30 -0
  36. package/dist/core/runner/stream-reap.js +40 -0
  37. package/dist/core/runner/stream-settle-backstop.d.ts +38 -0
  38. package/dist/core/runner/stream-settle-backstop.js +113 -0
  39. package/dist/core/runner/stream-steer-verb.d.ts +30 -0
  40. package/dist/core/runner/stream-steer-verb.js +185 -0
  41. package/dist/core/shell-lexer.d.ts +18 -0
  42. package/dist/core/shell-lexer.js +17 -10
  43. package/dist/core/shell-wrapper-table.js +8 -5
  44. package/dist/core/tool-policy.d.ts +4 -1
  45. package/dist/core/tool-policy.js +1 -1
  46. package/dist/core/tools.d.ts +28 -7
  47. package/dist/core/tools.js +44 -4
  48. package/dist/core/trace.d.ts +15 -0
  49. package/dist/engine/harness/agent-harness.d.ts +3 -1
  50. package/dist/engine/harness/agent-harness.js +1 -1
  51. package/dist/engine/harness/types.d.ts +4 -2
  52. package/dist/index.d.ts +3 -1
  53. package/dist/index.js +2 -0
  54. package/dist/orchestration/run-workflow-tool.d.ts +5 -2
  55. package/dist/orchestration/run-workflow-tool.js +2 -1
  56. package/dist/orchestration/workflow-governance.d.ts +3 -2
  57. package/dist/orchestration/workflow-primitives.d.ts +4 -1
  58. package/dist/orchestration/workflow-primitives.js +1 -6
  59. package/dist/orchestration/workflow.d.ts +12 -4
  60. package/dist/orchestration/workflow.js +24 -7
  61. package/dist/prompt-assembly/turn-snapshot.d.ts +4 -2
  62. package/dist/tools/fs/bash-readonly-classifier.d.ts +5 -0
  63. package/dist/tools/fs/bash-readonly-classifier.js +1 -0
  64. package/dist/tools/fs/fs-bash.js +3 -2
  65. package/package.json +1 -1
  66. package/test/export-surface.snapshot.json +35 -1
@@ -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
+ }
@@ -9,10 +9,20 @@ export interface ShellWord {
9
9
  * yield zero, one or several words. */
10
10
  readonly expands: false | "one" | "many";
11
11
  }
12
+ /** One redirection of a segment, as read: the operator and its operand word (a heredoc's operand is its
13
+ * delimiter; the IO number or `{name}` before the operator is folded into the operator's meaning and not
14
+ * kept). The tightening reader never reads these — a rule speaks about program runs; the read-only
15
+ * reader does (an output redirection is a write). */
16
+ export interface ShellRedirection {
17
+ readonly op: string;
18
+ readonly target: ShellWord;
19
+ }
12
20
  /** One program run the lexer found, after keyword stripping. */
13
21
  export interface ShellSegment {
14
22
  /** The run as SPELLED (a leading keyword removed): the first candidate a rule is compared against. */
15
23
  readonly argv: readonly ShellWord[];
24
+ /** The segment's redirections in source order (see {@link ShellRedirection}); empty when it has none. */
25
+ readonly redirections: readonly ShellRedirection[];
16
26
  /** The deeper candidates, one per peeled layer (leading assignments, then each wrapper of the table):
17
27
  * `sudo -u root rm -r x` ⇒ `[[rm, -r, x]]`. Empty when nothing peeled. */
18
28
  readonly peeled: readonly (readonly ShellWord[])[];
@@ -34,6 +44,14 @@ export interface ShellCommandShape {
34
44
  /** A subshell, group, control-structure keyword or dropped empty piece bounded the segments: the
35
45
  * connector list is not one flat chain a compound rule could spell. */
36
46
  readonly grouped: boolean;
47
+ /** A `&` connector anywhere — between two segments or after the last one (`ls &`): some segment runs in the
48
+ * background, past any approval-time reading of the line. The connector list alone cannot say so for a
49
+ * trailing `&` (a connector rides BETWEEN segments). */
50
+ readonly backgrounded: boolean;
51
+ /** A redirection on a piece that runs NO program (`> x` alone, `[ -f a ] > x`, a keyword's own redirection):
52
+ * such a piece is dropped from `segments` (the tightening reader asks about program runs, and it runs
53
+ * none) — but the shell still opens the target, so a reader that asks about EFFECTS must refuse the line. */
54
+ readonly strayRedirection: boolean;
37
55
  }
38
56
  export { SHELL_WRAPPER_TABLE, type ShellWrapperName } from "./shell-wrapper-table.js";
39
57
  /** Longer than this and the command is not read at all (one unreadable segment): every pass is linear,
@@ -13,11 +13,13 @@ const ALL_OPS = [...CONNECTOR_OPS, ...REDIRECT_OPS];
13
13
  export const MAX_SHELL_READ_CHARS = 10_000;
14
14
  export function readShellCommand(source) {
15
15
  if (source.length > MAX_SHELL_READ_CHARS)
16
- return { segments: [{ argv: [], peeled: [], unreadable: `the command is longer than ${MAX_SHELL_READ_CHARS} characters — not read` }], connectors: [], grouped: false };
16
+ return { segments: [{ argv: [], redirections: [], peeled: [], unreadable: `the command is longer than ${MAX_SHELL_READ_CHARS} characters — not read` }], connectors: [], grouped: false, backgrounded: false, strayRedirection: false };
17
17
  const segments = [];
18
18
  const connectors = [];
19
19
  let grouped = false;
20
- let seg = { words: [] };
20
+ let backgrounded = false;
21
+ let strayRedirection = false;
22
+ let seg = { words: [], redirections: [] };
21
23
  let word = freshWord();
22
24
  let pendingRedirect;
23
25
  let heredocs = [];
@@ -40,6 +42,7 @@ export function readShellCommand(source) {
40
42
  badDelimiter = true;
41
43
  heredocs.push({ delimiter: word.text, quoted: word.raw !== word.text, dash: pendingRedirect === "<<-" });
42
44
  }
45
+ seg.redirections.push({ op: pendingRedirect, target: publicWord(word) });
43
46
  pendingRedirect = undefined;
44
47
  }
45
48
  else {
@@ -56,17 +59,19 @@ export function readShellCommand(source) {
56
59
  let built = buildSegment(seg);
57
60
  if (built === "keyworded")
58
61
  grouped = true;
62
+ if ((built === "empty" || built === "keyworded") && seg.redirections.length > 0)
63
+ strayRedirection = true;
59
64
  if (built === "empty" || built === "keyworded") {
60
65
  const continuation = connector === "\n" && lastConnector !== undefined && lastConnector !== ";" && lastConnector !== "&" && lastConnector !== "\n";
61
66
  const legal = built === "keyworded" || afterBoundary || connector === undefined || connector === "\n" || lastConnector === "\n";
62
67
  if (!legal)
63
- built = { argv: [], peeled: [], unreadable: `an empty command before \`${connector}\` — a syntax error` };
68
+ built = { argv: [], redirections: seg.redirections, peeled: [], unreadable: `an empty command before \`${connector}\` — a syntax error` };
64
69
  else {
65
70
  if (connector !== undefined && connector !== "\n" && segments.length > 0)
66
71
  grouped = true;
67
72
  if (connector !== undefined && !continuation)
68
73
  lastConnector = connector;
69
- seg = { words: [] };
74
+ seg = { words: [], redirections: [] };
70
75
  afterBoundary = false;
71
76
  return;
72
77
  }
@@ -76,7 +81,7 @@ export function readShellCommand(source) {
76
81
  segments.push(built);
77
82
  if (connector !== undefined)
78
83
  lastConnector = connector;
79
- seg = { words: [] };
84
+ seg = { words: [], redirections: [] };
80
85
  afterBoundary = false;
81
86
  };
82
87
  const boundary = () => { finishSegment(undefined); grouped = true; afterBoundary = true; lastConnector = undefined; };
@@ -187,6 +192,8 @@ export function readShellCommand(source) {
187
192
  i += op.length;
188
193
  continue;
189
194
  }
195
+ if (op === "&")
196
+ backgrounded = true;
190
197
  finishSegment(op);
191
198
  i += op.length;
192
199
  continue;
@@ -271,7 +278,7 @@ export function readShellCommand(source) {
271
278
  seg.unreadable ??= `the connector \`${lastConnector}\` has no command after it — a syntax error`;
272
279
  finishSegment(undefined);
273
280
  }
274
- return { segments, connectors, grouped };
281
+ return { segments, connectors, grouped, backgrounded, strayRedirection };
275
282
  }
276
283
  const publicWord = ({ text, raw, expands }) => ({ text, raw, expands });
277
284
  const SUBSTITUTION_UNREAD = "a command substitution runs a program this reader does not read";
@@ -281,8 +288,8 @@ export function isFullyReadable(shape) {
281
288
  }
282
289
  function buildSegment(seg) {
283
290
  if (seg.unreadable !== undefined)
284
- return { argv: seg.words.map(publicWord), peeled: [], unreadable: seg.unreadable };
285
- const carried = () => (seg.substitution === true ? { argv: [], peeled: [], peelUnreadable: SUBSTITUTION_UNREAD } : keyworded ? "keyworded" : "empty");
291
+ return { argv: seg.words.map(publicWord), redirections: seg.redirections, peeled: [], unreadable: seg.unreadable };
292
+ const carried = () => (seg.substitution === true ? { argv: [], redirections: seg.redirections, peeled: [], peelUnreadable: SUBSTITUTION_UNREAD } : keyworded ? "keyworded" : "empty");
286
293
  let keyworded = false;
287
294
  let k0 = 0;
288
295
  while (k0 < seg.words.length && seg.words[k0].expands === false && (PREFIX_KEYWORDS.has(seg.words[k0].text) || CLOSING_KEYWORDS.has(seg.words[k0].text))) {
@@ -298,7 +305,7 @@ function buildSegment(seg) {
298
305
  if (head.expands === false && (head.text === "[[" || head.text === "["))
299
306
  return carried();
300
307
  if (head.expands === false && OPAQUE_KEYWORDS.has(head.text))
301
- return { argv: words.map(publicWord), peeled: [], unreadable: `a \`${head.text}\` control structure — its commands need a syntax tree` };
308
+ return { argv: words.map(publicWord), redirections: seg.redirections, peeled: [], unreadable: `a \`${head.text}\` control structure — its commands need a syntax tree` };
302
309
  const argv = words.map(publicWord);
303
310
  const peeled = [];
304
311
  let peelUnreadable;
@@ -338,7 +345,7 @@ function buildSegment(seg) {
338
345
  }
339
346
  if (seg.substitution === true || [argv, ...peeled].some((c) => c[0]?.expands === false && evaluatesOperand(c)))
340
347
  peelUnreadable ??= SUBSTITUTION_UNREAD;
341
- return { argv, peeled, ...(peelUnreadable !== undefined ? { peelUnreadable } : {}) };
348
+ return { argv, redirections: seg.redirections, peeled, ...(peelUnreadable !== undefined ? { peelUnreadable } : {}) };
342
349
  }
343
350
  function unwrap(words) {
344
351
  const head = words[0];
@@ -50,9 +50,12 @@ export function evaluatesOperand(run) {
50
50
  return run.some((w) => namesArrayElement(w.text) || namesArrayElement(w.raw) || (integer && /^[A-Za-z_][A-Za-z0-9_]*\+?=.*[A-Za-z_$]/.test(w.text)));
51
51
  }
52
52
  function namesArrayElement(word) {
53
- const open = word.indexOf("[");
54
- if (open <= 0 || !/[A-Za-z0-9_]/.test(word[open - 1]))
55
- return false;
56
- const close = word.indexOf("]", open);
57
- return close > 0 && /[A-Za-z_$]/.test(word.slice(open + 1, close));
53
+ for (let open = word.indexOf("["); open > 0; open = word.indexOf("[", open + 1)) {
54
+ if (!/[A-Za-z0-9_]/.test(word[open - 1]))
55
+ continue;
56
+ const close = word.indexOf("]", open);
57
+ if (close > 0 && /[A-Za-z_$]/.test(word.slice(open + 1, close)))
58
+ return true;
59
+ }
60
+ return false;
58
61
  }
@@ -99,7 +99,10 @@ export interface ToolCallRequest {
99
99
  * - `"org_unavailable"` — an org-governed deployment could not adjudicate against a snapshot, so the
100
100
  * whole decision boundary failed closed (see `ORG_UNAVAILABLE_DECISION_REASON`, the single
101
101
  * spelling this word is minted from). */
102
- declare const DECISION_REASONS: readonly ["rule", "mode", "hook", "safety", "classifier", "persisted_rule", "sandbox", "org_rule", "org_unavailable"];
102
+ /** `read_only` (#619): the shell tool's command was PROVABLY read-only by the engine's transcribed upstream
103
+ * tables (`readOnlyShellVerdict`) and the allow layer let it run without a question — after every
104
+ * deny/ask lane and the person's own allow rules, never over a mandated ask. */
105
+ declare const DECISION_REASONS: readonly ["rule", "mode", "hook", "safety", "classifier", "persisted_rule", "sandbox", "org_rule", "org_unavailable", "read_only"];
103
106
  export type DecisionReason = (typeof DECISION_REASONS)[number];
104
107
  /** Read the engine-attested settlement off a funneled decision FOR the named call (the gate's exit is the
105
108
  * one consumer): an attestation bound to a different call is a replayed object and answers absence.
@@ -10,7 +10,7 @@ import { delimitUntrusted, inlineUntrusted, REVIEWER_NOTE_MAX_BODY } from "./unt
10
10
  import { isNamespacedCoveringRuleName, namespacedRuleNameCovers, parsePermissionRule } from "./permission-rules.js";
11
11
  import { protocolOf } from "./protocol-table.js";
12
12
  import { isAbsolutePathForm, isWinFormPath, writeTargetPath } from "../tools/fs/safety.js";
13
- const DECISION_REASONS = ["rule", "mode", "hook", "safety", "classifier", "persisted_rule", "sandbox", "org_rule", "org_unavailable"];
13
+ const DECISION_REASONS = ["rule", "mode", "hook", "safety", "classifier", "persisted_rule", "sandbox", "org_rule", "org_unavailable", "read_only"];
14
14
  const DECISION_REASON_SET = new Set(DECISION_REASONS);
15
15
  function settledByNobody(kind) {
16
16
  return { kind, who: { party: "none" }, when: Date.now() };
@@ -15,14 +15,34 @@ export declare function errorResult(text: string, details?: unknown): {
15
15
  isError: true;
16
16
  details?: unknown;
17
17
  };
18
- /** True iff `x` is an `AgentTool` this module's `defineTool` itself constructed see {@link DEFINE_TOOL_BRAND}. */
18
+ /** The seat's function: the mount's ctx builder and the very object being mounted (the seal compares it to the stamped one). */
19
+ type DefineToolRebind = (enrich: ToolCtxEnricher, mounted: object) => AgentTool;
20
+ /** True iff `x` is an `AgentTool` this module's `defineTool` itself constructed — see {@link DEFINE_TOOL_BRAND}.
21
+ * An OWN property read: an object that merely INHERITS the brand (`Object.create(product)`, a Proxy over it)
22
+ * is not the product — the rebind seat would rebuild the product and drop whatever the derived object
23
+ * overrode, so such objects are not recognised here and fall to the spec arm's contract (which they do not
24
+ * meet either; see {@link stampDefineToolBrand} for the one supported wrapper form). */
19
25
  export declare function isDefineToolProduct(x: unknown): x is AgentTool;
20
- /** RB-362 类修同源章点 — the ONE place the brand is stamped. `defineTool` uses it on its own product;
21
- * the only OTHER legitimate caller is a wrapper that (a) starts from a branded product and (b)
22
- * preserves the AgentTool `execute(toolCallId, rawParams, signal, onUpdate)` contract faithfully
23
- * (e.g. teacher.ts's logging wrapper). A shallow copy that does NOT re-stamp deliberately loses the
24
- * brand that is the brand's documented survival contract, not an accident. Internal only. */
25
- export declare function stampDefineToolBrand<T extends object>(tool: T): T;
26
+ /** RB-362 类修同源章点 — the ONE place the brand is stamped, and with it the rebind seat (a branded object
27
+ * ALWAYS carries one; that invariant is what lets the caller mount rebind every product without a
28
+ * "cannot enrich" arm). `defineTool` uses it on its own product; the only OTHER legitimate caller is a
29
+ * wrapper that (a) starts from a branded product, (b) preserves the AgentTool
30
+ * `execute(toolCallId, rawParams, signal, onUpdate)` contract faithfully and (c) hands over a `rebind`
31
+ * that re-wraps the REBOUND inner product the same way (e.g. teacher.ts's logging wrapper) — so a mount
32
+ * rebinding the wrapper gets a wrapper over an enriched product, not an enriched product without the
33
+ * wrapper. A shallow copy that does NOT re-stamp deliberately loses the brand — that is the brand's
34
+ * documented survival contract, not an accident. Internal only. */
35
+ export declare function stampDefineToolBrand<T extends object>(tool: T, rebind: DefineToolRebind): T;
36
+ /**
37
+ * A branded product REBOUND to a mount's ctx builder — the product's own `execute` rebuilt over its own
38
+ * spec with `enrich` applied per call (see {@link ToolCtxEnricher}; a builder the product was built with
39
+ * runs AFTER the mount's, refining the run's trusted seats, and the per-call identity is re-stamped after
40
+ * both). This is how a `defineTool()` product handed to `TaskSpec.tools` receives exactly the ctx a raw
41
+ * `ToolSpec` on the same mount receives: one mount law, whatever object shape the caller handed in.
42
+ * Throws on an unbranded object — the caller checks {@link isDefineToolProduct} first; a branded object
43
+ * without the seat cannot be constructed (both are written at the one stamp site).
44
+ */
45
+ export declare function rebindDefineToolCtx(product: AgentTool, enrich: ToolCtxEnricher): AgentTool;
26
46
  /**
27
47
  * RB-409 — a per-call ctx builder a MOUNT hands to {@link defineTool}.
28
48
  *
@@ -58,3 +78,4 @@ export interface DefineToolOptions {
58
78
  }
59
79
  /** Adapt a friendly ToolSpec into the vendored AgentTool the agent loop expects. */
60
80
  export declare function defineTool<TParams extends TSchema = TSchema>(spec: ToolSpec<TParams>, options?: DefineToolOptions): AgentTool<TParams>;
81
+ export {};
@@ -26,13 +26,46 @@ export function errorResult(text, details) {
26
26
  return details === undefined ? { content: text, isError: true } : { content: text, isError: true, details };
27
27
  }
28
28
  const DEFINE_TOOL_BRAND = Symbol("sema.core.defineTool.product");
29
+ const DEFINE_TOOL_REBIND = Symbol("sema.core.defineTool.rebind");
29
30
  export function isDefineToolProduct(x) {
30
- return typeof x === "object" && x !== null && x[DEFINE_TOOL_BRAND] === true;
31
+ return typeof x === "object" && x !== null && Object.getOwnPropertyDescriptor(x, DEFINE_TOOL_BRAND)?.value === true;
31
32
  }
32
- export function stampDefineToolBrand(tool) {
33
+ export function stampDefineToolBrand(tool, rebind) {
33
34
  Object.defineProperty(tool, DEFINE_TOOL_BRAND, { value: true, enumerable: false });
35
+ Object.defineProperty(tool, DEFINE_TOOL_REBIND, { value: sealedRebind(tool, rebind), enumerable: false });
34
36
  return tool;
35
37
  }
38
+ function sealedRebind(stamped, rebuild) {
39
+ const ownExecute = Object.getOwnPropertyDescriptor(stamped, "execute")?.value;
40
+ const stillSealed = () => {
41
+ const d = Object.getOwnPropertyDescriptor(stamped, "execute");
42
+ return d !== undefined && "value" in d && d.value === ownExecute;
43
+ };
44
+ return (enrich, mounted) => {
45
+ if (mounted !== stamped) {
46
+ throw new Error(`defineTool product ${JSON.stringify(stamped.name)}: the object being mounted is not the one this brand was stamped on (a derived object — prototype, proxy or descriptor copy — carrying a borrowed brand), so it cannot be rebound to the run's ctx without dropping its own overrides — wrap the product as a NEW object (stampDefineToolBrand with a rebind that re-wraps the rebound inner product), or author it as a raw ToolSpec`);
47
+ }
48
+ const refuse = () => {
49
+ throw new Error(`defineTool product ${JSON.stringify(stamped.name)}: its execute is not the own data property the factory wrote (replaced, or turned into an accessor, after the factory built it), so it cannot be rebound to the run's ctx without dropping the replacement — wrap the product as a NEW object (stampDefineToolBrand with a rebind that re-wraps the rebound inner product), or author it as a raw ToolSpec`);
50
+ };
51
+ if (!stillSealed())
52
+ refuse();
53
+ const rebound = rebuild(enrich, mounted);
54
+ if (!stillSealed())
55
+ refuse();
56
+ return rebound;
57
+ };
58
+ }
59
+ export function rebindDefineToolCtx(product, enrich) {
60
+ const rebind = Object.getOwnPropertyDescriptor(product, DEFINE_TOOL_REBIND)?.value;
61
+ if (typeof rebind !== "function")
62
+ throw new Error(`rebindDefineToolCtx: ${product.name} is not a defineTool product (no rebind seat)`);
63
+ return rebind(enrich, product);
64
+ }
65
+ function stampCallIdentity(enriched, toolCallId, signal) {
66
+ const own = (value) => ({ value, enumerable: true, configurable: true, writable: true });
67
+ return Object.create(Object.getPrototypeOf(enriched), { ...Object.getOwnPropertyDescriptors(enriched), toolCallId: own(toolCallId), signal: own(signal) });
68
+ }
36
69
  export function defineTool(spec, options) {
37
70
  const executionMode = spec.executionMode ?? (spec.effect === "read" ? "parallel" : "sequential");
38
71
  const tool = {
@@ -77,7 +110,8 @@ export function defineTool(spec, options) {
77
110
  let ret;
78
111
  try {
79
112
  const baseCtx = { toolCallId, signal };
80
- ret = await spec.execute(params, options?.enrichCtx ? { ...options.enrichCtx(baseCtx), toolCallId, signal } : baseCtx);
113
+ const ctx = options?.enrichCtx ? stampCallIdentity(options.enrichCtx(baseCtx), toolCallId, signal) : baseCtx;
114
+ ret = await spec.execute(params, ctx);
81
115
  }
82
116
  catch (err) {
83
117
  const wrapped = new Error(formatToolError(err));
@@ -105,6 +139,12 @@ export function defineTool(spec, options) {
105
139
  ...(spec.aliases && spec.aliases.length > 0 ? { aliases: spec.aliases } : {}),
106
140
  });
107
141
  }
108
- stampDefineToolBrand(tool);
142
+ stampDefineToolBrand(tool, (enrich) => {
143
+ const composed = options?.enrichCtx === undefined ? enrich : (base) => options.enrichCtx(enrich(base));
144
+ const rebuilt = defineTool(spec, { ...options, enrichCtx: composed });
145
+ const { [DEFINE_TOOL_BRAND]: _brand, [DEFINE_TOOL_REBIND]: _seat, ...face } = Object.getOwnPropertyDescriptors(tool);
146
+ const copy = Object.create(Object.getPrototypeOf(tool), { ...face, execute: { value: rebuilt.execute, enumerable: true, configurable: true, writable: true } });
147
+ return stampDefineToolBrand(copy, (again) => rebindDefineToolCtx(rebuilt, again));
148
+ });
109
149
  return tool;
110
150
  }
@@ -425,6 +425,21 @@ export type TraceEvent = {
425
425
  toolCallId: string;
426
426
  rules: readonly string[];
427
427
  ts: number;
428
+ } | {
429
+ /**
430
+ * #619 — the READ-ONLY reader cleared a shell call, so no person and no classifier was asked: the
431
+ * allow layer's second attribution channel beside `permission.persisted_rule_allowed`. `command` is
432
+ * the FINAL command the gate judged (a policy rewrite included) — model-authored text, carried
433
+ * verbatim as the audit fact this frame exists for (a consumer rendering it applies its own
434
+ * display sanitizer, as it does for `tool_start` args).
435
+ */
436
+ kind: "permission.read_only_allowed";
437
+ version: 1;
438
+ taskId: string;
439
+ toolName: string;
440
+ toolCallId: string;
441
+ command: string;
442
+ ts: number;
428
443
  } | {
429
444
  /**
430
445
  * design/179 — the persisted allow-rule store could not be read, so this call was adjudicated with
@@ -131,6 +131,7 @@ export declare class AgentHarness<TSkill extends Skill = Skill, TPromptTemplate
131
131
  private runPromise?;
132
132
  private pendingSessionWrites;
133
133
  private model;
134
+ /** The run's thinking level; `undefined` = undeclared (provider default, no wire key) — distinct from "off". */
134
135
  private thinkingLevel;
135
136
  /** RB-30 terminal fix — runner-set sink for engine-note payloads left undrained at agent_end. */
136
137
  onUndrainedEngineNotes?: (payloads: unknown[]) => void;
@@ -410,7 +411,8 @@ export declare class AgentHarness<TSkill extends Skill = Skill, TPromptTemplate
410
411
  } & UserMessageProvenance): Promise<void>;
411
412
  appendMessage(message: AgentMessage): Promise<void>;
412
413
  getModel(): Model;
413
- getThinkingLevel(): ThinkingLevel;
414
+ /** The current level, or `undefined` when none was declared (provider default — no wire key). */
415
+ getThinkingLevel(): ThinkingLevel | undefined;
414
416
  setModel(model: Model): Promise<void>;
415
417
  setThinkingLevel(level: ThinkingLevel): Promise<void>;
416
418
  setActiveTools(toolNames: string[]): Promise<void>;
@@ -277,7 +277,7 @@ export class AgentHarness {
277
277
  this.tools.set(tool.name, tool);
278
278
  }
279
279
  this.model = options.model;
280
- this.thinkingLevel = options.thinkingLevel ?? "off";
280
+ this.thinkingLevel = options.thinkingLevel;
281
281
  this.activeToolNames =
282
282
  options.activeToolNames ?? (options.tools ?? []).map((tool) => tool.name);
283
283
  this.steeringQueueMode = options.steeringMode ?? "one-at-a-time";
@@ -1178,7 +1178,8 @@ export interface ModelSelectEvent {
1178
1178
  export interface ThinkingLevelSelectEvent {
1179
1179
  type: "thinking_level_select";
1180
1180
  level: ThinkingLevel;
1181
- previousLevel: ThinkingLevel;
1181
+ /** `undefined` when the run had no declared level before this selection (provider default). */
1182
+ previousLevel: ThinkingLevel | undefined;
1182
1183
  }
1183
1184
  export interface ResourcesUpdateEvent<TSkill extends Skill = Skill, TPromptTemplate extends PromptTemplate = PromptTemplate> {
1184
1185
  type: "resources_update";
@@ -1346,7 +1347,8 @@ export interface AgentHarnessOptions<TSkill extends Skill = Skill, TPromptTempla
1346
1347
  env: ExecutionEnv;
1347
1348
  session: Session;
1348
1349
  model: Model;
1349
- thinkingLevel: ThinkingLevel;
1350
+ /** `undefined` = no level declared (provider default). */
1351
+ thinkingLevel: ThinkingLevel | undefined;
1350
1352
  activeTools: TTool[];
1351
1353
  resources: AgentHarnessResources<TSkill, TPromptTemplate>;
1352
1354
  }) => string | Promise<string>);
package/dist/index.d.ts CHANGED
@@ -195,7 +195,9 @@ export { removePersistedRule, applyTombstones, sameScope, sameRuleIdentity, isVa
195
195
  export { syncPermissionRules, parseRuleSyncResponse, PERMISSION_RULE_SYNC_PATH, LOCAL_OWNER_UNSYNCABLE_CODE, type PermissionRuleSyncTransport, type PermissionRuleSyncResult, type RuleSyncRequestBody, type RuleSyncResponseBody, } from "./core/permission-rule-sync.js";
196
196
  export { createPermissionRuleStoreProvider, effectivePermissionRules, effectiveOrThrow, ruleSourceOf, type PermissionRuleStore, type PermissionRuleStoreProvider, type PermissionRuleStoreConfig, type EffectivePermissionRules, type EffectivePermissionRule, type RemovedPermissionRule, type RuleSource, } from "./core/permission-rule-provider.js";
197
197
  export { InMemorySessionRulePartition, type SessionRulePartition, type SessionRuleAdd, type SessionRuleApplyResult, } from "./core/permission-rule-session.js";
198
- export { readShellCommand, isFullyReadable, MAX_SHELL_READ_CHARS, SHELL_WRAPPER_TABLE, type ShellCommandShape, type ShellSegment, type ShellWord, type ShellWrapperName, } from "./core/shell-lexer.js";
198
+ export { readShellCommand, isFullyReadable, MAX_SHELL_READ_CHARS, SHELL_WRAPPER_TABLE, type ShellCommandShape, type ShellSegment, type ShellWord, type ShellRedirection, type ShellWrapperName, } from "./core/shell-lexer.js";
199
+ export { readOnlyShellVerdict, type ReadOnlyShellVerdict } from "./core/read-only-shell.js";
200
+ export { READ_ONLY_FLAG_ARITIES, FLAG_VALUE_ACCEPTS, READ_ONLY_COMMAND_TABLE, READ_ONLY_BARE_PROGRAMS, READ_ONLY_GLOB_PROGRAMS, READ_ONLY_EXACT_FORMS, READ_ONLY_BARE_ONLY, FIND_ACTION_PRIMARIES, FIND_VALUE_PRIMARIES, FIND_NEWER_PRIMARY, READ_ONLY_ENV_NAMES, XARGS_READ_ONLY_TARGETS, type ReadOnlyFlagArity, type ReadOnlyCommandRow, } from "./core/read-only-shell-table.js";
199
201
  export { orgRuleVerdictFor, type OrgRuleVerdict, orgRuleShadows, unenforceableOrgRules, ORG_UNAVAILABLE_DECISION_REASON, ORG_RULE_DECISION_REASON, ORG_ADJUDICATION_TIMEOUT_MS, type OrgPermissionRule, type OrgRuleSnapshot, type OrgRuleSnapshotProvider, type OrgRuleStatePersistence, type PersistedOrgRuleState, type OrgRulePartitionConfig, type OrgRuleResolution, type OrgRuleStatus, } from "./core/permission-rule-org.js";
200
202
  export { RULE_SYNC_DROP_CODES, type RuleSyncDropReason, type RuleQuarantineReason } from "./core/governance-codes.js";
201
203
  export { prepareCardApproval, confirmRuleApproval, type ConfirmResult, type ConfirmRefusalReason, precheckEditedRuleText, type EditedRuleTextPrecheck, redeemRuleTicket, redeemRuleBatch, prepareCcImport, prepareStarterBatch, mintRuleTicket, STARTER_RULES, InMemoryRuleApprovalRecordStore, ruleOffersOfRecord, type RuleTicket, type RuleCandidate, type RuleApprovalKind, type RuleApprovalRecord, type RuleApprovalRecordStore, type RuleOffer2, type StaleRuleApprovalRecord, type RuleConsentDeps, type RedeemResult, type RedeemedBatchMember, type CcImportLayer, type ImportedSettingsLayer, type ImportPreview, } from "./core/permission-rule-consent.js";
package/dist/index.js CHANGED
@@ -155,6 +155,8 @@ export { syncPermissionRules, parseRuleSyncResponse, PERMISSION_RULE_SYNC_PATH,
155
155
  export { createPermissionRuleStoreProvider, effectivePermissionRules, effectiveOrThrow, ruleSourceOf, } from "./core/permission-rule-provider.js";
156
156
  export { InMemorySessionRulePartition, } from "./core/permission-rule-session.js";
157
157
  export { readShellCommand, isFullyReadable, MAX_SHELL_READ_CHARS, SHELL_WRAPPER_TABLE, } from "./core/shell-lexer.js";
158
+ export { readOnlyShellVerdict } from "./core/read-only-shell.js";
159
+ export { READ_ONLY_FLAG_ARITIES, FLAG_VALUE_ACCEPTS, READ_ONLY_COMMAND_TABLE, READ_ONLY_BARE_PROGRAMS, READ_ONLY_GLOB_PROGRAMS, READ_ONLY_EXACT_FORMS, READ_ONLY_BARE_ONLY, FIND_ACTION_PRIMARIES, FIND_VALUE_PRIMARIES, FIND_NEWER_PRIMARY, READ_ONLY_ENV_NAMES, XARGS_READ_ONLY_TARGETS, } from "./core/read-only-shell-table.js";
158
160
  export { orgRuleVerdictFor, orgRuleShadows, unenforceableOrgRules, ORG_UNAVAILABLE_DECISION_REASON, ORG_RULE_DECISION_REASON, ORG_ADJUDICATION_TIMEOUT_MS, } from "./core/permission-rule-org.js";
159
161
  export { RULE_SYNC_DROP_CODES } from "./core/governance-codes.js";
160
162
  export { prepareCardApproval, confirmRuleApproval, precheckEditedRuleText, redeemRuleTicket, redeemRuleBatch, prepareCcImport, prepareStarterBatch, mintRuleTicket, STARTER_RULES, InMemoryRuleApprovalRecordStore, ruleOffersOfRecord, } from "./core/permission-rule-consent.js";
@@ -260,8 +260,11 @@ export interface RunWorkflowToolDeps {
260
260
  /** The HOST task's effective working root — threaded into every spawned agent's trusted internals so a
261
261
  * TOC env factory can root the child at the parent's cwd (CC parity, 2026-07-03). */
262
262
  parentCwd?: string;
263
- /** Call-time getter for the HOST task's current thinking level a spawned agent with no explicit
264
- * script/baseline `thinking` inherits it (the parentCwd/model-snapshot companion). */
263
+ /** Call-time getter for the level a spawned agent runs at when neither the script, the agentType
264
+ * definition nor the governance baseline set `thinking` (the mount folds the deployment's STATED
265
+ * `roles.subagent.thinking` over the HOST task's current level — the child thinking seat's lower rungs).
266
+ * Threaded to the workflow as `defaultThinking` and applied at the launch site AFTER the agentType fold,
267
+ * the `parentModel` → `defaultModel` companion. */
265
268
  parentThinking?: () => import("../core/types.js").TaskSpec["thinking"];
266
269
  /** 5.30 merge-rescan (design/199 parity gap) — call-time getter for the HOST task's RESOLVED
267
270
  * read-face containment; folds stricter-wins into every spawned workflow child (see
@@ -488,7 +488,7 @@ export async function createRunWorkflowTool(d) {
488
488
  }
489
489
  : governance;
490
490
  const scriptFn = (wfCtx) => {
491
- const primitives = buildWorkflowPrimitives(wfCtx, runGovernance, d.onAgentSpawn, d.parentThinking, principal, ctx.checkpointStoreDisabledForChildren === true || d.parentCheckpointStoreDisabled === true, d.parentReadFace, d.parentReadDenyPatterns, ctx.handsReadOnly === true || d.parentHandsReadOnly === true, ctx.interactiveTools === false || d.parentInteractiveTools === false);
491
+ const primitives = buildWorkflowPrimitives(wfCtx, runGovernance, d.onAgentSpawn, principal, ctx.checkpointStoreDisabledForChildren === true || d.parentCheckpointStoreDisabled === true, d.parentReadFace, d.parentReadDenyPatterns, ctx.handsReadOnly === true || d.parentHandsReadOnly === true, ctx.interactiveTools === false || d.parentInteractiveTools === false);
492
492
  return d.scriptRunner.run({ scriptSource: script, primitives, scriptArgs: effectiveArgs, signal: wfCtx.signal }).then((r) => r.result);
493
493
  };
494
494
  if (ctx.signal?.aborted) {
@@ -546,6 +546,7 @@ export async function createRunWorkflowTool(d) {
546
546
  return d.parentMemoryCaptureState !== undefined ? { parentMemoryCaptureState: d.parentMemoryCaptureState } : {};
547
547
  })(),
548
548
  ...(d.parentModel !== undefined ? { defaultModel: d.parentModel } : {}),
549
+ ...(d.parentThinking !== undefined ? { defaultThinking: d.parentThinking } : {}),
549
550
  ...(d.parentGetApiKeyAndHeaders !== undefined ? { defaultGetApiKeyAndHeaders: d.parentGetApiKeyAndHeaders } : {}),
550
551
  ...(hostDurableApproval !== undefined && d.store !== undefined ? { defaultDurableApproval: { ...hostDurableApproval } } : {}),
551
552
  ...(resumeFromRunId !== undefined && d.parkedResume !== undefined && d.parkedResume(resumeFromRunId) !== undefined ? { parkedResume: d.parkedResume(resumeFromRunId) } : {}),