@sema-agent/core 7.17.4 → 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 +77 -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 +10 -8
- package/dist/core/tool-catalog-entries.js +1 -1
- package/dist/tools/fs/bash-readonly-classifier.d.ts +46 -5
- package/dist/tools/fs/bash-readonly-classifier.js +572 -80
- package/dist/tools/fs/fs-bash.d.ts +28 -0
- package/dist/tools/fs/fs-bash.js +135 -27
- package/dist/tools/fs/index.d.ts +13 -0
- package/dist/tools/fs/index.js +1 -0
- package/package.json +3 -2
- package/wire-changes/core@7.10.0.json +739 -0
- package/wire-changes/core@7.11.0.json +685 -0
- package/wire-changes/core@7.11.1.json +112 -0
- package/wire-changes/core@7.11.2.json +73 -0
- package/wire-changes/core@7.12.0.json +356 -0
- package/wire-changes/core@7.13.0.json +199 -0
- package/wire-changes/core@7.14.0.json +578 -0
- package/wire-changes/core@7.15.0.json +174 -0
- package/wire-changes/core@7.16.0.json +132 -0
- package/wire-changes/core@7.17.0.json +1621 -0
- package/wire-changes/core@7.17.1.json +139 -0
- package/wire-changes/core@7.17.2.json +65 -0
- package/wire-changes/core@7.17.3.json +44 -0
- package/wire-changes/core@7.17.4.json +52 -0
- package/wire-changes/core@7.17.5.json +67 -0
- package/wire-changes/core@7.5.0.json +1063 -0
- package/wire-changes/core@7.5.1.json +252 -0
- package/wire-changes/core@7.5.2.json +418 -0
- package/wire-changes/core@7.6.0.json +1243 -0
- package/wire-changes/core@7.6.1.json +1130 -0
- package/wire-changes/core@7.7.0.json +509 -0
- package/wire-changes/core@7.8.0.json +1293 -0
- package/wire-changes/core@7.9.0.json +691 -0
- package/wire-changes/core@7.9.1.json +518 -0
- package/wire-changes/core@7.9.2.json +46 -0
|
@@ -54,6 +54,10 @@ import { type BashReadonlyRootBoundary } from "./bash-readonly-classifier.js";
|
|
|
54
54
|
* The other non-reversible arms stay bare on purpose — their cause is legible in the command itself (a
|
|
55
55
|
* write verb, a pipe, an unlisted name), and manufacturing an account for them would put text on a card
|
|
56
56
|
* that adds nothing the reader cannot already see.
|
|
57
|
+
*
|
|
58
|
+
* What the boundary half of this probe is — and what it is not — is stated once on the seat that owns it
|
|
59
|
+
* ({@link bashReadBoundaryProbe}, `@contract shell.read_boundary.friction_floor`); composing it with the
|
|
60
|
+
* shape classifier here changes what happens to the residual shell risk, never that reading.
|
|
57
61
|
*/
|
|
58
62
|
export declare function bashReversibilityProbe(allow?: readonly string[], boundary?: BashReadonlyRootBoundary | (() => BashReadonlyRootBoundary | undefined)): (args: unknown) => ReversibilityVerdict;
|
|
59
63
|
/**
|
|
@@ -99,6 +103,19 @@ export declare function bashReversibilityProbe(allow?: readonly string[], bounda
|
|
|
99
103
|
* The reader set is the engine's classify set ({@link BASH_CLASSIFY_DEFAULT_ALLOW}) — here it is NOT an
|
|
100
104
|
* auto-allow list but the set of programs whose operands the faces read; a wider set means more reads are
|
|
101
105
|
* judged, never that more runs are vouched.
|
|
106
|
+
*
|
|
107
|
+
* @contract shell.read_boundary.friction_floor — what this seat answers is a question about TEXT: which
|
|
108
|
+
* files the words of a command may name, resolved against the declared roots, the declared home and the
|
|
109
|
+
* observed working directory. It is not a question about what the process will open, and no amount of
|
|
110
|
+
* parsing makes it one. A deployment's containment for what a shell may reach is the `ExecutionEnv` the
|
|
111
|
+
* command runs in (a sandbox, a container, a remote adapter); everything here is the friction floor under
|
|
112
|
+
* that containment — it raises the cost of an accidental or careless read, and it is not a defence against
|
|
113
|
+
* a command written to defeat it. The floor's last cell belongs to the full shell's own execute leg: once
|
|
114
|
+
* the gate has allowed a call and before anything spawns, the candidates this seat accepted are resolved
|
|
115
|
+
* on the filesystem, and a target that leaves the roots only after a symlink is followed is REFUSED —
|
|
116
|
+
* refused rather than asked, because by then the approval step is behind the call (see `createBashTool`'s
|
|
117
|
+
* `readBoundary` option). The leg runs where an environment can resolve a link locally; a remote adapter
|
|
118
|
+
* keeps the textual answer alone.
|
|
102
119
|
*/
|
|
103
120
|
export declare function bashReadBoundaryProbe(boundary: BashReadonlyRootBoundary | (() => BashReadonlyRootBoundary | undefined)): (args: unknown) => ReversibilityVerdict;
|
|
104
121
|
/**
|
|
@@ -171,6 +188,17 @@ export declare function createBashTool(env: ExecutionEnv, rootCanonical: string,
|
|
|
171
188
|
* unconfined; passing this never gates anything.
|
|
172
189
|
*/
|
|
173
190
|
additionalRoots?: readonly string[];
|
|
191
|
+
/**
|
|
192
|
+
* The deployment's shell READ BOUNDARY — the same seat the gate's shell probes read
|
|
193
|
+
* ({@link bashReadBoundaryProbe}), handed to the tool so its execute leg can close the one gap a
|
|
194
|
+
* synchronous seat structurally cannot: the seat judges the WRITTEN spelling, the kernel opens the
|
|
195
|
+
* symlink-resolved target. Passed as a FUNCTION wherever the working directory is observable (an
|
|
196
|
+
* in-task `cd` moves the base a relative operand resolves against); evaluated per call.
|
|
197
|
+
*
|
|
198
|
+
* Absent ⇒ no recheck and no I/O: a deployment with no read boundary declared has no roots for a
|
|
199
|
+
* read to be outside of, exactly as the seat itself reads an absent boundary.
|
|
200
|
+
*/
|
|
201
|
+
readBoundary?: BashReadonlyRootBoundary | (() => BashReadonlyRootBoundary | undefined);
|
|
174
202
|
/** Override of the `timeout` parameter's DEFAULT budget, in ms — see
|
|
175
203
|
* {@link import("./index.js").HandsToolkitOptions.bashDefaultTimeoutMs} for the resolution
|
|
176
204
|
* contract (options over env over constant; invalid values discarded). */
|
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";
|
|
@@ -615,6 +615,27 @@ 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 ?? []), ...(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);
|
|
638
|
+
};
|
|
618
639
|
return defineTool({
|
|
619
640
|
name: "Bash",
|
|
620
641
|
...toolFace("bash"),
|
|
@@ -650,6 +671,9 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
|
|
|
650
671
|
const timeoutRefusal = bashTimeoutArgRefusal(timeout, timeoutCaps);
|
|
651
672
|
if (timeoutRefusal !== undefined)
|
|
652
673
|
return errorResult(timeoutRefusal, { type: "bash_invalid_timeout", code: "bash_invalid_timeout" });
|
|
674
|
+
const escape = await canonicalEscape(command, ctx.signal);
|
|
675
|
+
if (escape !== undefined)
|
|
676
|
+
return escape;
|
|
653
677
|
if (run_in_background) {
|
|
654
678
|
if (!hasBackgroundShell(env)) {
|
|
655
679
|
return errorResult("Error (Bash): this environment does not support background processes (run_in_background).");
|
|
@@ -659,6 +683,8 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
|
|
|
659
683
|
if (diagnosis !== undefined)
|
|
660
684
|
throw new Error(diagnosis);
|
|
661
685
|
}
|
|
686
|
+
if (ctx.signal?.aborted === true)
|
|
687
|
+
return errorResult("Error (Bash): the call was cancelled before the background command started.");
|
|
662
688
|
const requestedTimeoutSec = timeout !== undefined ? Math.max(1, Math.round(timeout / 1000)) : undefined;
|
|
663
689
|
const r = await env.spawnBackground(command, {
|
|
664
690
|
cwd: cwdRef.current,
|
|
@@ -904,7 +930,7 @@ function pathSegmentsFrom(p, from) {
|
|
|
904
930
|
return segs;
|
|
905
931
|
}
|
|
906
932
|
function createCanonicalReadBoundary(env, readRoots) {
|
|
907
|
-
const lexicalOnly =
|
|
933
|
+
const lexicalOnly = boundaryIsTheSandbox(env);
|
|
908
934
|
let rootsOnce;
|
|
909
935
|
const canonicalRoots = async (signal) => {
|
|
910
936
|
rootsOnce ??= (async () => {
|
|
@@ -917,18 +943,60 @@ function createCanonicalReadBoundary(env, readRoots) {
|
|
|
917
943
|
})();
|
|
918
944
|
return await rootsOnce;
|
|
919
945
|
};
|
|
920
|
-
const
|
|
946
|
+
const resolveWithoutParentSteps = async (p, signal) => {
|
|
921
947
|
const parts = p.split(/[\\/]/);
|
|
922
948
|
const tail = [];
|
|
949
|
+
let allAbsent = true;
|
|
923
950
|
while (parts.length > 0) {
|
|
924
951
|
const canon = await env.canonicalPath(parts.join("/") || "/", signal);
|
|
925
952
|
if (canon.ok) {
|
|
926
|
-
|
|
953
|
+
if (tail.length === 0)
|
|
954
|
+
return { value: canon.value, exact: true };
|
|
955
|
+
return { value: [canon.value.replace(/[\\/]+$/, ""), ...[...tail].reverse()].join("/"), exact: false, missing: allAbsent };
|
|
927
956
|
}
|
|
957
|
+
if (canon.error.code !== "not_found" && canon.error.code !== "not_directory")
|
|
958
|
+
allAbsent = false;
|
|
928
959
|
tail.push(parts.pop());
|
|
929
960
|
}
|
|
930
961
|
return undefined;
|
|
931
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
|
+
};
|
|
932
1000
|
const literalRootHead = (p, roots) => {
|
|
933
1001
|
let best;
|
|
934
1002
|
for (const r of [...readRoots, ...roots]) {
|
|
@@ -971,7 +1039,13 @@ function createCanonicalReadBoundary(env, readRoots) {
|
|
|
971
1039
|
if (++dirsUsed > MAX_GLOB_DIRS) {
|
|
972
1040
|
return { unverifiable: `"${p}" expands over more than ${MAX_GLOB_DIRS} directories, too many to enumerate to verify` };
|
|
973
1041
|
}
|
|
974
|
-
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);
|
|
975
1049
|
if (!listed.ok) {
|
|
976
1050
|
if (listed.error.code === "not_found" || listed.error.code === "not_directory")
|
|
977
1051
|
continue;
|
|
@@ -984,9 +1058,8 @@ function createCanonicalReadBoundary(env, readRoots) {
|
|
|
984
1058
|
}
|
|
985
1059
|
let dirInside = false;
|
|
986
1060
|
if (isLast) {
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
next.push(canonDir);
|
|
1061
|
+
if (canonDir !== undefined && !withinAnyRoot(roots, canonDir.value)) {
|
|
1062
|
+
next.push(canonDir.value);
|
|
990
1063
|
continue;
|
|
991
1064
|
}
|
|
992
1065
|
dirInside = canonDir !== undefined;
|
|
@@ -1014,12 +1087,17 @@ function createCanonicalReadBoundary(env, readRoots) {
|
|
|
1014
1087
|
return { escaping: [] };
|
|
1015
1088
|
const roots = await canonicalRoots(signal);
|
|
1016
1089
|
const escaping = [];
|
|
1090
|
+
let unresolvable;
|
|
1017
1091
|
const judge = async (p) => {
|
|
1018
1092
|
const real = await resolve(p, signal);
|
|
1019
1093
|
if (real === undefined)
|
|
1020
1094
|
return;
|
|
1021
|
-
if (!
|
|
1022
|
-
|
|
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);
|
|
1023
1101
|
};
|
|
1024
1102
|
for (const p of paths.literal)
|
|
1025
1103
|
await judge(p);
|
|
@@ -1030,7 +1108,13 @@ function createCanonicalReadBoundary(env, readRoots) {
|
|
|
1030
1108
|
for (const t of expanded !== undefined && expanded.paths.length > 0 ? expanded.paths : [p])
|
|
1031
1109
|
await judge(t);
|
|
1032
1110
|
}
|
|
1033
|
-
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;
|
|
1034
1118
|
},
|
|
1035
1119
|
allResolveInside: async (paths, signal) => {
|
|
1036
1120
|
if (lexicalOnly || paths.length === 0)
|
|
@@ -1038,13 +1122,38 @@ function createCanonicalReadBoundary(env, readRoots) {
|
|
|
1038
1122
|
const roots = await canonicalRoots(signal);
|
|
1039
1123
|
for (const p of paths) {
|
|
1040
1124
|
const real = await resolve(p, signal);
|
|
1041
|
-
if (real === undefined || !
|
|
1125
|
+
if (real === undefined || (!real.exact && real.missing !== true))
|
|
1126
|
+
return false;
|
|
1127
|
+
if (!withinAnyRoot(roots, real.value))
|
|
1042
1128
|
return false;
|
|
1043
1129
|
}
|
|
1044
1130
|
return true;
|
|
1045
1131
|
},
|
|
1046
1132
|
};
|
|
1047
1133
|
}
|
|
1134
|
+
async function canonicalEscapeRefusal(canonical, candidates, readRoots, face, signal) {
|
|
1135
|
+
const resolved = await canonical.escapesAfterResolution(candidates, signal);
|
|
1136
|
+
if (resolved.unverifiable !== undefined) {
|
|
1137
|
+
return errorResult(`Error (Bash): ${resolved.unverifiable}, so this command cannot be confirmed to read only inside the allowed directories for this session: ${readRoots.join(", ")}. ` +
|
|
1138
|
+
face.note, { type: "readonly_out_of_root", code: "readonly_out_of_root", paths: [] });
|
|
1139
|
+
}
|
|
1140
|
+
if (resolved.escaping.length > 0) {
|
|
1141
|
+
const quoted = resolved.escaping.map((p) => `"${p}"`).join(", ");
|
|
1142
|
+
return errorResult(`Error (Bash): ${face.subject} resolves through a symlink to ${quoted}, outside the allowed directories for this session: ${readRoots.join(", ")}${face.verdictPhrase}. ` +
|
|
1143
|
+
face.note, { type: "readonly_out_of_root", code: "readonly_out_of_root", paths: resolved.escaping });
|
|
1144
|
+
}
|
|
1145
|
+
return undefined;
|
|
1146
|
+
}
|
|
1147
|
+
const READONLY_FACE_ESCAPE_PROSE = {
|
|
1148
|
+
subject: "a path this command reads",
|
|
1149
|
+
verdictPhrase: ` ${NOT_AUTO_ALLOWED}`,
|
|
1150
|
+
note: BASH_READONLY_CONFINEMENT_NOTE,
|
|
1151
|
+
};
|
|
1152
|
+
const FULL_SHELL_FACE_ESCAPE_PROSE = {
|
|
1153
|
+
subject: "a path this command names",
|
|
1154
|
+
verdictPhrase: "",
|
|
1155
|
+
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.",
|
|
1156
|
+
};
|
|
1048
1157
|
export function createBashReadonlyTool(env, rootCanonical, allow, opts) {
|
|
1049
1158
|
const timeoutCaps = resolveBashTimeoutCaps(opts);
|
|
1050
1159
|
const timeoutCapsSecView = bashTimeoutCapsSec(timeoutCaps);
|
|
@@ -1054,11 +1163,14 @@ export function createBashReadonlyTool(env, rootCanonical, allow, opts) {
|
|
|
1054
1163
|
const readsOnlyEngineOverflowSpool = async (verdict) => {
|
|
1055
1164
|
if (verdict.outOfRootRead !== true)
|
|
1056
1165
|
return false;
|
|
1166
|
+
if ((verdict.unresolvedOperands ?? []).length > 0)
|
|
1167
|
+
return false;
|
|
1057
1168
|
const paths = verdict.outOfRootPaths ?? [];
|
|
1058
1169
|
if (paths.length === 0)
|
|
1059
1170
|
return false;
|
|
1060
|
-
for (const p of paths) {
|
|
1061
|
-
|
|
1171
|
+
for (const p of [...paths, ...(verdict.outOfRootSpellings ?? [])]) {
|
|
1172
|
+
const real = await canonicalBoundary.canonicalOf(p);
|
|
1173
|
+
if (!(await overflowSpoolFence(real ?? p)))
|
|
1062
1174
|
return false;
|
|
1063
1175
|
}
|
|
1064
1176
|
return true;
|
|
@@ -1071,7 +1183,7 @@ export function createBashReadonlyTool(env, rootCanonical, allow, opts) {
|
|
|
1071
1183
|
"operators (pipes, redirects, ;, &&, command substitution, subshells) are rejected and only " +
|
|
1072
1184
|
"allowlisted commands run. Reads are confined to the workspace root(s): a path outside them is " +
|
|
1073
1185
|
"refused, as is one that cannot be resolved statically (e.g. a `~` path). " +
|
|
1074
|
-
(
|
|
1186
|
+
(boundaryIsTheSandbox(env) ? "" : "A path that leads outside via a symlink counts as outside, including one a glob pattern expands to. ") +
|
|
1075
1187
|
"Still subject to the deployment's approval policy.",
|
|
1076
1188
|
parameters: Type.Object({
|
|
1077
1189
|
command: Type.String({ description: "A single allowlisted read-only command (no shell operators)." }),
|
|
@@ -1087,21 +1199,17 @@ export function createBashReadonlyTool(env, rootCanonical, allow, opts) {
|
|
|
1087
1199
|
return errorResult(`Error (Bash): ${reason}`);
|
|
1088
1200
|
const boundary = classifySimpleCommandReadBoundary(command, { roots: readRoots, cwd: rootCanonical });
|
|
1089
1201
|
if (boundary.reason !== undefined) {
|
|
1090
|
-
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));
|
|
1091
1206
|
if (!rescued && !(await readsOnlyEngineOverflowSpool(boundary))) {
|
|
1092
1207
|
return errorResult(`Error (Bash): ${boundary.reason}. ${BASH_READONLY_CONFINEMENT_NOTE}`, { type: "readonly_out_of_root", code: "readonly_out_of_root", paths: boundary.outOfRootPaths ?? [] });
|
|
1093
1208
|
}
|
|
1094
1209
|
}
|
|
1095
|
-
const
|
|
1096
|
-
if (
|
|
1097
|
-
return
|
|
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
|
-
}
|
|
1210
|
+
const escape = await canonicalEscapeRefusal(canonicalBoundary, { literal: [...(boundary.checkedPaths ?? []), ...(boundary.checkedSpellings ?? [])], patterns: [...(boundary.undecidedPaths ?? []), ...(boundary.undecidedSpellings ?? [])] }, readRoots, READONLY_FACE_ESCAPE_PROSE, ctx.signal);
|
|
1211
|
+
if (escape !== undefined)
|
|
1212
|
+
return escape;
|
|
1105
1213
|
return await runShell(env, rootCanonical, "Bash", command, msTimeoutToRequestedSec(timeout), timeoutCapsSecView, ctx.signal, undefined, undefined, true);
|
|
1106
1214
|
},
|
|
1107
1215
|
});
|
package/dist/tools/fs/index.d.ts
CHANGED
|
@@ -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. */
|
package/dist/tools/fs/index.js
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "7.17.6",
|
|
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"
|