@sema-agent/core 7.23.3 → 7.23.5

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 (52) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/dist/agents/list-agents-tool.d.ts +5 -1
  3. package/dist/agents/list-agents-tool.js +14 -2
  4. package/dist/brain/anthropic.js +3 -2
  5. package/dist/brain/circuit-breaker.js +3 -3
  6. package/dist/brain/degrading.js +3 -5
  7. package/dist/brain/errors.d.ts +35 -18
  8. package/dist/brain/errors.js +49 -31
  9. package/dist/brain/failover.js +2 -3
  10. package/dist/brain/open-responses.js +6 -5
  11. package/dist/brain/openai.js +6 -5
  12. package/dist/brain/route-adjudicator.d.ts +26 -4
  13. package/dist/brain/route-adjudicator.js +26 -0
  14. package/dist/brain/stream-engine.js +8 -2
  15. package/dist/core/checkpoint-store.d.ts +4 -6
  16. package/dist/core/checkpoint-store.js +2 -2
  17. package/dist/core/permission-rule-model.js +8 -5
  18. package/dist/core/runner/prepare-delegation-surface.d.ts +3 -0
  19. package/dist/core/runner/prepare-delegation-surface.js +1 -0
  20. package/dist/core/runner/prepare-hands-readface.d.ts +4 -1
  21. package/dist/core/runner/prepare-hands-readface.js +7 -7
  22. package/dist/core/runner/prepare-task.js +2 -2
  23. package/dist/core/runner/run-leg.js +2 -13
  24. package/dist/core/task-spec.d.ts +2 -1
  25. package/dist/core/thrown-value.d.ts +30 -0
  26. package/dist/core/thrown-value.js +23 -1
  27. package/dist/core/tool-catalog-entries.js +1 -1
  28. package/dist/core/tool-model-gate.js +3 -15
  29. package/dist/core/tools.js +7 -7
  30. package/dist/core/untrusted-text.d.ts +6 -0
  31. package/dist/core/untrusted-text.js +1 -0
  32. package/dist/core/workflow-run-store-contract.js +13 -0
  33. package/dist/core/workflow-run-store.d.ts +15 -0
  34. package/dist/core/workflow-run-store.js +1 -0
  35. package/dist/engine/loop/agent-loop.js +10 -24
  36. package/dist/tools/fs/bash-program-position.d.ts +23 -0
  37. package/dist/tools/fs/bash-program-position.js +11 -0
  38. package/dist/tools/fs/bash-readonly-classifier.d.ts +64 -42
  39. package/dist/tools/fs/bash-readonly-classifier.js +33 -10
  40. package/dist/tools/fs/fs-bash.d.ts +41 -3
  41. package/dist/tools/fs/fs-bash.js +152 -58
  42. package/dist/tools/fs/index.d.ts +2 -1
  43. package/dist/tools/fs/index.js +1 -0
  44. package/dist/tools/fs/read-deny.d.ts +28 -0
  45. package/dist/tools/fs/read-deny.js +11 -0
  46. package/dist/tools/fs/safety.d.ts +1 -1
  47. package/dist/tools/fs/safety.js +2 -1
  48. package/dist/tools/monitor.d.ts +5 -0
  49. package/dist/tools/monitor.js +7 -0
  50. package/package.json +1 -1
  51. package/wire-changes/core@7.23.4.json +58 -0
  52. package/wire-changes/core@7.23.5.json +133 -0
@@ -1,4 +1,5 @@
1
1
  import { AUTH_CARRIER_NAMES } from "./request-params.js";
2
+ import { BrainError } from "./errors.js";
2
3
  export function normalizeBaseUrl(url) {
3
4
  if (url === undefined)
4
5
  return "";
@@ -16,6 +17,31 @@ export function normalizeBaseUrl(url) {
16
17
  return raw;
17
18
  }
18
19
  }
20
+ export function assertServableRequestRoot(url, seat) {
21
+ if (typeof url !== "string" || url.trim() === "")
22
+ return undefined;
23
+ const trimmed = url.trim();
24
+ const parsed = URL.canParse(trimmed) ? new URL(trimmed) : undefined;
25
+ if (parsed === undefined || (parsed.protocol !== "http:" && parsed.protocol !== "https:")) {
26
+ const named = parsed === undefined ? "it did not parse as a URL" : `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
27
+ throw new BrainError("invalid_request", `${seat} is not a servable request root (${named}) — it must be an ABSOLUTE http(s) URL. Another scheme, ` +
28
+ `or a spelling that is not a URL at all, fails at the transport in a shape indistinguishable from an ` +
29
+ `outage, so it would be retried for the whole backoff ladder instead of refused here.`);
30
+ }
31
+ if (parsed.username === "" && parsed.password === "")
32
+ return trimmed;
33
+ throw new BrainError("invalid_request", `${seat} carries credentials in its authority (${parsed.protocol}//[redacted-credentials]@${parsed.host}${parsed.pathname}) — ` +
34
+ `a request cannot be built from a URL with userinfo, so every call through this brain would fail: put the ` +
35
+ `credential on the apiKey knob or an auth header and leave the URL bare`);
36
+ }
37
+ export function stripUrlCredentials(text) {
38
+ return text.replace(/([A-Za-z][A-Za-z0-9+.\-]*:\/\/)[^/\s"'@]*@/g, "$1[redacted-credentials]@");
39
+ }
40
+ export function resolveRequestRoot(modelBaseUrl, configBaseUrl, laneDefault = "") {
41
+ const entry = assertServableRequestRoot(modelBaseUrl, "Model.baseUrl");
42
+ const declared = assertServableRequestRoot(configBaseUrl, "config.baseUrl");
43
+ return (entry ?? declared ?? laneDefault).replace(/\/+$/, "");
44
+ }
19
45
  export function hasAuthCarrier(headers) {
20
46
  if (!headers)
21
47
  return false;
@@ -1,12 +1,14 @@
1
1
  import { createAssistantMessageEventStream, } from "../internal/llm.js";
2
2
  import { STREAM_TORN_WARNING } from "./stream-shared.js";
3
3
  import { FLOOR_OUTPUT_TOKENS, parseContextOverflow, planOutputCapAdjustment } from "./context-overflow.js";
4
- import { BrainError, classifyConnectFailure, classifyHttp, describeNetworkError, namesTheFailure, readProviderRequestId } from "./errors.js";
4
+ import { abortLikeName, BrainError, classifyConnectFailure, classifyFetchThrow, classifyHttp, describeNetworkError, namesTheFailure, readProviderRequestId } from "./errors.js";
5
+ import { stripUrlCredentials } from "./route-adjudicator.js";
5
6
  import { classifyInputTooLong } from "./input-too-long.js";
6
7
  import { FAST_MAX_BACKOFF_MS, providerWaitHint, retryBackoffMs } from "./retry.js";
7
8
  import { bindBrainStatusEmitter, emitBrainStatus, emitBrainTelemetry } from "./status-sink.js";
8
9
  import { createConnectController, resolveStallTimeoutMs } from "./timeout.js";
9
10
  import { thrownText } from "../core/thrown-value.js";
11
+ import { redactSecrets } from "../core/untrusted-egress.js";
10
12
  const DEFAULT_MAX_RETRIES = 10;
11
13
  const MAX_RETRIES_ENV_CEILING = 15;
12
14
  const MAX_RETRIES_ENV = "SEMA_MAX_RETRIES";
@@ -215,7 +217,7 @@ function emptyAssistant(model) {
215
217
  };
216
218
  }
217
219
  function isAbortError(err) {
218
- return err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError");
220
+ return abortLikeName(err) !== undefined;
219
221
  }
220
222
  function sleep(ms, signal) {
221
223
  return new Promise((resolve) => {
@@ -338,6 +340,10 @@ export function runStreamingBrain(args) {
338
340
  cc.dispose();
339
341
  throw e;
340
342
  }
343
+ else if (classifyFetchThrow(e) === "request_unbuildable") {
344
+ cc.dispose();
345
+ throw new BrainError("invalid_request", `${httpLabel} request could not be built: ${redactSecrets(stripUrlCredentials(thrownText(e)))}`);
346
+ }
341
347
  else {
342
348
  netErr = e;
343
349
  }
@@ -248,12 +248,10 @@ export interface ProbeCause {
248
248
  * account of an approval's reach is worse than none. */
249
249
  readonly further?: ProbeCauseOperands;
250
250
  }
251
- /** The bloat ceiling on {@link RiskDescriptor.probeReason} — one sentence of cause, sized
252
- * like its {@link RiskDescriptor.shadowedRule} sibling. The neutralizer is the injection ceiling; this
253
- * is only the length one. Exported (module-level, not part of the package's public surface) so the gate
254
- * that sanitizes at the entry boundary and the descriptor builder that re-sanitizes at the persist
255
- * boundary cannot cap the same value at two different lengths. */
256
- export declare const PROBE_REASON_MAX = 200;
251
+ /** The bloat ceiling on {@link RiskDescriptor.probeReason} — re-exported from the leaf neutralizer module
252
+ * for the same reason its `PROBE_CAUSE_PATH_MAX` sibling is homed there (a value import from tools/fs
253
+ * back into this module closes an ESM cycle), so mint, gate and persist read ONE cap. */
254
+ export { PROBE_REASON_MAX } from "./untrusted-text.js";
257
255
  /**
258
256
  * The ONE validator for a {@link ProbeCause} arriving from a probe.
259
257
  *
@@ -1,6 +1,6 @@
1
1
  import { randomBytes, randomUUID } from "node:crypto";
2
2
  import { uuidv7 } from "../internal/harness.js";
3
- import { PROBE_CAUSE_PATH_MAX, inlineUntrusted, sanitizeUntrustedText } from "./untrusted-text.js";
3
+ import { PROBE_CAUSE_PATH_MAX, PROBE_REASON_MAX, inlineUntrusted, sanitizeUntrustedText } from "./untrusted-text.js";
4
4
  import { carriesBidiControls } from "./tool-policy.js";
5
5
  import { readDenialLimitFallback } from "./auto-mode.js";
6
6
  import { isAskOrigin, isRuleStoreUnreadable } from "./ask-origin.js";
@@ -14,7 +14,7 @@ export function mintCheckpointToken() {
14
14
  export function mintCheckpointId() {
15
15
  return `cp_${randomUUID()}`;
16
16
  }
17
- export const PROBE_REASON_MAX = 200;
17
+ export { PROBE_REASON_MAX } from "./untrusted-text.js";
18
18
  const PROBE_CAUSE_MAX_SHOWN = 8;
19
19
  export function normalizeProbeCause(value) {
20
20
  if (!isPlainRecord(value))
@@ -144,9 +144,12 @@ function foldSpacing(s) {
144
144
  function reject(code, message) {
145
145
  return { reject: { code, message } };
146
146
  }
147
- const RULE_LANE_FLOOR = { pathPrefixedNameIsText: true };
148
- const MATCH_READING = { terminator: "keep", quotedOperatorsAreText: false, redirection: "reject" };
149
- const OFFER_READING = { terminator: "keep", quotedOperatorsAreText: false, redirection: "tolerate" };
147
+ const RULE_LANE_FLOOR = {
148
+ pathPrefixedNameIsText: true,
149
+ quotedOperatorsAreText: true,
150
+ };
151
+ const MATCH_READING = { terminator: "keep", redirection: "reject" };
152
+ const OFFER_READING = { terminator: "keep", redirection: "tolerate" };
150
153
  function commandBasename(name) {
151
154
  return name.slice(name.lastIndexOf("/") + 1);
152
155
  }
@@ -160,7 +163,7 @@ function ruleLaneShapeOf(command, reading) {
160
163
  names.push(undefined);
161
164
  continue;
162
165
  }
163
- const floor = parseLeadingCommandName(segment, { ...RULE_LANE_FLOOR, quotedOperatorsAreText: reading.quotedOperatorsAreText });
166
+ const floor = parseLeadingCommandName(segment, RULE_LANE_FLOOR);
164
167
  if ("reject" in floor) {
165
168
  return {
166
169
  reject: split.segments.length > 1
@@ -561,7 +564,7 @@ function admitsUnder(rule, command, reading) {
561
564
  if (rule.match === "exact")
562
565
  return folded === rule.command;
563
566
  const bodyShape = ruleLaneShapeOf(rule.command, reading);
564
- if ("reject" in bodyShape)
567
+ if ("reject" in bodyShape || foldSpacing(rule.command) === undefined)
565
568
  return false;
566
569
  if (shape.segments.length !== bodyShape.segments.length)
567
570
  return false;
@@ -1,6 +1,7 @@
1
1
  import type { AgentTool, ExecutionEnv, WorkspaceState } from "../../internal/harness.js";
2
2
  import { type PeerLaneRefs } from "../../agents/peer-session-drain.js";
3
3
  import type { SubagentRetainLedger } from "../../agents/retain-ledger.js";
4
+ import type { BashReadonlyRootBoundary } from "../../tools/fs/bash-readonly-classifier.js";
4
5
  import { type WorktreeSessionRef } from "../../tools/worktree.js";
5
6
  import type { CwdRef } from "../../tools/fs/index.js";
6
7
  import { type ToolResultStore } from "../tool-result-store.js";
@@ -36,6 +37,8 @@ export interface PrepareDelegationSurfaceInput {
36
37
  runnerSelf: RunnerSelfSeat | undefined;
37
38
  /** borrowed-readonly — the background/workflow door's first half: the hands mount decided a real shell with write hands. */
38
39
  backgroundTaskToolsActive: boolean;
40
+ /** borrowed-readonly — this run's shell read boundary (declared at the hands phase), for `Monitor`: the only P11 face that spawns a shell command. */
41
+ shellReadBoundarySeat: (() => BashReadonlyRootBoundary | undefined) | undefined;
39
42
  /** borrowed-readonly — the door's second half: the Workflow tool is on the roster. */
40
43
  workflowToolsActive: boolean;
41
44
  /** borrowed-readonly — the peer lane's mount verdict for this leg. */
@@ -103,6 +103,7 @@ export function prepareDelegationSurface(input) {
103
103
  envHandToolNames.add("Monitor");
104
104
  roster.mount(firstPartyOffload(createMonitorTool(executionEnv, {
105
105
  registry: defaultTaskRegistry,
106
+ ...(input.shellReadBoundarySeat !== undefined ? { readBoundary: input.shellReadBoundarySeat } : {}),
106
107
  owner: hostTaskId,
107
108
  scope: taskScope,
108
109
  ...(sessionId !== undefined ? { sessionId } : {}),
@@ -1,4 +1,4 @@
1
- import { type BeforeWriteHook, type CwdRef, type ReadDenyMatcher, type ContainmentFace } from "../../tools/fs/index.js";
1
+ import { type BashReadonlyRootBoundary, type BeforeWriteHook, type CwdRef, type ReadDenyMatcher, type ContainmentFace } from "../../tools/fs/index.js";
2
2
  import { type ReadFileState } from "../../tools/fs/safety.js";
3
3
  import type { RunnerDeps, ShellGateDoctrine, TaskSpec } from "../types.js";
4
4
  import type { Model } from "../../internal/llm.js";
@@ -266,6 +266,9 @@ export interface PrepareHandsReadFaceResult {
266
266
  /** owned — real shell + write hands + background-capable env: gates the driver's TaskOutput/
267
267
  * TaskStop/Monitor/SendMessage mounts (P11) and the Monitor arm of the shellGate fold here. */
268
268
  backgroundTaskToolsActive: boolean;
269
+ /** owned — this run's shell read BOUNDARY, published for a P11 face that spawns a shell command and
270
+ * mounts LATER than the band (`Monitor`): the SAME closure the band's shell faces got. ⇔ hands. */
271
+ shellReadBoundarySeat: (() => BashReadonlyRootBoundary | undefined) | undefined;
269
272
  /** write-back (D-3 hold) — the run's RESOLVED read face where hands mount; undefined ⇔ no hands
270
273
  * (the hands-less value lives in `handsLessResolvedFace`). The driver assigns this onto its
271
274
  * outer `let` for the call-time closures (ctx enrichment / checkpoint mint / envFacts). */
@@ -3,7 +3,7 @@ import { StubExecutionEnv } from "../stub-env.js";
3
3
  import { hasBackgroundShell } from "../background-shell.js";
4
4
  import { boundaryIsTheSandbox } from "../remote-env.js";
5
5
  import { defaultTaskRegistry } from "../task-registry.js";
6
- import { bashReadBoundaryProbe, bashReversibilityProbe, activeReadDenyBuiltinTiers, compileReadDeny, createHandsToolkit, deploymentFaceClampNotice, pdfModelCapabilitiesOf, inheritSessionReadFileState, resolveContainmentFace, seedReadFileStateFromContext, seedReadFileStateFromTranscript, } from "../../tools/fs/index.js";
6
+ import { bashReadBoundaryProbe, bashReversibilityProbe, activeReadDenyBuiltinTiers, compileReadDeny, readDenySeat, createHandsToolkit, deploymentFaceClampNotice, pdfModelCapabilitiesOf, inheritSessionReadFileState, resolveContainmentFace, seedReadFileStateFromContext, seedReadFileStateFromTranscript, } from "../../tools/fs/index.js";
7
7
  import { resolveKey, sha256 } from "../../tools/fs/safety.js";
8
8
  import { normalizeFileText } from "../../tools/fs/encoding.js";
9
9
  import { wholeFileRecordsFromTranscript } from "./session-file-state-replay.js";
@@ -137,6 +137,7 @@ export function resolveHandsLessReadFace(input) {
137
137
  wsSnapshot: undefined,
138
138
  rebaseWsPath: (p) => p,
139
139
  backgroundTaskToolsActive: false,
140
+ shellReadBoundarySeat: undefined,
140
141
  resolvedReadFace: undefined,
141
142
  resolvedWriteFace: undefined,
142
143
  readDenyAdditionsNormalized,
@@ -167,6 +168,7 @@ export async function prepareHandsMount(input) {
167
168
  let wsSnapshot;
168
169
  let rebaseWsPath = (p) => p;
169
170
  let backgroundTaskToolsActive = false;
171
+ let shellReadBoundarySeat;
170
172
  let resolvedReadFace;
171
173
  let resolvedWriteFace;
172
174
  let readDenyAdditionsNormalized = [];
@@ -327,18 +329,15 @@ export async function prepareHandsMount(input) {
327
329
  sessionSeats.reanchor(sessionId, readFileState, ownCommittedTailEntryId);
328
330
  }
329
331
  : undefined;
330
- const compiledDeny = readDenyMatcher;
331
- const denySeatJudge = compiledDeny !== undefined && compiledDeny.entries.length > 0
332
- ? (p) => compiledDeny.matchPath(p)?.pattern ?? null
333
- : undefined;
332
+ const denySeat = readDenySeat(readDenyMatcher);
334
333
  const shellReadBoundary = () => ({
335
334
  roots: [rootCanonical, ...additionalRootsCanonical, ...additionalReadRootsCanonical],
336
335
  ...(handsCwdRef?.current !== undefined ? { cwd: handsCwdRef.current } : {}),
337
336
  ...(executionEnv.homeDir !== undefined ? { homeDir: executionEnv.homeDir } : {}),
338
- ...(denySeatJudge !== undefined ? { denyMatch: denySeatJudge } : {}),
339
- denyEntries: (compiledDeny?.entries ?? []).map((e) => e.pattern),
337
+ ...denySeat,
340
338
  ...(resolvedReadFace !== undefined ? { face: resolvedReadFace } : {}),
341
339
  });
340
+ shellReadBoundarySeat = shellReadBoundary;
342
341
  const band = createHandsToolkit(executionEnv, readFileState, rootCanonical, {
343
342
  ...(additionalRootsCanonical.length > 0 ? { additionalRoots: additionalRootsCanonical } : {}),
344
343
  ...(additionalReadRootsCanonical.length > 0 ? { additionalReadRoots: additionalReadRootsCanonical } : {}),
@@ -426,6 +425,7 @@ export async function prepareHandsMount(input) {
426
425
  wsSnapshot,
427
426
  rebaseWsPath,
428
427
  backgroundTaskToolsActive,
428
+ shellReadBoundarySeat,
429
429
  resolvedReadFace,
430
430
  resolvedWriteFace,
431
431
  handsLessResolvedWriteFace,
@@ -513,14 +513,14 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
513
513
  }
514
514
  const handsReadFaceInput = { handsEnabled, executionEnv, taskRootFinal, taskRootCanonical, rebaseRestoredPath, resume, session, sessionId, hostTaskId, taskScope, fullShellReachable, effectiveShellGate, toolFaceSnapshot, model, spec, deps, internals, memoryWriteGate, ...(advertisedWritableDirs !== undefined ? { advertisedWritableDirs } : {}), firstPartyOffload, roster, egressTools, irreversibilityTier, irreversibleTools, reversibilityProbes, reminderMark, reminderDisclosureCounts, ...(trackFileEdit !== undefined ? { trackFileEdit } : {}), onFileEdited: noteFileEdited, harnessRef, toolProgressRef, ...(restoredFilePaths.length > 0 ? { restoredFilePaths } : {}) };
515
515
  const handsReadFace = handsEnabled ? await prepareHandsMount(handsReadFaceInput) : resolveHandsLessReadFace(handsReadFaceInput);
516
- const { readFileStateForCheckpoint, seedContextFiles, handsCwdRef, workspaceStateSettle, wsSnapshot, rebaseWsPath, backgroundTaskToolsActive, additionalRootsCanonical, additionalReadRootsCanonical, attachmentRootCanonical, readDenyMatcher, envHandToolNames, sealReadStateSeat } = handsReadFace;
516
+ const { readFileStateForCheckpoint, seedContextFiles, handsCwdRef, workspaceStateSettle, wsSnapshot, rebaseWsPath, backgroundTaskToolsActive, shellReadBoundarySeat, additionalRootsCanonical, additionalReadRootsCanonical, attachmentRootCanonical, readDenyMatcher, envHandToolNames, sealReadStateSeat } = handsReadFace;
517
517
  resolvedReadFace = handsReadFace.resolvedReadFace;
518
518
  resolvedWriteFace = handsReadFace.resolvedWriteFace;
519
519
  readDenyAdditionsNormalized = handsReadFace.readDenyAdditionsNormalized;
520
520
  handsLessResolvedFace = handsReadFace.handsLessResolvedFace;
521
521
  handsLessResolvedWriteFace = handsReadFace.handsLessResolvedWriteFace;
522
522
  shellGatedTools = handsReadFace.shellGatedTools;
523
- const { worktreeSessionRef } = prepareDelegationSurface({ spec, deps, internals, resume, sessionId, hostTaskId, taskScope, taskRootFinal, executionEnv, runnerSelf, backgroundTaskToolsActive, workflowToolsActive, peerLaneActive, peerSendMessageBuiltIn, peerLaneRefs, roster, toolEffects, axisExplicitNegatives, envHandToolNames, toolFaceSnapshot, firstPartyOffload, offloadStore, subagentRetain, enrichSpecToolCtx, handsCwdRef, wsSnapshot, rebaseWsPath, rebaseRestoredPath, workspaceStateSettle });
523
+ const { worktreeSessionRef } = prepareDelegationSurface({ spec, deps, internals, resume, sessionId, hostTaskId, taskScope, taskRootFinal, executionEnv, runnerSelf, backgroundTaskToolsActive, shellReadBoundarySeat, workflowToolsActive, peerLaneActive, peerSendMessageBuiltIn, peerLaneRefs, roster, toolEffects, axisExplicitNegatives, envHandToolNames, toolFaceSnapshot, firstPartyOffload, offloadStore, subagentRetain, enrichSpecToolCtx, handsCwdRef, wsSnapshot, rebaseWsPath, rebaseRestoredPath, workspaceStateSettle });
524
524
  const { liveQuestionFace, contentAskBindings, contentAskRoutable, lateStrandedAnswers, discloseStrandedAnswers, settleContentAskBindings, durableQuestionFace, questionToolMounted, mountedQuestionTool } = prepareQuestionFace({ frozenOnQuestion, spec, deps, checkpointStore, runtimeCaps, resolvedInteractionPosture, resume, sessionId, roster, inheritedUnavailableAsks: () => inheritedUnavailableAsks });
525
525
  if (spec.handsReadOnly !== true) {
526
526
  const sessionScope = sessionId ?? spec.principal ?? spec.taskId ?? "default";
@@ -29,7 +29,7 @@ import { toolEndBodyFrom, toolResultMsg } from "./tool-end-body.js";
29
29
  import { announceListingTruncation } from "../skill-listing-budget.js";
30
30
  import { attachmentEnvelopeTags, commitAgentListing, commitSkillsListing, reduceToolEnd, renderAgentListingFrame, renderOrphanedBackgroundTasks, renderSkillsListingFrame, stampWriteAnchor } from "./turn-attachments.js";
31
31
  import { flushOwedAnnouncedSnapshot } from "./announce-once-ledger.js";
32
- import { asThrownError, thrownText } from "../thrown-value.js";
32
+ import { asThrownError, thrownMarksOf, thrownText } from "../thrown-value.js";
33
33
  export async function runLeg(input) {
34
34
  const { spec, queue, prepared, resume, rs, isDelegatedChild, entryActor, effectiveTimeoutMs, timeout, pushContent, ident, emitCommitted, writeFamilyOf, startedToolCallIds, announceWorkspaceMove, withBrainSinks, flushGitMirror, loopLatch, notificationLane, unsubscribeTaskNotifications, unsubGitRetry, unsubBoundary, unsub, runner, next } = input;
35
35
  let final;
@@ -693,18 +693,7 @@ async function resolvePendingCall(runner, prepared, resume, emit, emitCommitted,
693
693
  }
694
694
  catch (err) {
695
695
  const execError = `Error: ${thrownText(err)}`;
696
- const marks = (() => {
697
- if (err === null || typeof err !== "object")
698
- return undefined;
699
- const src = err;
700
- let d;
701
- if (src.details !== null && typeof src.details === "object" && !Array.isArray(src.details)) {
702
- d = { ...src.details };
703
- }
704
- if (typeof src.errorKind === "string")
705
- d = { ...(d ?? {}), errorKind: src.errorKind };
706
- return d;
707
- })();
696
+ const marks = thrownMarksOf(err);
708
697
  await settleEnd(true, { content: execError, ...(marks !== undefined ? { details: marks } : {}) });
709
698
  const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, execError, true));
710
699
  emitCommitted(eid, "toolResult", pendingAction.toolCallId);
@@ -784,7 +784,8 @@ export interface TaskSpec {
784
784
  * design/199 件B — TASK-layer ADDITIONS to the built-in sensitive-path READ deny set
785
785
  * ({@link import("../tools/fs/read-deny.js").READ_FACE_DEFAULT_DENY_ENTRIES}). Judged by the
786
786
  * structured read faces (Read/Grep/Glob/RepoMap, their traversals, the classify shell gate's
787
- * auto-allow probe and the compaction attachment reader) in BOTH containment modes. Add-only at
787
+ * auto-allow probe, the compaction attachment reader and the `bash_readonly` shell mounted by
788
+ * {@link handsReadOnly} — one deny judge for every read face) in BOTH containment modes. Add-only at
788
789
  * the task layer: entries here UNION with the active built-ins and the deployment's
789
790
  * {@link RunnerDeps.readDenyPatterns}; `[]` ≡ absent (union identity); NOTHING at the task layer
790
791
  * can remove a built-in — the one removal channel is the DEPLOYMENT's built-in configuration
@@ -1,3 +1,18 @@
1
+ /**
2
+ * Is this an Error, including one minted in ANOTHER REALM?
3
+ *
4
+ * `instanceof` compares against THIS realm's `Error.prototype`, so a genuine `Error` thrown by a script the
5
+ * engine ran inside `vm.createContext` — a workflow script is exactly that — fails the test and would be
6
+ * described as an anonymous object, losing the message that is the entire point of reporting it. The brand
7
+ * check is the realm-independent one: an object with the `[[ErrorData]]` internal slot answers `[object Error]`
8
+ * no matter where it was made, and a proxy forwards it. A plain object can forge the tag through
9
+ * `Symbol.toStringTag`, which costs nothing — the only thing this decides is whether we look for a `message`,
10
+ * and a forger that supplies a string message has told us what it wanted rendered anyway.
11
+ *
12
+ * Guarded, because both operations are attacker-reachable: `instanceof` walks a prototype chain a revoked
13
+ * proxy traps, and the tag is a getter that can throw.
14
+ */
15
+ export declare function isErrorLike(value: unknown): boolean;
1
16
  /**
2
17
  * Turning a value that was THROWN at us into text — the ONE rule, for the whole engine.
3
18
  *
@@ -77,3 +92,18 @@ export declare function asThrownError(err: unknown): Error;
77
92
  * to describing the value, which {@link thrownText} already does totally.
78
93
  */
79
94
  export declare function readMember(value: unknown, key: string): unknown;
95
+ /** The PLURAL of {@link readMember}: read a value's own enumerable members, once each, into an inert
96
+ * snapshot — for sites that want a COPY of a whole bag an untrusted value handed over (a thrown
97
+ * error's `details` mark, on its way onto a tool result). The idiom it replaces is `{ ...value }`,
98
+ * a second way of running the value's code: the spread asks for its own keys and evaluates every
99
+ * getter behind them, so one hostile member took the copy down with it, and a revoked proxy traps the
100
+ * enumeration itself (even `Array.isArray` on one throws). Non-objects, arrays (spreading one yields
101
+ * index keys) and anything whose keys cannot be enumerated answer `undefined` — "there is no bag
102
+ * here"; a member that refuses to be read comes back `undefined` in a PRESENT key. */
103
+ export declare function readOwnMembers(value: unknown): Record<string, unknown> | undefined;
104
+ /** The MACHINE-READABLE marks a thrown value carries (`details`, with a string `errorKind` folded in) —
105
+ * the one extractor for both legs that lift them onto a tool result. The live leg and the resume leg
106
+ * each had their own copy, and the resume copy still spread the bag and called `Array.isArray` on it,
107
+ * so a hostile `details` failed the whole leg where the live one had stopped losing anything but the
108
+ * mark. `undefined` = nothing to carry; a caller that must always present a bag folds that itself. */
109
+ export declare function thrownMarksOf(err: unknown): Record<string, unknown> | undefined;
@@ -1,5 +1,5 @@
1
1
  import { describeRejectedValue } from "./tool-search-setting.js";
2
- function isErrorLike(value) {
2
+ export function isErrorLike(value) {
3
3
  try {
4
4
  if (value instanceof Error)
5
5
  return true;
@@ -54,3 +54,25 @@ export function readMember(value, key) {
54
54
  return undefined;
55
55
  }
56
56
  }
57
+ export function readOwnMembers(value) {
58
+ let keys;
59
+ try {
60
+ if (value === null || typeof value !== "object" || Array.isArray(value))
61
+ return undefined;
62
+ keys = Object.keys(value);
63
+ }
64
+ catch {
65
+ return undefined;
66
+ }
67
+ const out = {};
68
+ for (const key of keys)
69
+ out[key] = readMember(value, key);
70
+ return out;
71
+ }
72
+ export function thrownMarksOf(err) {
73
+ const details = readOwnMembers(readMember(err, "details"));
74
+ const errorKind = readMember(err, "errorKind");
75
+ if (typeof errorKind !== "string")
76
+ return details;
77
+ return { ...(details ?? {}), errorKind };
78
+ }
@@ -108,7 +108,7 @@ export const TOOL_CATALOG_ENTRIES = [
108
108
  { id: "task-stop-hands", name: "TaskStop", source: "builtin", definedIn: "src/tools/fs/fs-bash.ts", mountedBy: ["hands"], cards: [], face: { family: "delegate", effect: "write", contract: TASK_STOP_CONTRACT } },
109
109
  { id: "task-output", name: "TaskOutput", source: "builtin", definedIn: "src/core/task-registry.ts", mountedBy: ["delegation"], cards: ["task-output"], face: { family: "delegate", effect: "read", contract: TASK_OUTPUT_CONTRACT } },
110
110
  { id: "task-stop", name: "TaskStop", source: "builtin", definedIn: "src/core/task-registry.ts", mountedBy: ["delegation"], cards: ["task-stop"], face: { family: "delegate", effect: "write", contract: TASK_STOP_CONTRACT } },
111
- { id: "monitor", name: "Monitor", source: "builtin", definedIn: "src/tools/monitor.ts", mountedBy: ["backgroundTasks"], cards: ["monitor-start"], face: { family: "other", effect: "write", contract: c("core.monitor@1"), offloadThresholdChars: 10_000 } },
111
+ { id: "monitor", name: "Monitor", source: "builtin", definedIn: "src/tools/monitor.ts", mountedBy: ["backgroundTasks"], cards: ["monitor-start", "readonly_out_of_root"], face: { family: "other", effect: "write", contract: c("core.monitor@1"), offloadThresholdChars: 10_000 } },
112
112
  { id: "send-message", name: "SendMessage", source: "builtin", definedIn: "src/agents/send-message-tool.ts", mountedBy: ["delegation", "hostInternals", "peerLane"], cards: ["send-message"], face: { family: "protocol", effect: "write", contract: c("core.send_message@1") } },
113
113
  { id: "agent-transcript", name: "AgentTranscript", source: "builtin", definedIn: "src/agents/agent-transcript-tool.ts", mountedBy: ["delegation"], cards: ["agent-transcript"], face: { family: "delegate", effect: "read", contract: c("core.agent_transcript@1") } },
114
114
  { id: "list-agents", name: "ListAgents", source: "builtin", definedIn: "src/agents/list-agents-tool.ts", mountedBy: ["peerLane"], cards: ["list-agents"], face: { family: "protocol", effect: "read", aliases: ["ListPeers"], contract: c("core.list_agents@1") } },
@@ -1,4 +1,4 @@
1
- import { thrownText } from "./thrown-value.js";
1
+ import { readMember, thrownText } from "./thrown-value.js";
2
2
  export function modelIdTail(id) {
3
3
  return id.toLowerCase().split("/").pop() ?? "";
4
4
  }
@@ -207,22 +207,10 @@ export function applyToolModelGate(input) {
207
207
  merged = input.depsSeat === false ? undefined : mergeToolModelGateClasses(input.depsSeat);
208
208
  }
209
209
  catch (e) {
210
- let code;
211
- try {
212
- code = e?.code;
213
- }
214
- catch {
215
- code = undefined;
216
- }
210
+ const code = readMember(e, "code");
217
211
  if (code !== undefined)
218
212
  throw e;
219
- let msg;
220
- try {
221
- msg = thrownText(e);
222
- }
223
- catch {
224
- msg = "[unreportable throw]";
225
- }
213
+ const msg = thrownText(e);
226
214
  throw gateConfigError(`RunnerDeps.toolModelGate could not be read (${msg}) — a config seat whose reads throw is a bad value, refused rather than passed.`);
227
215
  }
228
216
  const raw = input.envRaw;
@@ -1,5 +1,6 @@
1
1
  import { Value } from "typebox/value";
2
2
  import { formatToolError, formatZodValidationError } from "./tool-errors.js";
3
+ import { readMember } from "./thrown-value.js";
3
4
  import { attachToolContract } from "../prompt-assembly/tool-catalog.js";
4
5
  function normalizeContent(ret) {
5
6
  if (typeof ret === "string") {
@@ -115,13 +116,12 @@ export function defineTool(spec, options) {
115
116
  }
116
117
  catch (err) {
117
118
  const wrapped = new Error(formatToolError(err));
118
- if (err !== null && typeof err === "object") {
119
- const src = err;
120
- if (src.details !== undefined)
121
- wrapped.details = src.details;
122
- if (typeof src.errorKind === "string")
123
- wrapped.errorKind = src.errorKind;
124
- }
119
+ const marks = readMember(err, "details");
120
+ if (marks !== undefined)
121
+ wrapped.details = marks;
122
+ const errorKind = readMember(err, "errorKind");
123
+ if (typeof errorKind === "string")
124
+ wrapped.errorKind = errorKind;
125
125
  throw wrapped;
126
126
  }
127
127
  const { content, details, terminate, isError } = normalizeContent(ret);
@@ -405,6 +405,12 @@ export declare function inlineUntrusted(text: string, maxLen?: number): string;
405
405
  * checkpoint-store closed an ESM evaluation cycle (checkpoint-store → tool-policy → tools/fs →
406
406
  * checkpoint-store, a latent TDZ). Both already import this module; one cap, no cycle. */
407
407
  export declare const PROBE_CAUSE_PATH_MAX = 200;
408
+ /** The bloat ceiling on a probe's one-sentence account (`RiskDescriptor.probeReason` /
409
+ * `AskRequest.probeReason`); the neutralizer is the INJECTION ceiling, this is only the LENGTH one. Homed
410
+ * here for the same reason as the constant above: three readers — the shell probe that MINTS the sentence
411
+ * (tools/fs) and must fit every load-bearing part inside this bound, the gate, the persist boundary — and
412
+ * a value import from tools/fs into checkpoint-store closes an ESM cycle. */
413
+ export declare const PROBE_REASON_MAX = 200;
408
414
  /** The one body bound every "reviewer note" relay passes to {@link delimitUntrusted} — the decider's
409
415
  * free text attached to a deny (sync `AskOutcome.reason` and the durable `ResumeOutcome` `reason`
410
416
  * legs alike). A note is steering, not payload: unbounded it can flood the transcript/context the
@@ -601,6 +601,7 @@ function sanitizeLabel(label) {
601
601
  return inlineUntrusted(label, LABEL_MAX);
602
602
  }
603
603
  export const PROBE_CAUSE_PATH_MAX = 200;
604
+ export const PROBE_REASON_MAX = 200;
604
605
  export const REVIEWER_NOTE_MAX_BODY = 2048;
605
606
  export function delimitUntrusted(label, text, maxBody) {
606
607
  return delimitUntrustedWithClip(label, text, maxBody).text;
@@ -118,6 +118,19 @@ export async function workflowRunStoreContract(make, runAssertion = defaultSeque
118
118
  byId.get("d-over").budgetOvershoot.spentTokens = 7;
119
119
  assert.equal((await store.get("d-over")).budgetOvershoot.spentTokens, 1_200);
120
120
  });
121
+ run("the failure CODE reaches the LIST row verbatim, and a run without one keeps the key ABSENT", async () => {
122
+ const store = make();
123
+ const coded = createWorkflowRun({ id: "f-coded", status: "failed", createdAt: 5_000, error: "the body gave up", errorCode: "workflow.agent_blocked" });
124
+ const codeless = createWorkflowRun({ id: "f-codeless", status: "failed", createdAt: 6_000, error: "the body gave up" });
125
+ const ok = createWorkflowRun({ id: "f-ok", status: "completed", createdAt: 7_000 });
126
+ for (const r of [coded, codeless, ok])
127
+ await store.put(r.id, r);
128
+ const byId = new Map((await store.listByScope("tenant-a")).map((s) => [s.id, s]));
129
+ assert.equal(byId.get("f-coded").errorCode, "workflow.agent_blocked");
130
+ assert.equal("errorCode" in byId.get("f-codeless"), false, "a failed run with no code ⇒ the key is OMITTED, not an `unknown` stand-in");
131
+ assert.equal("errorCode" in byId.get("f-ok"), false, "a completed run never carries one ⇒ the key is OMITTED");
132
+ assert.deepEqual(byId.get("f-coded"), summarizeWorkflowRun(coded));
133
+ });
121
134
  run("the TOTAL-TIMEOUT disclosure reaches the LIST row: timeoutInterruption projects (de-aliased), and a run without it keeps the key ABSENT", async () => {
122
135
  const store = make();
123
136
  const seat = { timeoutMs: 600_000, agentsCompleted: 3, agentsFailed: 1, agentsInFlight: 2, approvalWaitedMs: 540_000 };
@@ -46,6 +46,21 @@ export interface WorkflowRunSummary {
46
46
  description?: string;
47
47
  /** Lifecycle status ({@link WorkflowRun.status}): running / completed / failed. */
48
48
  status: WorkflowRunStatus;
49
+ /** The MACHINE code of the failure that ended the run ({@link WorkflowRun.errorCode}) — the same word,
50
+ * in the same vocabulary, that the full run carries; never re-derived and never a second taxonomy.
51
+ * The fourth member of the same family as {@link agentFailures} / {@link budgetOvershoot} /
52
+ * {@link timeoutInterruption}: those three qualify a row that LOOKS clean, and this one says WHY a
53
+ * `failed` row failed. Nothing else on this projection can answer that — {@link status} is `failed`
54
+ * for a refused spawn, a blown budget, a stalled run and a body that simply threw alike, so a list
55
+ * face without this seat must `get` the full run per failed row to say anything at all, which is the
56
+ * N+1 this projection exists to avoid.
57
+ *
58
+ * PRESENT EXACTLY WHEN THE RUN CARRIES ONE, absent otherwise — an absent key reads "no code on the
59
+ * record", never "the run succeeded" and never a `workflow.unknown` stand-in. A `completed` run never
60
+ * carries one (per {@link WorkflowRun.errorCode}), and a `failed` run whose body threw a codeless
61
+ * error with no failed child on the record carries none either. Free of any own attribution rule: the
62
+ * attribution (thrown code → first failed agent → absent) happens once, where the run is written. */
63
+ errorCode?: string;
49
64
  /** Count of agent-runs that ended failed ({@link WorkflowRun.agentFailures}) — present only when > 0.
50
65
  * Lets a list view flag a "completed, with failures" run without an N+1 `get` of the full run. */
51
66
  agentFailures?: number;
@@ -8,6 +8,7 @@ export function summarizeWorkflowRun(run) {
8
8
  ...(run.name !== undefined ? { name: run.name } : {}),
9
9
  ...(run.description !== undefined ? { description: run.description } : {}),
10
10
  status: run.status,
11
+ ...(run.errorCode !== undefined ? { errorCode: run.errorCode } : {}),
11
12
  ...(run.agentFailures !== undefined ? { agentFailures: run.agentFailures } : {}),
12
13
  ...(run.budgetOvershoot !== undefined ? { budgetOvershoot: { ...run.budgetOvershoot } } : {}),
13
14
  ...(run.timeoutInterruption !== undefined ? { timeoutInterruption: { ...run.timeoutInterruption } } : {}),
@@ -2,7 +2,7 @@ import { findToolByName, validateToolArguments } from "../llm/index.js";
2
2
  import { truncateError } from "../../core/tool-errors.js";
3
3
  import { INTERRUPTED_BY_USER_FOR_TOOL_USE_MARKER, INTERRUPTED_BY_USER_MARKER, } from "../../core/session-reconcile.js";
4
4
  import { resolveAgentCoreStreamFn } from "./runtime-deps.js";
5
- import { thrownText } from "../../core/thrown-value.js";
5
+ import { isErrorLike, readMember, thrownMarksOf, thrownText } from "../../core/thrown-value.js";
6
6
  function appendTextDeltaToAssistantMessage(message, contentIndex, delta) {
7
7
  const content = [...message.content];
8
8
  const currentContent = content[contentIndex];
@@ -1537,33 +1537,19 @@ function safeAbortDetails(fn) {
1537
1537
  }
1538
1538
  }
1539
1539
  function isAbortSemanticsError(error) {
1540
- try {
1541
- let cursor = error;
1542
- for (let depth = 0; depth < 8 && cursor instanceof Error; depth++) {
1543
- if (cursor.name === "AbortError")
1544
- return true;
1545
- const next = cursor.cause;
1546
- if (next === cursor)
1547
- break;
1548
- cursor = next;
1549
- }
1550
- }
1551
- catch {
1552
- return false;
1540
+ let cursor = error;
1541
+ for (let depth = 0; depth < 8 && isErrorLike(cursor); depth++) {
1542
+ if (readMember(cursor, "name") === "AbortError")
1543
+ return true;
1544
+ const next = readMember(cursor, "cause");
1545
+ if (next === cursor)
1546
+ break;
1547
+ cursor = next;
1553
1548
  }
1554
1549
  return false;
1555
1550
  }
1556
1551
  function createErrorToolResult(message, source) {
1557
- let details = {};
1558
- if (source !== null && typeof source === "object") {
1559
- const marks = source;
1560
- if (marks.details !== null && typeof marks.details === "object" && !Array.isArray(marks.details)) {
1561
- details = { ...marks.details };
1562
- }
1563
- if (typeof marks.errorKind === "string") {
1564
- details.errorKind = marks.errorKind;
1565
- }
1566
- }
1552
+ const details = thrownMarksOf(source) ?? {};
1567
1553
  return {
1568
1554
  content: [{ type: "text", text: truncateError(message) }],
1569
1555
  details,
@@ -9,6 +9,7 @@
9
9
  * does (an option skipped, a chain member missed) is how one face used to vouch for a program another refused.
10
10
  */
11
11
  import type { SegmentTokens } from "./bash-lexer.js";
12
+ import type { AssertAllKeysHandled } from "../../core/ask-origin.js";
12
13
  /** The verdict every unresolvable / out-of-root / unbounded finding ends on. The findings differ in what
13
14
  * they found; the consequence is one consequence, and a copy that drifts reads as a second rule. */
14
15
  export declare const NOT_AUTO_ALLOWED = "\u2014 not auto-allowed";
@@ -170,6 +171,28 @@ export declare function argvReentryUnjudged(pos: ProgramPosition, acceptedTarget
170
171
  */
171
172
  export declare const SHELL_SCAN_DECLINES: readonly ["unreadable_program_position", "stream_fed_operands"];
172
173
  export type ShellScanDecline = (typeof SHELL_SCAN_DECLINES)[number];
174
+ /** The two SEAT TABLES over that set — ROW PER MEMBER behind a compile-time fence, registered in
175
+ * `docs/CLOSED-SETS.md` so `gate:closed-sets` is their runtime twin. The ARGUMENT for the rows is at the
176
+ * seats that read them (`fs-bash.ts`), which DERIVE the `Set` shape their readers take. Rows and not two
177
+ * sets because a derived set beside an empty literal made a third decline SILENTLY asymmetric, in the
178
+ * widening direction; a member with no row now reds `tsc` at both fences with the word spelled out. */
179
+ export declare const BOUNDARY_SEAT_READS_DECLINE: {
180
+ readonly unreadable_program_position: true;
181
+ readonly stream_fed_operands: true;
182
+ };
183
+ /** The fence over the read-boundary seat's table: `never` while every decline has a row. */
184
+ export type BoundarySeatTableCoversEveryDecline = AssertAllKeysHandled<Exclude<ShellScanDecline, keyof typeof BOUNDARY_SEAT_READS_DECLINE>>;
185
+ /** The classify doctrine's table — every row `false` BY THE ARGUMENT (stated at the seat), not by omission. */
186
+ export declare const CLASSIFY_SEAT_READS_DECLINE: {
187
+ readonly unreadable_program_position: false;
188
+ readonly stream_fed_operands: false;
189
+ };
190
+ /** The fence over the classify seat's table. */
191
+ export type ClassifySeatTableCoversEveryDecline = AssertAllKeysHandled<Exclude<ShellScanDecline, keyof typeof CLASSIFY_SEAT_READS_DECLINE>>;
192
+ /** A seat's table in the shape its readers take — the members it answers YES for. DERIVED, so the ROWS are
193
+ * the single source and a set can never say something its table does not. The parameter is TOTAL, not
194
+ * `Partial`: that stops a third seat being written `seatDeclineEvidence({})`, one level up from the fences. */
195
+ export declare function seatDeclineEvidence(table: Record<ShellScanDecline, boolean>): ReadonlySet<ShellScanDecline>;
173
196
  /** Does a SHELL stand where this segment's program would be — as the program itself (`sh -c`), or as a launcher
174
197
  * ROW handing its operands to one (`watch "…"`)? ONE predicate for the re-entry class, so the two spellings
175
198
  * cannot be answered differently; which names are shells is the caller's declaration. Not a decline. */