@sema-agent/core 2.7.0 → 2.8.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.
@@ -1,6 +1,7 @@
1
1
  import { Type } from "typebox";
2
2
  import type { Runner } from "../core/runner/runtask.js";
3
3
  import type { TaskNotificationPayload } from "../core/task-notification.js";
4
+ import { type ToolCtxEnricher } from "../core/tools.js";
4
5
  import { SubagentRetainLedger } from "./retain-ledger.js";
5
6
  import { type SubagentSteerHandle } from "./subagent.js";
6
7
  export declare const SEND_MESSAGE_TOOL_NAME = "SendMessage";
@@ -35,6 +36,7 @@ export interface SendMessageToolOptions {
35
36
  content: string;
36
37
  details?: unknown;
37
38
  }>;
39
+ enrichCtx?: ToolCtxEnricher;
38
40
  }
39
41
  export declare const SEND_MESSAGE_SUMMARY_MAX = 200;
40
42
  export declare function clipSendMessageSummary(raw: string): string;
@@ -554,5 +554,5 @@ export function createSendMessageTool(opts) {
554
554
  }
555
555
  });
556
556
  },
557
- });
557
+ }, opts.enrichCtx !== undefined ? { enrichCtx: opts.enrichCtx } : {});
558
558
  }
@@ -1278,6 +1278,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1278
1278
  ...(internals?.parentRetainLedger !== undefined ? { siblingRetain: internals.parentRetainLedger } : {}),
1279
1279
  ...(internals?.parentTaskId !== undefined ? { parentTaskId: internals.parentTaskId } : {}),
1280
1280
  ...(internals?.parentSessionId !== undefined ? { parentSessionId: internals.parentSessionId } : {}),
1281
+ enrichCtx: enrichSpecToolCtx,
1281
1282
  ...(deps.rosterStore !== undefined ? { roster: deps.rosterStore } : {}),
1282
1283
  ...(deps.onBackgroundChildEvent ? { onBackgroundChildEvent: deps.onBackgroundChildEvent } : {}),
1283
1284
  ...(deps.backgroundAgentStore !== undefined ? { agentStore: deps.backgroundAgentStore } : {}),
@@ -1,3 +1,5 @@
1
1
  import type { FileSnapshotStore } from "../file-snapshot-store.js";
2
2
  import { type ContractAssertionRunner } from "./contract-harness.js";
3
- export declare function fileSnapshotStoreContract(make: () => FileSnapshotStore, runAssertion?: ContractAssertionRunner): Promise<void>;
3
+ export declare function fileSnapshotStoreContract(make: () => FileSnapshotStore, runAssertion?: ContractAssertionRunner, options?: {
4
+ blobGc?: "immediate" | "eventual";
5
+ }): Promise<void>;
@@ -4,8 +4,9 @@ import { beginContract } from "./contract-harness.js";
4
4
  const bytes = (s) => new TextEncoder().encode(s);
5
5
  const sha256 = (s) => createHash("sha256").update(bytes(s)).digest("hex");
6
6
  const srcBlob = async (hash) => hash === sha256("alpha") ? bytes("alpha") : hash === sha256("beta") ? bytes("beta") : undefined;
7
- export async function fileSnapshotStoreContract(make, runAssertion) {
7
+ export async function fileSnapshotStoreContract(make, runAssertion, options) {
8
8
  const { run, settle } = beginContract(runAssertion);
9
+ const blobGc = options?.blobGc ?? "immediate";
9
10
  run("kit prerequisites: exportManifest/getBlob/putBlob/importManifest are implemented (REQUIRED by this kit)", async () => {
10
11
  const probe = make();
11
12
  const missing = ["exportManifest", "getBlob", "putBlob", "importManifest"].filter((m) => typeof probe[m] !== "function");
@@ -115,12 +116,19 @@ export async function fileSnapshotStoreContract(make, runAssertion) {
115
116
  assert.deepEqual(await store.listKeys("sc"), ["k1"]);
116
117
  assert.deepEqual([...(await store.getBlob(sha256("alpha")))], [...bytes("alpha")]);
117
118
  });
118
- run("reap keep-nothing drops the key and GCs the now-unreferenced blobs", async () => {
119
+ run("reap keep-nothing drops the key IMMEDIATELY (every backend)", async () => {
119
120
  const store = make();
120
121
  await store.importManifest("sc", "k1", new Map([["a.txt", sha256("alpha")]]), srcBlob);
121
122
  assert.equal(await store.reap("sc", []), 1);
122
123
  assert.equal(await store.has("sc", "k1"), false);
123
- assert.equal(await store.getBlob(sha256("alpha")), undefined);
124
124
  });
125
+ if (blobGc === "immediate") {
126
+ run("reap keep-nothing GCs the now-unreferenced blob bytes immediately (blobGc: immediate)", async () => {
127
+ const store = make();
128
+ await store.importManifest("sc", "k1", new Map([["a.txt", sha256("alpha")]]), srcBlob);
129
+ assert.equal(await store.reap("sc", []), 1);
130
+ assert.equal(await store.getBlob(sha256("alpha")), undefined);
131
+ });
132
+ }
125
133
  await settle();
126
134
  }
@@ -1,6 +1,6 @@
1
1
  import type { TSchema } from "typebox";
2
2
  import type { AgentTool } from "../internal/harness.js";
3
- import type { ToolSpec } from "./types.js";
3
+ import type { ToolExecuteContext, ToolSpec } from "./types.js";
4
4
  export declare function errorResult(text: string, details?: unknown): {
5
5
  content: string;
6
6
  isError: true;
@@ -8,4 +8,8 @@ export declare function errorResult(text: string, details?: unknown): {
8
8
  };
9
9
  export declare function isDefineToolProduct(x: unknown): x is AgentTool;
10
10
  export declare function stampDefineToolBrand<T extends object>(tool: T): T;
11
- export declare function defineTool<TParams extends TSchema = TSchema>(spec: ToolSpec<TParams>): AgentTool<TParams>;
11
+ export type ToolCtxEnricher = (base: ToolExecuteContext) => ToolExecuteContext;
12
+ export interface DefineToolOptions {
13
+ enrichCtx?: ToolCtxEnricher;
14
+ }
15
+ export declare function defineTool<TParams extends TSchema = TSchema>(spec: ToolSpec<TParams>, options?: DefineToolOptions): AgentTool<TParams>;
@@ -33,7 +33,7 @@ export function stampDefineToolBrand(tool) {
33
33
  Object.defineProperty(tool, DEFINE_TOOL_BRAND, { value: true, enumerable: false });
34
34
  return tool;
35
35
  }
36
- export function defineTool(spec) {
36
+ export function defineTool(spec, options) {
37
37
  const executionMode = spec.executionMode ?? (spec.effect === "read" ? "parallel" : "sequential");
38
38
  const tool = {
39
39
  name: spec.name,
@@ -60,7 +60,8 @@ export function defineTool(spec) {
60
60
  }
61
61
  let ret;
62
62
  try {
63
- ret = await spec.execute(params, { toolCallId, signal });
63
+ const baseCtx = { toolCallId, signal };
64
+ ret = await spec.execute(params, options?.enrichCtx ? { ...options.enrichCtx(baseCtx), toolCallId, signal } : baseCtx);
64
65
  }
65
66
  catch (err) {
66
67
  throw new Error(formatToolError(err));
package/dist/index.d.ts CHANGED
@@ -69,7 +69,7 @@ export type { InvariantKind, FunctionContract, Invariant, InvariantViolation, Ch
69
69
  export { HAND_TOOL_EFFECTS, bashReversibilityProbe, BASH_READONLY_DEFAULT_ALLOW, parseLeadingCommandName, classifyCompoundReadonly, MAX_EDIT_BYTES } from "./tools/fs/index.js";
70
70
  export { classifyCompoundReadonlyDetailed, formatOutOfRootReadApprovalOption, type BashReadonlyRootBoundary, type CompoundReadonlyVerdict, } from "./tools/fs/index.js";
71
71
  export { resolveBashTimeoutCaps } from "./tools/fs/index.js";
72
- export { InMemoryToolResultStore, OFFLOAD_TOOL_NAME, DEFAULT_TOOL_RESULT_THRESHOLD_CHARS, type ToolResultStore, type ToolResultSlice, } from "./core/tool-result-store.js";
72
+ export { InMemoryToolResultStore, OFFLOAD_TOOL_NAME, DEFAULT_TOOL_RESULT_THRESHOLD_CHARS, assertSafeToolResultRef, type ToolResultStore, type ToolResultSlice, } from "./core/tool-result-store.js";
73
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";
74
74
  export { InMemoryFileSnapshotStore, DEFAULT_SNAPSHOT_BOUNDS } from "./core/file-snapshot-store.js";
75
75
  export { captureManifest, applyManifest } from "./core/file-snapshot-store.js";
package/dist/index.js CHANGED
@@ -60,7 +60,7 @@ 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
61
  export { classifyCompoundReadonlyDetailed, formatOutOfRootReadApprovalOption, } from "./tools/fs/index.js";
62
62
  export { resolveBashTimeoutCaps } from "./tools/fs/index.js";
63
- export { InMemoryToolResultStore, OFFLOAD_TOOL_NAME, DEFAULT_TOOL_RESULT_THRESHOLD_CHARS, } from "./core/tool-result-store.js";
63
+ export { InMemoryToolResultStore, OFFLOAD_TOOL_NAME, DEFAULT_TOOL_RESULT_THRESHOLD_CHARS, assertSafeToolResultRef, } from "./core/tool-result-store.js";
64
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";
65
65
  export { InMemoryFileSnapshotStore, DEFAULT_SNAPSHOT_BOUNDS } from "./core/file-snapshot-store.js";
66
66
  export { captureManifest, applyManifest } from "./core/file-snapshot-store.js";
@@ -17,4 +17,5 @@ export interface CompoundReadonlyVerdict {
17
17
  }
18
18
  export declare function formatOutOfRootReadApprovalOption(directory: string): string;
19
19
  export declare function classifyCompoundReadonlyDetailed(command: string, allow: ReadonlySet<string>, boundary?: BashReadonlyRootBoundary): CompoundReadonlyVerdict;
20
+ export declare function classifySimpleCommandReadBoundary(command: string, boundary: BashReadonlyRootBoundary): CompoundReadonlyVerdict;
20
21
  export declare function classifyCompoundReadonly(command: string, allow: ReadonlySet<string>, boundary?: BashReadonlyRootBoundary): string | undefined;
@@ -82,6 +82,13 @@ function resolveOperandLexically(base, operand, homeDir) {
82
82
  return undefined;
83
83
  return normalizeAbsPathLexicalEitherFamily(`${base.replace(/[/\\]+$/, "")}/${raw}`);
84
84
  }
85
+ function tokenizeSegment(segment) {
86
+ return segment
87
+ .trim()
88
+ .split(/\s+/)
89
+ .filter((t) => t.length > 0)
90
+ .map(foldQuoteRemovalToken);
91
+ }
85
92
  function collectSegmentBoundaryFindings(toks, boundary) {
86
93
  const name = toks[0];
87
94
  if (NO_PATH_OPERAND_COMMANDS.has(name))
@@ -220,8 +227,7 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
220
227
  const STDIN_FILE_FLOOR = { cat: 1, grep: 2, head: 1, tail: 1, wc: 1, cut: 1, tr: Infinity };
221
228
  const foldedSegments = [];
222
229
  for (let si = 0; si < segments.length; si++) {
223
- const toks = segments[si].trim().split(/\s+/).filter((t) => t.length > 0)
224
- .map(foldQuoteRemovalToken);
230
+ const toks = tokenizeSegment(segments[si]);
225
231
  if (toks.length === 0)
226
232
  continue;
227
233
  foldedSegments.push(toks);
@@ -273,27 +279,35 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
273
279
  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" };
274
280
  }
275
281
  }
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
- };
282
+ if (boundary !== undefined)
283
+ return evaluateReadBoundary(foldedSegments, boundary);
284
+ return {};
285
+ }
286
+ function evaluateReadBoundary(foldedSegments, boundary) {
287
+ const outside = [];
288
+ for (const toks of foldedSegments) {
289
+ for (const finding of collectSegmentBoundaryFindings(toks, boundary)) {
290
+ if (finding.kind === "unresolvable")
291
+ return { reason: finding.reason };
292
+ if (!outside.some((o) => o.path === finding.path))
293
+ outside.push(finding);
294
294
  }
295
295
  }
296
- return {};
296
+ if (outside.length === 0)
297
+ return {};
298
+ const paths = outside.map((o) => `"${o.path}"`).join(", ");
299
+ const allowed = boundary.roots.length > 0 ? boundary.roots.join(", ") : "(none)";
300
+ return {
301
+ reason: `"${outside[0].command}" reads ${paths}, outside the allowed directories for this session: ${allowed} — not auto-allowed`,
302
+ outOfRootRead: true,
303
+ outOfRootPaths: outside.map((o) => o.path),
304
+ };
305
+ }
306
+ export function classifySimpleCommandReadBoundary(command, boundary) {
307
+ const toks = tokenizeSegment(command);
308
+ if (toks.length === 0)
309
+ return {};
310
+ return evaluateReadBoundary([toks], boundary);
297
311
  }
298
312
  export function classifyCompoundReadonly(command, allow, boundary) {
299
313
  return classifyCompoundReadonlyDetailed(command, allow, boundary).reason;
@@ -31,9 +31,10 @@ export declare function createBashTool(env: ExecutionEnv, rootCanonical: string,
31
31
  bashDefaultTimeoutMs?: number;
32
32
  bashMaxTimeoutMs?: number;
33
33
  }): AgentTool;
34
- export declare function createBashReadonlyTool(env: ExecutionEnv, rootCanonical: string, allow: ReadonlySet<string>, execClamp?: ExecClampOption, timeoutOpts?: {
34
+ export declare function createBashReadonlyTool(env: ExecutionEnv, rootCanonical: string, allow: ReadonlySet<string>, execClamp?: ExecClampOption, opts?: {
35
35
  bashDefaultTimeoutMs?: number;
36
36
  bashMaxTimeoutMs?: number;
37
+ additionalRoots?: readonly string[];
37
38
  }): AgentTool;
38
39
  export declare function createEnvTaskOutputTool(env: ExecutionEnv): AgentTool;
39
40
  export declare function createEnvTaskStopTool(env: ExecutionEnv, registry?: TaskRegistry): AgentTool;
@@ -8,7 +8,7 @@ import { MCP_IMAGE_MAX_BASE64 } from "../../core/mcp.js";
8
8
  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
- import { BASH_READONLY_DEFAULT_ALLOW, coarseReadonlyCheck, classifyCompoundReadonly } from "./bash-readonly-classifier.js";
11
+ import { BASH_READONLY_DEFAULT_ALLOW, coarseReadonlyCheck, classifyCompoundReadonly, classifySimpleCommandReadBoundary, } from "./bash-readonly-classifier.js";
12
12
  export function bashReversibilityProbe(allow, boundary) {
13
13
  const allowSet = new Set(allow ?? BASH_READONLY_DEFAULT_ALLOW);
14
14
  return (args) => {
@@ -627,16 +627,20 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
627
627
  },
628
628
  });
629
629
  }
630
- export function createBashReadonlyTool(env, rootCanonical, allow, execClamp, timeoutOpts) {
631
- const timeoutCaps = resolveBashTimeoutCaps(timeoutOpts);
630
+ export function createBashReadonlyTool(env, rootCanonical, allow, execClamp, opts) {
631
+ const timeoutCaps = resolveBashTimeoutCaps(opts);
632
632
  const timeoutCapsSecView = bashTimeoutCapsSec(timeoutCaps);
633
633
  const sample = [...allow].slice(0, 6).join(", ");
634
+ const readRoots = [rootCanonical, ...(opts?.additionalRoots ?? [])];
635
+ const mintedOutputFiles = new Set();
634
636
  return defineTool({
635
637
  name: "Bash",
636
638
  contract: { contractId: "core.bash_readonly@1", implementationRevision: "1" },
637
639
  description: `Run a SINGLE read-only inspection command (e.g. ${sample}, …) and return its output. Shell ` +
638
640
  "operators (pipes, redirects, ;, &&, command substitution, subshells) are rejected and only " +
639
- "allowlisted commands run. Still subject to the deployment's approval policy.",
641
+ "allowlisted commands run. Reads are confined to the workspace root(s): a path outside them is " +
642
+ "refused, as is one that cannot be resolved statically (e.g. a `~` path). Still subject to the " +
643
+ "deployment's approval policy.",
640
644
  parameters: Type.Object({
641
645
  command: Type.String({ description: "A single allowlisted read-only command (no shell operators)." }),
642
646
  timeout: Type.Optional(Type.Number({ description: `Timeout in milliseconds (default ${timeoutCaps.defaultMs}, max ${timeoutCaps.maxMs}; requests above the max are capped to it).` })),
@@ -647,7 +651,17 @@ export function createBashReadonlyTool(env, rootCanonical, allow, execClamp, tim
647
651
  const reason = coarseReadonlyCheck(command, allow);
648
652
  if (reason)
649
653
  return errorResult(`Error (Bash): ${reason}`);
650
- return runShell(env, rootCanonical, "Bash", command, msTimeoutToRequestedSec(timeout), timeoutCapsSecView, ctx.signal, undefined, undefined, execClamp, ctx.toolCallId, true);
654
+ const boundary = classifySimpleCommandReadBoundary(command, { roots: [...readRoots, ...mintedOutputFiles], cwd: rootCanonical });
655
+ if (boundary.reason !== undefined) {
656
+ return errorResult(`Error (Bash): ${boundary.reason}. bash_readonly is confined to the workspace roots; it has no approval path, so the call is refused rather than escalated.`, { code: "readonly_out_of_root", paths: boundary.outOfRootPaths ?? [] });
657
+ }
658
+ const result = await runShell(env, rootCanonical, "Bash", command, msTimeoutToRequestedSec(timeout), timeoutCapsSecView, ctx.signal, undefined, undefined, execClamp, ctx.toolCallId, true);
659
+ if (typeof result !== "string") {
660
+ const outputFile = result.details.output_file;
661
+ if (typeof outputFile === "string")
662
+ mintedOutputFiles.add(outputFile);
663
+ }
664
+ return result;
651
665
  },
652
666
  });
653
667
  }
@@ -38,6 +38,7 @@ export function createHandsToolkit(env, readFileState, rootCanonical, opts = {})
38
38
  ? createBashReadonlyTool(env, rootCanonical, new Set(bashReadonlyAllow ?? BASH_READONLY_DEFAULT_ALLOW), opts.execClamp, {
39
39
  ...(opts.bashDefaultTimeoutMs !== undefined ? { bashDefaultTimeoutMs: opts.bashDefaultTimeoutMs } : {}),
40
40
  ...(opts.bashMaxTimeoutMs !== undefined ? { bashMaxTimeoutMs: opts.bashMaxTimeoutMs } : {}),
41
+ ...(additionalRoots !== undefined ? { additionalRoots } : {}),
41
42
  })
42
43
  : createBashTool(env, rootCanonical, commitCoAuthor, cwdRef, {
43
44
  taskRegistry: opts.taskRegistry,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "2.7.0",
3
+ "version": "2.8.0",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",