@sema-agent/core 7.17.4 → 7.17.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 (35) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/core/runner/prepare-hands-readface.js +10 -8
  3. package/dist/core/tool-catalog-entries.js +1 -1
  4. package/dist/tools/fs/bash-readonly-classifier.d.ts +18 -5
  5. package/dist/tools/fs/bash-readonly-classifier.js +180 -61
  6. package/dist/tools/fs/fs-bash.d.ts +28 -0
  7. package/dist/tools/fs/fs-bash.js +52 -10
  8. package/dist/tools/fs/index.d.ts +13 -0
  9. package/dist/tools/fs/index.js +1 -0
  10. package/package.json +3 -2
  11. package/wire-changes/core@7.10.0.json +739 -0
  12. package/wire-changes/core@7.11.0.json +685 -0
  13. package/wire-changes/core@7.11.1.json +112 -0
  14. package/wire-changes/core@7.11.2.json +73 -0
  15. package/wire-changes/core@7.12.0.json +356 -0
  16. package/wire-changes/core@7.13.0.json +199 -0
  17. package/wire-changes/core@7.14.0.json +578 -0
  18. package/wire-changes/core@7.15.0.json +174 -0
  19. package/wire-changes/core@7.16.0.json +132 -0
  20. package/wire-changes/core@7.17.0.json +1621 -0
  21. package/wire-changes/core@7.17.1.json +139 -0
  22. package/wire-changes/core@7.17.2.json +65 -0
  23. package/wire-changes/core@7.17.3.json +44 -0
  24. package/wire-changes/core@7.17.4.json +52 -0
  25. package/wire-changes/core@7.17.5.json +67 -0
  26. package/wire-changes/core@7.5.0.json +1063 -0
  27. package/wire-changes/core@7.5.1.json +252 -0
  28. package/wire-changes/core@7.5.2.json +418 -0
  29. package/wire-changes/core@7.6.0.json +1243 -0
  30. package/wire-changes/core@7.6.1.json +1130 -0
  31. package/wire-changes/core@7.7.0.json +509 -0
  32. package/wire-changes/core@7.8.0.json +1293 -0
  33. package/wire-changes/core@7.9.0.json +691 -0
  34. package/wire-changes/core@7.9.1.json +518 -0
  35. package/wire-changes/core@7.9.2.json +46 -0
@@ -615,6 +615,25 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
615
615
  const bgRetained = hasBackgroundShell(env) && env.backgroundCapabilities.retainBackgroundProcesses === true;
616
616
  const bgSessionScoped = taskOpts.sessionId !== undefined && taskOpts.taskRegistry !== undefined;
617
617
  const bgEnvIsolatedOwned = hasDestroy(env) && isIsolated(env);
618
+ const recheckReaders = new Set(BASH_CLASSIFY_DEFAULT_ALLOW);
619
+ let canonicalForRoots;
620
+ const canonicalBoundaryFor = (roots) => {
621
+ const key = roots.join("\u0000");
622
+ if (canonicalForRoots?.key !== key)
623
+ canonicalForRoots = { key, boundary: createCanonicalReadBoundary(env, roots) };
624
+ return canonicalForRoots.boundary;
625
+ };
626
+ const canonicalEscape = async (command, signal) => {
627
+ const resolvedBoundary = typeof taskOpts.readBoundary === "function" ? taskOpts.readBoundary() : taskOpts.readBoundary;
628
+ if (resolvedBoundary === undefined || resolvedBoundary.face === "open")
629
+ return undefined;
630
+ const faces = [
631
+ classifyCompoundReadonlyDetailed(command, recheckReaders, resolvedBoundary),
632
+ classifyBoundedReadonlyPollLoopDetailed(command, recheckReaders, resolvedBoundary),
633
+ ];
634
+ const literal = [...new Set(faces.flatMap((v) => v.checkedPaths ?? []))];
635
+ return await canonicalEscapeRefusal(canonicalBoundaryFor(resolvedBoundary.roots), { literal, patterns: [] }, resolvedBoundary.roots, FULL_SHELL_FACE_ESCAPE_PROSE, signal);
636
+ };
618
637
  return defineTool({
619
638
  name: "Bash",
620
639
  ...toolFace("bash"),
@@ -650,6 +669,9 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
650
669
  const timeoutRefusal = bashTimeoutArgRefusal(timeout, timeoutCaps);
651
670
  if (timeoutRefusal !== undefined)
652
671
  return errorResult(timeoutRefusal, { type: "bash_invalid_timeout", code: "bash_invalid_timeout" });
672
+ const escape = await canonicalEscape(command, ctx.signal);
673
+ if (escape !== undefined)
674
+ return escape;
653
675
  if (run_in_background) {
654
676
  if (!hasBackgroundShell(env)) {
655
677
  return errorResult("Error (Bash): this environment does not support background processes (run_in_background).");
@@ -659,6 +681,8 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
659
681
  if (diagnosis !== undefined)
660
682
  throw new Error(diagnosis);
661
683
  }
684
+ if (ctx.signal?.aborted === true)
685
+ return errorResult("Error (Bash): the call was cancelled before the background command started.");
662
686
  const requestedTimeoutSec = timeout !== undefined ? Math.max(1, Math.round(timeout / 1000)) : undefined;
663
687
  const r = await env.spawnBackground(command, {
664
688
  cwd: cwdRef.current,
@@ -923,6 +947,8 @@ function createCanonicalReadBoundary(env, readRoots) {
923
947
  while (parts.length > 0) {
924
948
  const canon = await env.canonicalPath(parts.join("/") || "/", signal);
925
949
  if (canon.ok) {
950
+ if (tail.length === 0)
951
+ return canon.value;
926
952
  return [canon.value.replace(/[\\/]+$/, ""), ...[...tail].reverse()].join("/");
927
953
  }
928
954
  tail.push(parts.pop());
@@ -1045,6 +1071,29 @@ function createCanonicalReadBoundary(env, readRoots) {
1045
1071
  },
1046
1072
  };
1047
1073
  }
1074
+ async function canonicalEscapeRefusal(canonical, candidates, readRoots, face, signal) {
1075
+ const resolved = await canonical.escapesAfterResolution(candidates, signal);
1076
+ if (resolved.unverifiable !== undefined) {
1077
+ return errorResult(`Error (Bash): ${resolved.unverifiable}, so this command cannot be confirmed to read only inside the allowed directories for this session: ${readRoots.join(", ")}. ` +
1078
+ face.note, { type: "readonly_out_of_root", code: "readonly_out_of_root", paths: [] });
1079
+ }
1080
+ if (resolved.escaping.length > 0) {
1081
+ const quoted = resolved.escaping.map((p) => `"${p}"`).join(", ");
1082
+ return errorResult(`Error (Bash): ${face.subject} resolves through a symlink to ${quoted}, outside the allowed directories for this session: ${readRoots.join(", ")}${face.verdictPhrase}. ` +
1083
+ face.note, { type: "readonly_out_of_root", code: "readonly_out_of_root", paths: resolved.escaping });
1084
+ }
1085
+ return undefined;
1086
+ }
1087
+ const READONLY_FACE_ESCAPE_PROSE = {
1088
+ subject: "a path this command reads",
1089
+ verdictPhrase: ` ${NOT_AUTO_ALLOWED}`,
1090
+ note: BASH_READONLY_CONFINEMENT_NOTE,
1091
+ };
1092
+ const FULL_SHELL_FACE_ESCAPE_PROSE = {
1093
+ subject: "a path this command names",
1094
+ verdictPhrase: "",
1095
+ note: "The read boundary judged this command's written spelling before the call was approved, and execution is past the approval step — a target reached only through symlink resolution is refused here rather than escalated.",
1096
+ };
1048
1097
  export function createBashReadonlyTool(env, rootCanonical, allow, opts) {
1049
1098
  const timeoutCaps = resolveBashTimeoutCaps(opts);
1050
1099
  const timeoutCapsSecView = bashTimeoutCapsSec(timeoutCaps);
@@ -1092,16 +1141,9 @@ export function createBashReadonlyTool(env, rootCanonical, allow, opts) {
1092
1141
  return errorResult(`Error (Bash): ${boundary.reason}. ${BASH_READONLY_CONFINEMENT_NOTE}`, { type: "readonly_out_of_root", code: "readonly_out_of_root", paths: boundary.outOfRootPaths ?? [] });
1093
1142
  }
1094
1143
  }
1095
- const resolved = await canonicalBoundary.escapesAfterResolution({ literal: boundary.checkedPaths ?? [], patterns: boundary.undecidedPaths ?? [] }, ctx.signal);
1096
- if (resolved.unverifiable !== undefined) {
1097
- return errorResult(`Error (Bash): ${resolved.unverifiable}, so this command cannot be confirmed to read only inside the allowed directories for this session: ${readRoots.join(", ")}. ` +
1098
- BASH_READONLY_CONFINEMENT_NOTE, { type: "readonly_out_of_root", code: "readonly_out_of_root", paths: [] });
1099
- }
1100
- if (resolved.escaping.length > 0) {
1101
- const quoted = resolved.escaping.map((p) => `"${p}"`).join(", ");
1102
- return errorResult(`Error (Bash): a path this command reads resolves through a symlink to ${quoted}, outside the allowed directories for this session: ${readRoots.join(", ")} ${NOT_AUTO_ALLOWED}. ` +
1103
- BASH_READONLY_CONFINEMENT_NOTE, { type: "readonly_out_of_root", code: "readonly_out_of_root", paths: resolved.escaping });
1104
- }
1144
+ const escape = await canonicalEscapeRefusal(canonicalBoundary, { literal: boundary.checkedPaths ?? [], patterns: boundary.undecidedPaths ?? [] }, readRoots, READONLY_FACE_ESCAPE_PROSE, ctx.signal);
1145
+ if (escape !== undefined)
1146
+ return escape;
1105
1147
  return await runShell(env, rootCanonical, "Bash", command, msTimeoutToRequestedSec(timeout), timeoutCapsSecView, ctx.signal, undefined, undefined, true);
1106
1148
  },
1107
1149
  });
@@ -11,6 +11,7 @@ export * from "./fs-search-tools.js";
11
11
  export * from "./bash-readonly-classifier.js";
12
12
  export * from "./fs-bash.js";
13
13
  export * from "./read-deny.js";
14
+ import { type BashReadonlyRootBoundary } from "./bash-readonly-classifier.js";
14
15
  export { BASH_CLASSIFY_DEFAULT_ALLOW } from "./bash-readonly-classifier.js";
15
16
  import { type ReadDenyEntry } from "./read-deny.js";
16
17
  export * from "./read-face.js";
@@ -40,6 +41,18 @@ export interface HandsToolkitOptions {
40
41
  readOnly?: boolean;
41
42
  /** Override the `bash_readonly` command allowlist (default {@link BASH_READONLY_DEFAULT_ALLOW}). */
42
43
  bashReadonlyAllow?: readonly string[];
44
+ /** The deployment's shell READ BOUNDARY, for the FULL `bash` face's execute-time canonical recheck
45
+ * (see `createBashTool`'s `readBoundary`). The caller passes the SAME seat it gives the gate's shell
46
+ * probes, so the boundary that judges a command's written spelling before approval and the leg that
47
+ * re-judges its symlink-resolved targets before execution are one declaration. Ignored in `readOnly`
48
+ * mode — `bash_readonly` builds its own from the roots it is confined to.
49
+ *
50
+ * ABSENT ⇒ the leg does not run and costs no I/O: with no boundary declared there is no "outside" for
51
+ * a read to be outside of, which is the same reading the probe seats take of an absent boundary. It
52
+ * deliberately does NOT fall back to `additionalRoots`/`rootCanonical`: those are the WRITE side's
53
+ * fence, they exclude `additionalReadRoots`, and standing a second root table up here would refuse
54
+ * reads this deployment sanctions while claiming to enforce the boundary it was never given. */
55
+ shellReadBoundary?: BashReadonlyRootBoundary | (() => BashReadonlyRootBoundary | undefined);
43
56
  /** Commit-message attribution trailer for the `bash` git protocol. Default NONE — attribution is a
44
57
  * deployment identity asset, so the branded deployment sets it; pass `"Name <email>"` to advertise
45
58
  * one, `false` (≡ unset) for no trailer section. */
@@ -74,6 +74,7 @@ export function createHandsToolkit(env, readFileState, rootCanonical, opts = {})
74
74
  ...(opts.autoBackgroundOnTimeout !== undefined ? { autoBackgroundOnTimeout: opts.autoBackgroundOnTimeout } : {}),
75
75
  ...(opts.oneShot !== undefined ? { oneShot: opts.oneShot } : {}),
76
76
  ...(additionalRoots !== undefined ? { additionalRoots } : {}),
77
+ ...(opts.shellReadBoundary !== undefined ? { readBoundary: opts.shellReadBoundary } : {}),
77
78
  ...(opts.bashDefaultTimeoutMs !== undefined ? { bashDefaultTimeoutMs: opts.bashDefaultTimeoutMs } : {}),
78
79
  ...(opts.bashMaxTimeoutMs !== undefined ? { bashMaxTimeoutMs: opts.bashMaxTimeoutMs } : {}),
79
80
  ...(opts.onNotice !== undefined ? { onNotice: opts.onNotice } : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "7.17.4",
3
+ "version": "7.17.5",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -40,7 +40,8 @@
40
40
  "README.md",
41
41
  "NOTICE.md",
42
42
  "CHANGELOG.md",
43
- "LICENSE"
43
+ "LICENSE",
44
+ "wire-changes/"
44
45
  ],
45
46
  "engines": {
46
47
  "node": ">=20.3.0"