@sema-agent/core 7.17.5 → 7.17.6

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.
@@ -6,8 +6,8 @@ import { TASK_OUTPUT_TOOL_NAME, TASK_STOP_TOOL_NAME, TASK_OUTPUT_MISSING_ID_MESS
6
6
  import { hasBackgroundShell } from "../../core/background-shell.js";
7
7
  import { delimitUntrusted } from "../../core/untrusted-text.js";
8
8
  import { MCP_IMAGE_MAX_BASE64 } from "../../core/mcp.js";
9
- import { imageMagicMatches, withinAnyRoot } from "./safety.js";
10
- import { isRemoteExecutionEnv, hasDestroy, isIsolated } from "../../core/remote-env.js";
9
+ import { imageMagicMatches, normalizeAbsPathLexically, withinAnyRoot } from "./safety.js";
10
+ import { boundaryIsTheSandbox, hasDestroy, isIsolated } from "../../core/remote-env.js";
11
11
  import { ghRateLimitHint } from "./gh-rate-limit.js";
12
12
  import { resolveBashTimeoutCaps, bashTimeoutCapsSec, bashTimeoutArgRefusal, bashTimeoutParamDescription, envErrorDetail, bashMaxOutputChars, clipShellOutput, writeShellOverflowFile, createShellOverflowSpoolFence, shellRecoveryHint, CWD_SENTINEL, BASH_READONLY_CONFINEMENT_NOTE, } from "./fs-shared.js";
13
13
  import { PROBE_CAUSE_PATH_MAX, inlineUntrusted } from "../../core/untrusted-text.js";
@@ -631,8 +631,10 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
631
631
  classifyCompoundReadonlyDetailed(command, recheckReaders, resolvedBoundary),
632
632
  classifyBoundedReadonlyPollLoopDetailed(command, recheckReaders, resolvedBoundary),
633
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);
634
+ const literal = [...new Set(faces.flatMap((v) => [...(v.checkedPaths ?? []), ...(v.checkedSpellings ?? [])]))];
635
+ const asked = new Set(faces.flatMap((v) => v.outOfRootPaths ?? []));
636
+ const patterns = [...new Set(faces.flatMap((v) => [...(v.undecidedPaths ?? []), ...(v.undecidedSpellings ?? [])]))].filter((p) => !asked.has(normalizeAbsPathLexically(p)));
637
+ return await canonicalEscapeRefusal(canonicalBoundaryFor(resolvedBoundary.roots), { literal, patterns }, resolvedBoundary.roots, FULL_SHELL_FACE_ESCAPE_PROSE, signal);
636
638
  };
637
639
  return defineTool({
638
640
  name: "Bash",
@@ -928,7 +930,7 @@ function pathSegmentsFrom(p, from) {
928
930
  return segs;
929
931
  }
930
932
  function createCanonicalReadBoundary(env, readRoots) {
931
- const lexicalOnly = isRemoteExecutionEnv(env);
933
+ const lexicalOnly = boundaryIsTheSandbox(env);
932
934
  let rootsOnce;
933
935
  const canonicalRoots = async (signal) => {
934
936
  rootsOnce ??= (async () => {
@@ -941,20 +943,60 @@ function createCanonicalReadBoundary(env, readRoots) {
941
943
  })();
942
944
  return await rootsOnce;
943
945
  };
944
- const resolve = async (p, signal) => {
946
+ const resolveWithoutParentSteps = async (p, signal) => {
945
947
  const parts = p.split(/[\\/]/);
946
948
  const tail = [];
949
+ let allAbsent = true;
947
950
  while (parts.length > 0) {
948
951
  const canon = await env.canonicalPath(parts.join("/") || "/", signal);
949
952
  if (canon.ok) {
950
953
  if (tail.length === 0)
951
- return canon.value;
952
- return [canon.value.replace(/[\\/]+$/, ""), ...[...tail].reverse()].join("/");
954
+ return { value: canon.value, exact: true };
955
+ return { value: [canon.value.replace(/[\\/]+$/, ""), ...[...tail].reverse()].join("/"), exact: false, missing: allAbsent };
953
956
  }
957
+ if (canon.error.code !== "not_found" && canon.error.code !== "not_directory")
958
+ allAbsent = false;
954
959
  tail.push(parts.pop());
955
960
  }
956
961
  return undefined;
957
962
  };
963
+ const parentOf = (canonical) => normalizeAbsPathLexically(`${canonical}/..`);
964
+ const resolve = async (p, signal) => {
965
+ const parts = p.split(/[\\/]/);
966
+ const first = parts.findIndex((seg) => seg === "..");
967
+ if (first < 0)
968
+ return await resolveWithoutParentSteps(p, signal);
969
+ const headEnd = parts.slice(0, first).reduce((n, seg) => n + seg.length + 1, 0);
970
+ let head = await resolveWithoutParentSteps(p.slice(0, headEnd) || "/", signal);
971
+ if (head === undefined)
972
+ return undefined;
973
+ let pending = [];
974
+ const flush = async () => {
975
+ if (pending.length === 0)
976
+ return true;
977
+ const next = await resolveWithoutParentSteps([head.value.replace(/[\\/]+$/, ""), ...pending].join("/"), signal);
978
+ pending = [];
979
+ if (next === undefined)
980
+ return false;
981
+ head = next;
982
+ return true;
983
+ };
984
+ for (let i = first; i < parts.length; i++) {
985
+ const seg = parts[i];
986
+ if (seg === "" || seg === ".")
987
+ continue;
988
+ if (seg !== "..") {
989
+ pending.push(seg);
990
+ continue;
991
+ }
992
+ if (!(await flush()))
993
+ return undefined;
994
+ if (!head.exact)
995
+ return head;
996
+ head = { value: parentOf(head.value), exact: true };
997
+ }
998
+ return (await flush()) ? head : undefined;
999
+ };
958
1000
  const literalRootHead = (p, roots) => {
959
1001
  let best;
960
1002
  for (const r of [...readRoots, ...roots]) {
@@ -997,7 +1039,13 @@ function createCanonicalReadBoundary(env, readRoots) {
997
1039
  if (++dirsUsed > MAX_GLOB_DIRS) {
998
1040
  return { unverifiable: `"${p}" expands over more than ${MAX_GLOB_DIRS} directories, too many to enumerate to verify` };
999
1041
  }
1000
- const listed = await env.listDir(dir, signal);
1042
+ const canonDir = await resolve(dir, signal);
1043
+ if (canonDir !== undefined && canonDir.missing === true)
1044
+ continue;
1045
+ if (canonDir !== undefined && !canonDir.exact) {
1046
+ return { unverifiable: `"${dir}" cannot be resolved to verify what "${seg}" would expand to` };
1047
+ }
1048
+ const listed = await env.listDir(canonDir?.value ?? dir, signal);
1001
1049
  if (!listed.ok) {
1002
1050
  if (listed.error.code === "not_found" || listed.error.code === "not_directory")
1003
1051
  continue;
@@ -1010,9 +1058,8 @@ function createCanonicalReadBoundary(env, readRoots) {
1010
1058
  }
1011
1059
  let dirInside = false;
1012
1060
  if (isLast) {
1013
- const canonDir = await resolve(dir, signal);
1014
- if (canonDir !== undefined && !withinAnyRoot(roots, canonDir)) {
1015
- next.push(canonDir);
1061
+ if (canonDir !== undefined && !withinAnyRoot(roots, canonDir.value)) {
1062
+ next.push(canonDir.value);
1016
1063
  continue;
1017
1064
  }
1018
1065
  dirInside = canonDir !== undefined;
@@ -1040,12 +1087,17 @@ function createCanonicalReadBoundary(env, readRoots) {
1040
1087
  return { escaping: [] };
1041
1088
  const roots = await canonicalRoots(signal);
1042
1089
  const escaping = [];
1090
+ let unresolvable;
1043
1091
  const judge = async (p) => {
1044
1092
  const real = await resolve(p, signal);
1045
1093
  if (real === undefined)
1046
1094
  return;
1047
- if (!withinAnyRoot(roots, real) && !escaping.includes(real))
1048
- escaping.push(real);
1095
+ if (!real.exact && real.missing !== true) {
1096
+ unresolvable ??= `"${p}" cannot be resolved to verify where it reads`;
1097
+ return;
1098
+ }
1099
+ if (!withinAnyRoot(roots, real.value) && !escaping.includes(real.value))
1100
+ escaping.push(real.value);
1049
1101
  };
1050
1102
  for (const p of paths.literal)
1051
1103
  await judge(p);
@@ -1056,7 +1108,13 @@ function createCanonicalReadBoundary(env, readRoots) {
1056
1108
  for (const t of expanded !== undefined && expanded.paths.length > 0 ? expanded.paths : [p])
1057
1109
  await judge(t);
1058
1110
  }
1059
- return { escaping };
1111
+ return unresolvable === undefined ? { escaping } : { escaping, unverifiable: unresolvable };
1112
+ },
1113
+ canonicalOf: async (path, signal) => {
1114
+ if (lexicalOnly)
1115
+ return undefined;
1116
+ const real = await resolve(path, signal);
1117
+ return real === undefined || (!real.exact && real.missing !== true) ? undefined : real.value;
1060
1118
  },
1061
1119
  allResolveInside: async (paths, signal) => {
1062
1120
  if (lexicalOnly || paths.length === 0)
@@ -1064,7 +1122,9 @@ function createCanonicalReadBoundary(env, readRoots) {
1064
1122
  const roots = await canonicalRoots(signal);
1065
1123
  for (const p of paths) {
1066
1124
  const real = await resolve(p, signal);
1067
- if (real === undefined || !withinAnyRoot(roots, real))
1125
+ if (real === undefined || (!real.exact && real.missing !== true))
1126
+ return false;
1127
+ if (!withinAnyRoot(roots, real.value))
1068
1128
  return false;
1069
1129
  }
1070
1130
  return true;
@@ -1103,11 +1163,14 @@ export function createBashReadonlyTool(env, rootCanonical, allow, opts) {
1103
1163
  const readsOnlyEngineOverflowSpool = async (verdict) => {
1104
1164
  if (verdict.outOfRootRead !== true)
1105
1165
  return false;
1166
+ if ((verdict.unresolvedOperands ?? []).length > 0)
1167
+ return false;
1106
1168
  const paths = verdict.outOfRootPaths ?? [];
1107
1169
  if (paths.length === 0)
1108
1170
  return false;
1109
- for (const p of paths) {
1110
- if (!(await overflowSpoolFence(p)))
1171
+ for (const p of [...paths, ...(verdict.outOfRootSpellings ?? [])]) {
1172
+ const real = await canonicalBoundary.canonicalOf(p);
1173
+ if (!(await overflowSpoolFence(real ?? p)))
1111
1174
  return false;
1112
1175
  }
1113
1176
  return true;
@@ -1120,7 +1183,7 @@ export function createBashReadonlyTool(env, rootCanonical, allow, opts) {
1120
1183
  "operators (pipes, redirects, ;, &&, command substitution, subshells) are rejected and only " +
1121
1184
  "allowlisted commands run. Reads are confined to the workspace root(s): a path outside them is " +
1122
1185
  "refused, as is one that cannot be resolved statically (e.g. a `~` path). " +
1123
- (isRemoteExecutionEnv(env) ? "" : "A path that leads outside via a symlink counts as outside, including one a glob pattern expands to. ") +
1186
+ (boundaryIsTheSandbox(env) ? "" : "A path that leads outside via a symlink counts as outside, including one a glob pattern expands to. ") +
1124
1187
  "Still subject to the deployment's approval policy.",
1125
1188
  parameters: Type.Object({
1126
1189
  command: Type.String({ description: "A single allowlisted read-only command (no shell operators)." }),
@@ -1136,12 +1199,15 @@ export function createBashReadonlyTool(env, rootCanonical, allow, opts) {
1136
1199
  return errorResult(`Error (Bash): ${reason}`);
1137
1200
  const boundary = classifySimpleCommandReadBoundary(command, { roots: readRoots, cwd: rootCanonical });
1138
1201
  if (boundary.reason !== undefined) {
1139
- const rescued = boundary.outOfRootRead === true && (await canonicalBoundary.allResolveInside(boundary.outOfRootPaths ?? [], ctx.signal));
1202
+ const rescueCandidates = [...(boundary.outOfRootPaths ?? []), ...(boundary.outOfRootSpellings ?? [])];
1203
+ const rescued = boundary.outOfRootRead === true &&
1204
+ (boundary.unresolvedOperands ?? []).length === 0 &&
1205
+ (await canonicalBoundary.allResolveInside(rescueCandidates, ctx.signal));
1140
1206
  if (!rescued && !(await readsOnlyEngineOverflowSpool(boundary))) {
1141
1207
  return errorResult(`Error (Bash): ${boundary.reason}. ${BASH_READONLY_CONFINEMENT_NOTE}`, { type: "readonly_out_of_root", code: "readonly_out_of_root", paths: boundary.outOfRootPaths ?? [] });
1142
1208
  }
1143
1209
  }
1144
- const escape = await canonicalEscapeRefusal(canonicalBoundary, { literal: boundary.checkedPaths ?? [], patterns: boundary.undecidedPaths ?? [] }, readRoots, READONLY_FACE_ESCAPE_PROSE, ctx.signal);
1210
+ const escape = await canonicalEscapeRefusal(canonicalBoundary, { literal: [...(boundary.checkedPaths ?? []), ...(boundary.checkedSpellings ?? [])], patterns: [...(boundary.undecidedPaths ?? []), ...(boundary.undecidedSpellings ?? [])] }, readRoots, READONLY_FACE_ESCAPE_PROSE, ctx.signal);
1145
1211
  if (escape !== undefined)
1146
1212
  return escape;
1147
1213
  return await runShell(env, rootCanonical, "Bash", command, msTimeoutToRequestedSec(timeout), timeoutCapsSecView, ctx.signal, undefined, undefined, true);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "7.17.5",
3
+ "version": "7.17.6",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -2,7 +2,7 @@
2
2
  "repo": "core",
3
3
  "version": "7.17.5",
4
4
  "from": "dd51253ac702ab232224dac50de33eb0dd09e40f",
5
- "to": "99b9ef8d61e5b2a67d62ec6a6cdf65e831c4752f",
5
+ "to": "3dfd4ff66bfd1c6ea217fa251d2f28df79052f87",
6
6
  "added": [],
7
7
  "removed": [],
8
8
  "changed": [],
@@ -60,7 +60,7 @@
60
60
  "cli": [],
61
61
  "server": [],
62
62
  "sdk": [],
63
- "note": "scanned cli@846136de server@82cf402c sdk@6910789 — a name listed is MENTIONED there today, which is evidence of consumption, not proof of it"
63
+ "note": "scanned cli@0a45f646 server@82cf402c sdk@6910789 — a name listed is MENTIONED there today, which is evidence of consumption, not proof of it"
64
64
  },
65
65
  "breakingCount": 0,
66
66
  "breakingItems": []