@sema-agent/core 2.6.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -37,6 +37,9 @@ export interface BackgroundAgentRecord {
37
37
  finalOutput?: string;
38
38
  finalOutputFull?: string;
39
39
  error?: string;
40
+ errorCode?: string;
41
+ errorRetryable?: boolean;
42
+ errorKind?: string;
40
43
  resultIsPartial?: boolean;
41
44
  recentSteps?: SubagentStep[];
42
45
  editedFiles?: SubagentEditedFile[];
@@ -1225,14 +1225,18 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1225
1225
  shellGatedBash = !egressTools.has("Bash") && !irreversibilityTier.has("Bash");
1226
1226
  irreversibilityTier.set("Bash", shellGate === "always" ? "always" : "maybe");
1227
1227
  irreversibleTools.add("Bash");
1228
+ const shellReadBoundary = () => ({
1229
+ roots: [rootCanonical, ...additionalRootsCanonical],
1230
+ ...(handsCwdRef?.current !== undefined ? { cwd: handsCwdRef.current } : {}),
1231
+ });
1228
1232
  if (shellGate === "classify")
1229
- reversibilityProbes.set("Bash", bashReversibilityProbe());
1233
+ reversibilityProbes.set("Bash", bashReversibilityProbe(undefined, shellReadBoundary));
1230
1234
  if (backgroundTaskToolsActive) {
1231
1235
  shellGatedMonitor = !egressTools.has("Monitor") && !irreversibilityTier.has("Monitor");
1232
1236
  irreversibilityTier.set("Monitor", shellGate === "always" ? "always" : "maybe");
1233
1237
  irreversibleTools.add("Monitor");
1234
1238
  if (shellGate === "classify")
1235
- reversibilityProbes.set("Monitor", bashReversibilityProbe());
1239
+ reversibilityProbes.set("Monitor", bashReversibilityProbe(undefined, shellReadBoundary));
1236
1240
  }
1237
1241
  }
1238
1242
  }
@@ -723,6 +723,9 @@ export function settleBackgroundAgentLane(core, id, outcome) {
723
723
  ...(handle.resultFull !== undefined ? { finalOutputFull: handle.resultFull } : {}),
724
724
  ...(handle.resultIsPartial ? { resultIsPartial: true } : {}),
725
725
  ...(handle.error !== undefined ? { error: handle.error } : {}),
726
+ ...(handle.errorCode !== undefined ? { errorCode: handle.errorCode } : {}),
727
+ ...(handle.errorRetryable !== undefined ? { errorRetryable: handle.errorRetryable } : {}),
728
+ ...(handle.errorKind !== undefined ? { errorKind: handle.errorKind } : {}),
726
729
  }, ["parkedCheckpointToken", "parkClaimId", "parkedAt"]);
727
730
  return outcome.status;
728
731
  }
@@ -856,7 +859,22 @@ export function reviveBackgroundAgentLane(core, id, access, abort) {
856
859
  handle.reviveCycle = (handle.reviveCycle ?? 0) + 1;
857
860
  handle.cycleSeq = (handle.cycleSeq ?? 1) + 1;
858
861
  handle.updatedAt = Date.now();
859
- durableAgentWriteLane(handle, { status: "running" }, ["settledAt", "stoppedBy", "finalOutput", "finalOutputFull", "error", "resultIsPartial", "completionId", "summary", "recentSteps", "editedFiles", "usage"]);
862
+ durableAgentWriteLane(handle, { status: "running" }, [
863
+ "settledAt",
864
+ "stoppedBy",
865
+ "finalOutput",
866
+ "finalOutputFull",
867
+ "error",
868
+ "errorCode",
869
+ "errorRetryable",
870
+ "errorKind",
871
+ "resultIsPartial",
872
+ "completionId",
873
+ "summary",
874
+ "recentSteps",
875
+ "editedFiles",
876
+ "usage",
877
+ ]);
860
878
  return { ok: true, cycle: handle.reviveCycle };
861
879
  }
862
880
  export function settleRevivedAgentLane(core, id, cycle, outcome) {
@@ -1019,8 +1037,11 @@ The agent is durably suspended, waiting for an approval decision. It resumes whe
1019
1037
  },
1020
1038
  };
1021
1039
  }
1040
+ const kindClause = row.status === "failed" && row.errorKind !== undefined && row.errorRetryable !== undefined
1041
+ ? ` (error_kind: ${row.errorKind}, retryable: ${row.errorRetryable})`
1042
+ : "";
1022
1043
  const body = `status: ${row.status}
1023
- ${row.error ? `error: ${row.error}
1044
+ ${row.error ? `error: ${row.error}${kindClause}
1024
1045
  ` : ""}${row.finalOutput ? `--- result${row.resultIsPartial ? " (partial — produced before the task was stopped)" : ""} ---
1025
1046
  ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}`;
1026
1047
  return {
@@ -1034,6 +1055,8 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
1034
1055
  ...(row.seq !== undefined ? { seq: row.seq } : {}),
1035
1056
  ...(row.resultIsPartial ? { partial_result: true } : {}),
1036
1057
  ...(row.completionId !== undefined ? { completionId: row.completionId } : {}),
1058
+ ...(row.status === "failed" && row.errorCode !== undefined ? { errorCode: row.errorCode } : {}),
1059
+ ...(row.status === "failed" && row.errorRetryable !== undefined ? { retryable: row.errorRetryable } : {}),
1037
1060
  },
1038
1061
  ...(row.status === "failed" ? { isError: true } : {}),
1039
1062
  };
package/dist/index.d.ts CHANGED
@@ -67,6 +67,7 @@ export { sanitizeUntrustedText, delimitUntrusted, inlineUntrusted } from "./core
67
67
  export { deriveInvariants, checkInvariants } from "./core/property-harness.js";
68
68
  export type { InvariantKind, FunctionContract, Invariant, InvariantViolation, CheckResult, } from "./core/property-harness.js";
69
69
  export { HAND_TOOL_EFFECTS, bashReversibilityProbe, BASH_READONLY_DEFAULT_ALLOW, parseLeadingCommandName, classifyCompoundReadonly, MAX_EDIT_BYTES } from "./tools/fs/index.js";
70
+ export { classifyCompoundReadonlyDetailed, formatOutOfRootReadApprovalOption, type BashReadonlyRootBoundary, type CompoundReadonlyVerdict, } from "./tools/fs/index.js";
70
71
  export { resolveBashTimeoutCaps } from "./tools/fs/index.js";
71
72
  export { InMemoryToolResultStore, OFFLOAD_TOOL_NAME, DEFAULT_TOOL_RESULT_THRESHOLD_CHARS, type ToolResultStore, type ToolResultSlice, } from "./core/tool-result-store.js";
72
73
  export { InMemoryCheckpointStore, CheckpointError, mintCheckpointToken, checkpointVersionOf, CURRENT_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, WALLTIME_CHECKPOINT_VERSION, debitLedger, remainingBudgetMicroUsd, remainingWalltimeMs, winnerFromOutcome, validatePendingSteer, riskSeverity, buildRiskDescriptor, summarizeCheckpoint, type RiskDescriptor, type CheckpointStore, type CheckpointSummary, type Checkpoint, type CheckpointToken, type CheckpointGate, type CheckpointState, type SerializedCheckpointState, type CheckpointFaultMode, type PendingAction, type ResumeOutcome, type ResolvedOutcome, type ReopenReason, type ResolveExpectation, type SafetyAxis, type ResourceLedger, type ResourceLimitReason, } from "./core/checkpoint-store.js";
package/dist/index.js CHANGED
@@ -58,6 +58,7 @@ export { runExecGate } from "./core/exec-gate.js";
58
58
  export { sanitizeUntrustedText, delimitUntrusted, inlineUntrusted } from "./core/untrusted-text.js";
59
59
  export { deriveInvariants, checkInvariants } from "./core/property-harness.js";
60
60
  export { HAND_TOOL_EFFECTS, bashReversibilityProbe, BASH_READONLY_DEFAULT_ALLOW, parseLeadingCommandName, classifyCompoundReadonly, MAX_EDIT_BYTES } from "./tools/fs/index.js";
61
+ export { classifyCompoundReadonlyDetailed, formatOutOfRootReadApprovalOption, } from "./tools/fs/index.js";
61
62
  export { resolveBashTimeoutCaps } from "./tools/fs/index.js";
62
63
  export { InMemoryToolResultStore, OFFLOAD_TOOL_NAME, DEFAULT_TOOL_RESULT_THRESHOLD_CHARS, } from "./core/tool-result-store.js";
63
64
  export { InMemoryCheckpointStore, CheckpointError, mintCheckpointToken, checkpointVersionOf, CURRENT_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, WALLTIME_CHECKPOINT_VERSION, debitLedger, remainingBudgetMicroUsd, remainingWalltimeMs, winnerFromOutcome, validatePendingSteer, riskSeverity, buildRiskDescriptor, summarizeCheckpoint, } from "./core/checkpoint-store.js";
@@ -5,4 +5,16 @@ export declare function parseLeadingCommandName(command: string): {
5
5
  reject: string;
6
6
  };
7
7
  export declare function coarseReadonlyCheck(command: string, allow: ReadonlySet<string>): string | undefined;
8
- export declare function classifyCompoundReadonly(command: string, allow: ReadonlySet<string>): string | undefined;
8
+ export interface BashReadonlyRootBoundary {
9
+ roots: readonly string[];
10
+ cwd?: string;
11
+ homeDir?: string;
12
+ }
13
+ export interface CompoundReadonlyVerdict {
14
+ reason?: string;
15
+ outOfRootRead?: true;
16
+ outOfRootPaths?: readonly string[];
17
+ }
18
+ export declare function formatOutOfRootReadApprovalOption(directory: string): string;
19
+ export declare function classifyCompoundReadonlyDetailed(command: string, allow: ReadonlySet<string>, boundary?: BashReadonlyRootBoundary): CompoundReadonlyVerdict;
20
+ export declare function classifyCompoundReadonly(command: string, allow: ReadonlySet<string>, boundary?: BashReadonlyRootBoundary): string | undefined;
@@ -1,4 +1,4 @@
1
- import { isBlockedDevicePath, normalizeAbsPathLexically } from "./safety.js";
1
+ import { isBlockedDevicePath, normalizeAbsPathLexically, withinAnyRoot } from "./safety.js";
2
2
  export const BASH_READONLY_DEFAULT_ALLOW = [
3
3
  "ls", "cat", "head", "tail", "wc", "pwd", "echo", "whoami", "uname",
4
4
  "grep", "cut", "tr", "basename", "dirname", "stat", "du", "df", "which",
@@ -49,12 +49,102 @@ function foldQuoteRemovalToken(tok) {
49
49
  }
50
50
  return out;
51
51
  }
52
- export function classifyCompoundReadonly(command, allow) {
52
+ export function formatOutOfRootReadApprovalOption(directory) {
53
+ const trimmed = directory.replace(/[/\\]+$/, "");
54
+ const cut = Math.max(trimmed.lastIndexOf("/"), trimmed.lastIndexOf("\\"));
55
+ const leaf = cut >= 0 ? trimmed.slice(cut + 1) : trimmed;
56
+ return `Yes, allow reading from ${leaf || directory}/ from this project`;
57
+ }
58
+ const NO_PATH_OPERAND_COMMANDS = new Set(["pwd", "echo", "whoami", "uname", "which", "tr", "basename", "dirname"]);
59
+ function isAbsolutePathToken(p) {
60
+ return p.startsWith("/") || /^[A-Za-z]:[\\/]/.test(p);
61
+ }
62
+ function isPathShapedToken(tok) {
63
+ return tok.includes("/") || tok.startsWith("~") || tok === "." || tok === "..";
64
+ }
65
+ function normalizeAbsPathLexicalEitherFamily(p) {
66
+ const s = p.replace(/\\/g, "/");
67
+ return /^[A-Za-z]:\//.test(s) ? s.slice(0, 2) + normalizeAbsPathLexically(s.slice(2)) : normalizeAbsPathLexically(s);
68
+ }
69
+ function resolveOperandLexically(base, operand, homeDir) {
70
+ let raw = operand;
71
+ if (raw === "~" || raw.startsWith("~/")) {
72
+ if (homeDir === undefined || !isAbsolutePathToken(homeDir))
73
+ return undefined;
74
+ raw = raw === "~" ? homeDir : `${homeDir.replace(/[/\\]+$/, "")}/${raw.slice(2)}`;
75
+ }
76
+ else if (raw.startsWith("~")) {
77
+ return undefined;
78
+ }
79
+ if (isAbsolutePathToken(raw))
80
+ return normalizeAbsPathLexicalEitherFamily(raw);
81
+ if (base === undefined || !isAbsolutePathToken(base))
82
+ return undefined;
83
+ return normalizeAbsPathLexicalEitherFamily(`${base.replace(/[/\\]+$/, "")}/${raw}`);
84
+ }
85
+ function collectSegmentBoundaryFindings(toks, boundary) {
86
+ const name = toks[0];
87
+ if (NO_PATH_OPERAND_COMMANDS.has(name))
88
+ return [];
89
+ const args = toks.slice(1);
90
+ const findings = [];
91
+ const candidates = [];
92
+ if (name === "cd") {
93
+ const target = args.find((t) => !t.startsWith("-") || t === "-");
94
+ if (target === undefined) {
95
+ return [{ kind: "unresolvable", reason: '`cd` with no argument targets the home directory, which cannot be checked against the allowed directories — not auto-allowed' }];
96
+ }
97
+ if (target === "-") {
98
+ return [{ kind: "unresolvable", reason: '`cd -` targets the previous working directory, which cannot be resolved statically — not auto-allowed' }];
99
+ }
100
+ candidates.push(target);
101
+ }
102
+ else {
103
+ const patternSuppliedByFlag = name === "grep" && args.some((t) => t === "-e" || t.startsWith("-e") || t === "-f" || t.startsWith("-f") || t.startsWith("--regexp") || t.startsWith("--file"));
104
+ let sawOperand = false;
105
+ for (let k = 0; k < args.length; k++) {
106
+ const t = args[k];
107
+ if (name === "cut" && (t === "-d" || t === "--delimiter" || t === "--output-delimiter")) {
108
+ k++;
109
+ continue;
110
+ }
111
+ if (t.startsWith("-") && t !== "-") {
112
+ const eq = t.indexOf("=");
113
+ const value = eq > 0 ? t.slice(eq + 1) : "";
114
+ if (value.length > 0 && isAbsolutePathToken(value))
115
+ candidates.push(value);
116
+ continue;
117
+ }
118
+ if (t === "-")
119
+ continue;
120
+ const isGrepPatternSlot = name === "grep" && !patternSuppliedByFlag && !sawOperand;
121
+ sawOperand = true;
122
+ if (isGrepPatternSlot)
123
+ continue;
124
+ if (isPathShapedToken(t))
125
+ candidates.push(t);
126
+ }
127
+ }
128
+ for (const candidate of candidates) {
129
+ const resolved = resolveOperandLexically(boundary.cwd ?? boundary.roots[0], candidate, boundary.homeDir);
130
+ if (resolved === undefined) {
131
+ findings.push({
132
+ kind: "unresolvable",
133
+ reason: `"${name}" names the path "${candidate}", which cannot be resolved statically — not auto-allowed`,
134
+ });
135
+ continue;
136
+ }
137
+ if (!withinAnyRoot(boundary.roots, resolved))
138
+ findings.push({ kind: "outside", command: name, path: resolved });
139
+ }
140
+ return findings;
141
+ }
142
+ export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
53
143
  const trimmed = command.trim();
54
144
  if (!trimmed)
55
- return "empty command";
145
+ return { reason: "empty command" };
56
146
  if (SHELL_SEGMENT_HARD_REJECT.test(trimmed)) {
57
- return "redirection, command/variable substitution, subshells, escapes, and line breaks are not allowed";
147
+ return { reason: "redirection, command/variable substitution, subshells, escapes, and line breaks are not allowed" };
58
148
  }
59
149
  const source = trimmed.endsWith(";") ? trimmed.slice(0, -1) : trimmed;
60
150
  const segments = [];
@@ -70,7 +160,7 @@ export function classifyCompoundReadonly(command, allow) {
70
160
  i++;
71
161
  }
72
162
  else
73
- return "`&` backgrounding is not allowed — a backgrounded process outlives the command";
163
+ return { reason: "`&` backgrounding is not allowed — a backgrounded process outlives the command" };
74
164
  }
75
165
  else if (c === "|") {
76
166
  if (source[i + 1] === "|") {
@@ -97,7 +187,7 @@ export function classifyCompoundReadonly(command, allow) {
97
187
  for (const segment of segments) {
98
188
  const reason = coarseReadonlyCheck(segment, allow);
99
189
  if (reason !== undefined)
100
- return reason;
190
+ return { reason };
101
191
  }
102
192
  const HEAD_AUTO_ALLOW_MAX = 1_000_000;
103
193
  const headBoundIsSmall = (name, toks) => {
@@ -128,11 +218,13 @@ export function classifyCompoundReadonly(command, allow) {
128
218
  return byteBound;
129
219
  };
130
220
  const STDIN_FILE_FLOOR = { cat: 1, grep: 2, head: 1, tail: 1, wc: 1, cut: 1, tr: Infinity };
221
+ const foldedSegments = [];
131
222
  for (let si = 0; si < segments.length; si++) {
132
223
  const toks = segments[si].trim().split(/\s+/).filter((t) => t.length > 0)
133
224
  .map(foldQuoteRemovalToken);
134
225
  if (toks.length === 0)
135
226
  continue;
227
+ foldedSegments.push(toks);
136
228
  const name = toks[0];
137
229
  const nonOption = toks.slice(1).filter((t) => !t.startsWith("-"));
138
230
  if (!(pipeFed[si] ?? false)) {
@@ -165,21 +257,44 @@ export function classifyCompoundReadonly(command, allow) {
165
257
  }
166
258
  }
167
259
  if (floor !== undefined && hasStdinDash) {
168
- return `"${name}" reads stdin via an explicit "-" argument and would block until the tool timeout — not auto-allowed`;
260
+ return { reason: `"${name}" reads stdin via an explicit "-" argument and would block until the tool timeout — not auto-allowed` };
169
261
  }
170
262
  if (floor !== undefined && nonOption.length < floor) {
171
- return `"${name}" with no file argument reads stdin and would block until the tool timeout — not auto-allowed`;
263
+ return { reason: `"${name}" with no file argument reads stdin and would block until the tool timeout — not auto-allowed` };
172
264
  }
173
265
  }
174
266
  if (name === "tail" && toks.slice(1).some((t) => t === "--follow" || t.startsWith("--follow=") || /^[-+][^\s]*[fF]/.test(t))) {
175
- return "`tail` in follow mode never terminates — not auto-allowed";
267
+ return { reason: "`tail` in follow mode never terminates — not auto-allowed" };
176
268
  }
177
269
  const GENERATOR_DEVICES = new Set(["/dev/zero", "/dev/random", "/dev/urandom", "/dev/full"]);
178
270
  const deviceArgs = toks.slice(1).filter((t) => !t.startsWith("-")).map(normalizeAbsPathLexically).filter(isBlockedDevicePath);
179
271
  const rescuedByHead = headBoundIsSmall(name, toks) && deviceArgs.every((d) => GENERATOR_DEVICES.has(d));
180
272
  if (!rescuedByHead && deviceArgs.length > 0) {
181
- return "reads a device/special file that is either unbounded (/dev/zero, /dev/stdin, /proc/<pid>/fd/0, … — blocks the pipeline until the tool timeout) or process-private (/proc/<pid>/environ, /proc/<pid>/mem, …) — not auto-allowed";
273
+ return { reason: "reads a device/special file that is either unbounded (/dev/zero, /dev/stdin, /proc/<pid>/fd/0, … — blocks the pipeline until the tool timeout) or process-private (/proc/<pid>/environ, /proc/<pid>/mem, …) — not auto-allowed" };
182
274
  }
183
275
  }
184
- return undefined;
276
+ if (boundary !== undefined) {
277
+ const outside = [];
278
+ for (const toks of foldedSegments) {
279
+ for (const finding of collectSegmentBoundaryFindings(toks, boundary)) {
280
+ if (finding.kind === "unresolvable")
281
+ return { reason: finding.reason };
282
+ if (!outside.some((o) => o.path === finding.path))
283
+ outside.push(finding);
284
+ }
285
+ }
286
+ if (outside.length > 0) {
287
+ const paths = outside.map((o) => `"${o.path}"`).join(", ");
288
+ const allowed = boundary.roots.length > 0 ? boundary.roots.join(", ") : "(none)";
289
+ return {
290
+ reason: `"${outside[0].command}" reads ${paths}, outside the allowed directories for this session: ${allowed} — not auto-allowed`,
291
+ outOfRootRead: true,
292
+ outOfRootPaths: outside.map((o) => o.path),
293
+ };
294
+ }
295
+ }
296
+ return {};
297
+ }
298
+ export function classifyCompoundReadonly(command, allow, boundary) {
299
+ return classifyCompoundReadonlyDetailed(command, allow, boundary).reason;
185
300
  }
@@ -1,7 +1,8 @@
1
1
  import type { AgentTool, ExecutionEnv } from "../../internal/harness-types.js";
2
2
  import { type TaskRegistry } from "../../core/task-registry.js";
3
3
  import { type CwdRef } from "./fs-shared.js";
4
- export declare function bashReversibilityProbe(allow?: readonly string[]): (args: unknown) => {
4
+ import { type BashReadonlyRootBoundary } from "./bash-readonly-classifier.js";
5
+ export declare function bashReversibilityProbe(allow?: readonly string[], boundary?: BashReadonlyRootBoundary | (() => BashReadonlyRootBoundary | undefined)): (args: unknown) => {
5
6
  reversible: boolean;
6
7
  };
7
8
  export interface ExecClampOption {
@@ -9,13 +9,14 @@ import { imageMagicMatches, withinAnyRoot } from "./safety.js";
9
9
  import { ghRateLimitHint } from "./gh-rate-limit.js";
10
10
  import { resolveBashTimeoutCaps, bashTimeoutCapsSec, bashMaxOutputChars, clipShellOutput, writeShellOverflowFile, shellRecoveryHint, CWD_SENTINEL, } from "./fs-shared.js";
11
11
  import { BASH_READONLY_DEFAULT_ALLOW, coarseReadonlyCheck, classifyCompoundReadonly } from "./bash-readonly-classifier.js";
12
- export function bashReversibilityProbe(allow) {
12
+ export function bashReversibilityProbe(allow, boundary) {
13
13
  const allowSet = new Set(allow ?? BASH_READONLY_DEFAULT_ALLOW);
14
14
  return (args) => {
15
15
  const command = args?.command;
16
16
  if (typeof command !== "string")
17
17
  return { reversible: false };
18
- return { reversible: classifyCompoundReadonly(command, allowSet) === undefined };
18
+ const resolved = typeof boundary === "function" ? boundary() : boundary;
19
+ return { reversible: classifyCompoundReadonly(command, allowSet, resolved) === undefined };
19
20
  };
20
21
  }
21
22
  const EXIT1_INTERPRETATION = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "2.6.0",
3
+ "version": "2.7.0",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",