@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.
- package/CHANGELOG.md +48 -0
- package/dist/core/memory-engine/delegation-settlement.d.ts +27 -9
- package/dist/core/memory-engine/delegation-settlement.js +155 -46
- package/dist/core/memory-engine/engine.js +12 -6
- package/dist/core/remote-env.d.ts +23 -1
- package/dist/core/remote-env.js +4 -1
- package/dist/core/runner/prepare-hands-readface.js +2 -2
- package/dist/tools/fs/bash-readonly-classifier.d.ts +28 -0
- package/dist/tools/fs/bash-readonly-classifier.js +394 -21
- package/dist/tools/fs/fs-bash.js +87 -21
- package/package.json +1 -1
- package/wire-changes/core@7.17.5.json +2 -2
package/dist/tools/fs/fs-bash.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
1014
|
-
|
|
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 (!
|
|
1048
|
-
|
|
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 || !
|
|
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
|
-
|
|
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
|
-
(
|
|
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
|
|
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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"repo": "core",
|
|
3
3
|
"version": "7.17.5",
|
|
4
4
|
"from": "dd51253ac702ab232224dac50de33eb0dd09e40f",
|
|
5
|
-
"to": "
|
|
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@
|
|
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": []
|