@sema-agent/core 7.24.1 → 7.24.3
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 +62 -0
- package/dist/agents/subagent.js +4 -1
- package/dist/brain/anthropic.js +2 -2
- package/dist/brain/errors.d.ts +6 -4
- package/dist/brain/open-responses.js +2 -2
- package/dist/brain/openai.js +2 -2
- package/dist/brain/route-adjudicator.d.ts +41 -13
- package/dist/brain/route-adjudicator.js +97 -18
- package/dist/core/checkpoint-store.d.ts +127 -102
- package/dist/core/checkpoint-store.js +33 -9
- package/dist/core/mcp-failure.d.ts +73 -0
- package/dist/core/mcp-failure.js +21 -0
- package/dist/core/mcp.d.ts +18 -1
- package/dist/core/mcp.js +33 -12
- package/dist/core/memory-engine/distiller.js +3 -3
- package/dist/core/runner/prepare-boundary-parks.js +3 -3
- package/dist/core/runner/prepare-park-ask.js +2 -2
- package/dist/core/runner/prepare-wiring-manifest.d.ts +3 -2
- package/dist/core/runner/prepare-wiring-manifest.js +3 -1
- package/dist/core/runner/resume-checkpoint-screen.js +11 -10
- package/dist/core/runner/resume-policy-outcome.js +2 -2
- package/dist/core/wiring-manifest.d.ts +20 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/prompt-assembly/packs/sema-default.js +15 -0
- package/dist/prompts/default.d.ts +1 -1
- package/dist/prompts/default.js +4 -4
- package/dist/tools/fs/bash-program-position.d.ts +5 -0
- package/dist/tools/fs/bash-program-position.js +1 -1
- package/dist/tools/fs/bash-readonly-classifier.d.ts +476 -1
- package/dist/tools/fs/bash-readonly-classifier.js +98 -34
- package/dist/tools/fs/fs-bash.js +2 -325
- package/dist/tools/fs/shell-read-establish.d.ts +127 -0
- package/dist/tools/fs/shell-read-establish.js +326 -0
- package/dist/tools/fs/shell-read-plan.d.ts +113 -0
- package/dist/tools/fs/shell-read-plan.js +700 -0
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +9 -1
- package/wire-changes/core@7.24.1.json +3 -17
- package/wire-changes/core@7.24.2.json +81 -0
- package/wire-changes/core@7.24.3.json +244 -0
|
@@ -203,12 +203,12 @@ function outOfRootGrantCandidateOf(paths, boundary) {
|
|
|
203
203
|
return paths.length === 1 ? { path: paths[0], exact: true } : undefined;
|
|
204
204
|
return { path: candidate };
|
|
205
205
|
}
|
|
206
|
-
const NO_PATH_OPERAND_COMMANDS = new Set([
|
|
206
|
+
export const NO_PATH_OPERAND_COMMANDS = new Set([
|
|
207
207
|
"pwd", "echo", "printf", "whoami", "uname", "which", "tr", "basename", "dirname",
|
|
208
208
|
"cal", "uptime", "id", "groups", "nproc", "locale", "free", "true", "false", "seq", "expr", "type", "sleep",
|
|
209
209
|
]);
|
|
210
210
|
const XARGS_TARGETS_ACCEPTED_UNJUDGED = NO_PATH_OPERAND_COMMANDS;
|
|
211
|
-
function isPathShapedToken(tok) {
|
|
211
|
+
export function isPathShapedToken(tok) {
|
|
212
212
|
return tok.includes("/") || tok.startsWith("~") || tok === "." || tok === "..";
|
|
213
213
|
}
|
|
214
214
|
export function resolveOperandLexically(base, operand, homeDir) {
|
|
@@ -261,7 +261,7 @@ function tokensMoveBase(toks) {
|
|
|
261
261
|
return false;
|
|
262
262
|
return pos.refusal !== undefined || pos.program?.basename === "cd";
|
|
263
263
|
}
|
|
264
|
-
const SHELL_REENTRY_PROGRAM_RE = /^(?:(?:ba|z|da|k|fi|a|mk|ya|pdk|c|tc)?sh|eval|source|\.|parallel)$/;
|
|
264
|
+
export const SHELL_REENTRY_PROGRAM_RE = /^(?:(?:ba|z|da|k|fi|a|mk|ya|pdk|c|tc)?sh|eval|source|\.|parallel)$/;
|
|
265
265
|
export function refusedCommandMayRead(command, allow, boundary, declinesAreEvidence) {
|
|
266
266
|
const scan = { declined: false };
|
|
267
267
|
const exempt = refusedCommandExempt(command, allow, boundary, declinesAreEvidence, scan, 0);
|
|
@@ -436,7 +436,7 @@ function tokenizeSegment(segment) {
|
|
|
436
436
|
return { folded: [], raw: [] };
|
|
437
437
|
return wordsOf(lexed.segments[0]);
|
|
438
438
|
}
|
|
439
|
-
function hasUnquotedExpansionMetachar(rawToken) {
|
|
439
|
+
export function hasUnquotedExpansionMetachar(rawToken) {
|
|
440
440
|
let open;
|
|
441
441
|
for (const ch of rawToken) {
|
|
442
442
|
if (open === undefined && (ch === '"' || ch === "'")) {
|
|
@@ -454,7 +454,7 @@ function hasUnquotedExpansionMetachar(rawToken) {
|
|
|
454
454
|
}
|
|
455
455
|
return false;
|
|
456
456
|
}
|
|
457
|
-
function hasUnquotedGlobMetachar(rawToken) {
|
|
457
|
+
export function hasUnquotedGlobMetachar(rawToken) {
|
|
458
458
|
let open;
|
|
459
459
|
for (const ch of rawToken) {
|
|
460
460
|
if (open === undefined && (ch === '"' || ch === "'")) {
|
|
@@ -481,7 +481,7 @@ function longOptionNameOf(tok) {
|
|
|
481
481
|
function isLongOptionAbbrevOf(name, full) {
|
|
482
482
|
return full.startsWith(name);
|
|
483
483
|
}
|
|
484
|
-
function suppliesFirstOperandSlot(slot, tok) {
|
|
484
|
+
export function suppliesFirstOperandSlot(slot, tok) {
|
|
485
485
|
if (tok.startsWith("--")) {
|
|
486
486
|
const long = longOptionNameOf(tok);
|
|
487
487
|
return long !== undefined && slot.suppliedByLongNames.some((full) => isLongOptionAbbrevOf(long, full));
|
|
@@ -716,7 +716,7 @@ const VERB_OPTION_SLOTS = {
|
|
|
716
716
|
],
|
|
717
717
|
},
|
|
718
718
|
};
|
|
719
|
-
function readVerbOptionSlot(name, tok) {
|
|
719
|
+
export function readVerbOptionSlot(name, tok) {
|
|
720
720
|
const row = VERB_OPTION_SLOTS[name];
|
|
721
721
|
if (row === undefined)
|
|
722
722
|
return undefined;
|
|
@@ -777,9 +777,12 @@ function readVerbOptionSlot(name, tok) {
|
|
|
777
777
|
}
|
|
778
778
|
return { form: "no-value" };
|
|
779
779
|
}
|
|
780
|
-
function verbOptionsArePositionIndependent(name) {
|
|
780
|
+
export function verbOptionsArePositionIndependent(name) {
|
|
781
781
|
return VERB_OPTION_SLOTS[name]?.optionsArePositionIndependent === true;
|
|
782
782
|
}
|
|
783
|
+
export function verbFirstOperandSlot(name) {
|
|
784
|
+
return VERB_OPTION_SLOTS[name]?.firstOperand;
|
|
785
|
+
}
|
|
783
786
|
function localFileUrlPath(value) {
|
|
784
787
|
const m = /^file:(?:\/\/[^/?#]*)?(\/[^?#]*)/i.exec(value.trim());
|
|
785
788
|
if (m === null)
|
|
@@ -804,7 +807,7 @@ function formSpecFileNames(value) {
|
|
|
804
807
|
after(part);
|
|
805
808
|
return [...new Set(out)];
|
|
806
809
|
}
|
|
807
|
-
function slotValueCandidates(value, kind) {
|
|
810
|
+
export function slotValueCandidates(value, kind) {
|
|
808
811
|
if (!valueNamesAFile(kind))
|
|
809
812
|
return [];
|
|
810
813
|
if (kind === "url") {
|
|
@@ -877,7 +880,7 @@ const GREP_RECURSIVE_READ_FORM = {
|
|
|
877
880
|
enumOptions: [{ shortLetter: "d", longName: "directories", recursiveValue: "recurse" }],
|
|
878
881
|
dashIsStdin: true,
|
|
879
882
|
};
|
|
880
|
-
const RECURSIVE_READ_FORMS = {
|
|
883
|
+
export const RECURSIVE_READ_FORMS = {
|
|
881
884
|
grep: GREP_RECURSIVE_READ_FORM,
|
|
882
885
|
egrep: GREP_RECURSIVE_READ_FORM,
|
|
883
886
|
fgrep: GREP_RECURSIVE_READ_FORM,
|
|
@@ -914,12 +917,29 @@ function utf8Length(text) {
|
|
|
914
917
|
}
|
|
915
918
|
return bytes;
|
|
916
919
|
}
|
|
920
|
+
function sedReadingKind(reading) {
|
|
921
|
+
return typeof reading === "string" ? reading : "names";
|
|
922
|
+
}
|
|
917
923
|
function readSedScript(script) {
|
|
918
924
|
if (script.split("\n").some((line) => utf8Length(line) >= SED_SCRIPT_LINE_LIMIT))
|
|
919
925
|
return "unseen";
|
|
920
926
|
let i = 0;
|
|
921
927
|
let depth = 0;
|
|
922
928
|
const len = script.length;
|
|
929
|
+
const reads = [];
|
|
930
|
+
const writes = [];
|
|
931
|
+
const commands = [];
|
|
932
|
+
let executesPatternSpace = false;
|
|
933
|
+
const anyName = () => reads.length + writes.length + commands.length > 0 || executesPatternSpace;
|
|
934
|
+
const named = () => ({ names: { reads, writes, commands, executesPatternSpace } });
|
|
935
|
+
const failed = () => (anyName() ? named() : "unparsed");
|
|
936
|
+
const restOfLine = () => {
|
|
937
|
+
skipBlank();
|
|
938
|
+
const from = i;
|
|
939
|
+
while (i < len && script[i] !== "\n")
|
|
940
|
+
i++;
|
|
941
|
+
return script.slice(from, i);
|
|
942
|
+
};
|
|
923
943
|
const isBlank = (ch) => ch === " " || ch === "\t" || ch === "\v" || ch === "\f";
|
|
924
944
|
const skipBlank = () => {
|
|
925
945
|
while (i < len && isBlank(script[i]))
|
|
@@ -1011,14 +1031,14 @@ function readSedScript(script) {
|
|
|
1011
1031
|
}
|
|
1012
1032
|
if (ch === "}") {
|
|
1013
1033
|
if (depth === 0)
|
|
1014
|
-
return
|
|
1034
|
+
return failed();
|
|
1015
1035
|
depth--;
|
|
1016
1036
|
i++;
|
|
1017
1037
|
continue;
|
|
1018
1038
|
}
|
|
1019
1039
|
const first = readAddress();
|
|
1020
1040
|
if (first === false)
|
|
1021
|
-
return
|
|
1041
|
+
return failed();
|
|
1022
1042
|
if (first === true) {
|
|
1023
1043
|
skipBlank();
|
|
1024
1044
|
if (script[i] === ",") {
|
|
@@ -1027,11 +1047,11 @@ function readSedScript(script) {
|
|
|
1027
1047
|
if (script[i] === "+" || script[i] === "~") {
|
|
1028
1048
|
i++;
|
|
1029
1049
|
if (!(script[i] >= "0" && script[i] <= "9"))
|
|
1030
|
-
return
|
|
1050
|
+
return failed();
|
|
1031
1051
|
digits();
|
|
1032
1052
|
}
|
|
1033
1053
|
else if (readAddress() !== true)
|
|
1034
|
-
return
|
|
1054
|
+
return failed();
|
|
1035
1055
|
}
|
|
1036
1056
|
skipBlank();
|
|
1037
1057
|
}
|
|
@@ -1041,10 +1061,22 @@ function readSedScript(script) {
|
|
|
1041
1061
|
}
|
|
1042
1062
|
const cmd = script[i];
|
|
1043
1063
|
if (cmd === undefined)
|
|
1044
|
-
return
|
|
1064
|
+
return failed();
|
|
1045
1065
|
i++;
|
|
1046
|
-
if (SED_FILE_OR_EXEC_COMMANDS.has(cmd))
|
|
1047
|
-
|
|
1066
|
+
if (SED_FILE_OR_EXEC_COMMANDS.has(cmd)) {
|
|
1067
|
+
const arg = restOfLine();
|
|
1068
|
+
if (cmd === "e") {
|
|
1069
|
+
if (arg === "")
|
|
1070
|
+
executesPatternSpace = true;
|
|
1071
|
+
else
|
|
1072
|
+
commands.push(arg);
|
|
1073
|
+
}
|
|
1074
|
+
else if (cmd === "r" || cmd === "R")
|
|
1075
|
+
reads.push(arg);
|
|
1076
|
+
else
|
|
1077
|
+
writes.push(arg);
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
1048
1080
|
if (cmd === "{") {
|
|
1049
1081
|
depth++;
|
|
1050
1082
|
continue;
|
|
@@ -1052,25 +1084,33 @@ function readSedScript(script) {
|
|
|
1052
1084
|
if (cmd === "s" || cmd === "y") {
|
|
1053
1085
|
const delim = script[i];
|
|
1054
1086
|
if (delim === undefined || delim === "\\" || delim === "\n")
|
|
1055
|
-
return
|
|
1087
|
+
return failed();
|
|
1056
1088
|
i++;
|
|
1057
1089
|
if (!consumeDelimited(delim, cmd === "s"))
|
|
1058
|
-
return
|
|
1090
|
+
return failed();
|
|
1059
1091
|
if (!consumeDelimited(delim, false))
|
|
1060
|
-
return
|
|
1092
|
+
return failed();
|
|
1061
1093
|
if (cmd === "y")
|
|
1062
1094
|
continue;
|
|
1063
1095
|
while (i < len) {
|
|
1064
1096
|
const flag = script[i];
|
|
1065
|
-
if (flag === "w" || flag === "W"
|
|
1066
|
-
|
|
1097
|
+
if (flag === "w" || flag === "W") {
|
|
1098
|
+
i++;
|
|
1099
|
+
writes.push(restOfLine());
|
|
1100
|
+
break;
|
|
1101
|
+
}
|
|
1102
|
+
if (flag === "e") {
|
|
1103
|
+
executesPatternSpace = true;
|
|
1104
|
+
i++;
|
|
1105
|
+
continue;
|
|
1106
|
+
}
|
|
1067
1107
|
if (SED_SUBSTITUTE_FLAGS.test(flag) || isBlank(flag)) {
|
|
1068
1108
|
i++;
|
|
1069
1109
|
continue;
|
|
1070
1110
|
}
|
|
1071
1111
|
if (flag === ";" || flag === "\n" || flag === "\r" || flag === "}" || flag === "#")
|
|
1072
1112
|
break;
|
|
1073
|
-
return
|
|
1113
|
+
return failed();
|
|
1074
1114
|
}
|
|
1075
1115
|
continue;
|
|
1076
1116
|
}
|
|
@@ -1091,9 +1131,11 @@ function readSedScript(script) {
|
|
|
1091
1131
|
continue;
|
|
1092
1132
|
}
|
|
1093
1133
|
if (!SED_PLAIN_COMMANDS.has(cmd))
|
|
1094
|
-
return
|
|
1134
|
+
return failed();
|
|
1095
1135
|
}
|
|
1096
|
-
|
|
1136
|
+
if (depth !== 0)
|
|
1137
|
+
return failed();
|
|
1138
|
+
return anyName() ? named() : "clean";
|
|
1097
1139
|
}
|
|
1098
1140
|
const SED_OPTION_DIALECTS = [
|
|
1099
1141
|
{ separated: "efl", suffix: "iI", stopsAtFirstOperand: false },
|
|
@@ -1108,6 +1150,9 @@ const SED_LONG_PLAIN_OPTIONS = [
|
|
|
1108
1150
|
"sandbox", "separate", "silent", "unbuffered", "version", "zero-terminated",
|
|
1109
1151
|
];
|
|
1110
1152
|
function readSedInvocation(args, dialect) {
|
|
1153
|
+
return sedReadingKind(readSedInvocationDetailed(args, dialect));
|
|
1154
|
+
}
|
|
1155
|
+
function readSedInvocationDetailed(args, dialect) {
|
|
1111
1156
|
const scripts = [];
|
|
1112
1157
|
let scriptSupplied = false;
|
|
1113
1158
|
let endOfOptions = false;
|
|
@@ -1181,6 +1226,25 @@ function readSedInvocation(args, dialect) {
|
|
|
1181
1226
|
return readSedScript(script);
|
|
1182
1227
|
}
|
|
1183
1228
|
const SED_SHORT_PLAIN_LETTERS = "nsuzEraHl";
|
|
1229
|
+
export function sedScriptSlotFiles(args) {
|
|
1230
|
+
const reads = new Set();
|
|
1231
|
+
const writes = new Set();
|
|
1232
|
+
const commands = new Set();
|
|
1233
|
+
let executesPatternSpace = false;
|
|
1234
|
+
for (const dialect of SED_OPTION_DIALECTS) {
|
|
1235
|
+
const reading = readSedInvocationDetailed(args, dialect);
|
|
1236
|
+
if (typeof reading === "string")
|
|
1237
|
+
continue;
|
|
1238
|
+
for (const f of reading.names.reads)
|
|
1239
|
+
reads.add(f);
|
|
1240
|
+
for (const f of reading.names.writes)
|
|
1241
|
+
writes.add(f);
|
|
1242
|
+
for (const c of reading.names.commands)
|
|
1243
|
+
commands.add(c);
|
|
1244
|
+
executesPatternSpace ||= reading.names.executesPatternSpace;
|
|
1245
|
+
}
|
|
1246
|
+
return { reads: [...reads], writes: [...writes], commands: [...commands], executesPatternSpace };
|
|
1247
|
+
}
|
|
1184
1248
|
const SCRIPT_SLOT_VERBS = new Map([
|
|
1185
1249
|
["sed", (args) => {
|
|
1186
1250
|
const readings = SED_OPTION_DIALECTS.map((dialect) => readSedInvocation(args, dialect));
|
|
@@ -1286,7 +1350,7 @@ const BUN_INLINE = {
|
|
|
1286
1350
|
...BLANK_INLINE, longText: new Set(["--eval", "--print"]), textLetters: "ep",
|
|
1287
1351
|
separatedValueLetters: "r", bannerLetters: "vh", stdinWhenUnnamed: false,
|
|
1288
1352
|
};
|
|
1289
|
-
const INTERPRETER_INLINE_PROGRAMS = new Map([
|
|
1353
|
+
export const INTERPRETER_INLINE_PROGRAMS = new Map([
|
|
1290
1354
|
["perl", PERL_INLINE],
|
|
1291
1355
|
["ruby", RUBY_INLINE],
|
|
1292
1356
|
["python", PYTHON_INLINE],
|
|
@@ -1314,10 +1378,10 @@ const INTERPRETER_INLINE_PROGRAMS = new Map([
|
|
|
1314
1378
|
["groovy", GROOVY_INLINE],
|
|
1315
1379
|
["bun", BUN_INLINE],
|
|
1316
1380
|
]);
|
|
1317
|
-
function interpreterKey(name) {
|
|
1381
|
+
export function interpreterKey(name) {
|
|
1318
1382
|
return name.replace(/\d+(?:\.\d+)*$/, "");
|
|
1319
1383
|
}
|
|
1320
|
-
function inlineProgramClause(model, args) {
|
|
1384
|
+
export function inlineProgramClause(model, args) {
|
|
1321
1385
|
let endOfOptions = false;
|
|
1322
1386
|
let banner = false;
|
|
1323
1387
|
let interactive = false;
|
|
@@ -1527,7 +1591,7 @@ function guardedVerbFormReason(name, toks, raw) {
|
|
|
1527
1591
|
}
|
|
1528
1592
|
return undefined;
|
|
1529
1593
|
}
|
|
1530
|
-
function segmentSelectsRecursiveRead(name, args) {
|
|
1594
|
+
export function segmentSelectsRecursiveRead(name, args) {
|
|
1531
1595
|
const model = RECURSIVE_READ_FORMS[name];
|
|
1532
1596
|
if (model === undefined)
|
|
1533
1597
|
return false;
|
|
@@ -1604,7 +1668,7 @@ function isGrepFileStdinLongOption(tok) {
|
|
|
1604
1668
|
}
|
|
1605
1669
|
const CLUSTER_OPTION_CHAR = /[A-Za-z0-9]/;
|
|
1606
1670
|
const ASSIGNMENT_OR_APPEND_WORD = /^[A-Za-z_][A-Za-z0-9_]*\+?=/;
|
|
1607
|
-
function assignmentWordPayload(word) {
|
|
1671
|
+
export function assignmentWordPayload(word) {
|
|
1608
1672
|
if (!ASSIGNMENT_OR_APPEND_WORD.test(word))
|
|
1609
1673
|
return undefined;
|
|
1610
1674
|
const eq = word.indexOf("=");
|
|
@@ -1615,8 +1679,8 @@ function assignmentWordPayload(word) {
|
|
|
1615
1679
|
return { kind: "unresolved", reason: `"${word}" carries a tilde whose expansion the text does not settle` };
|
|
1616
1680
|
return value === "" ? undefined : { kind: "value", value };
|
|
1617
1681
|
}
|
|
1618
|
-
const ASSIGNMENT_VALUE_NOT_PATHS = new Set(["make", "gmake", "bmake", "cmake", "ninja", "meson", "docker", "podman"]);
|
|
1619
|
-
function argvProgramOf(tokens) {
|
|
1682
|
+
export const ASSIGNMENT_VALUE_NOT_PATHS = new Set(["make", "gmake", "bmake", "cmake", "ninja", "meson", "docker", "podman"]);
|
|
1683
|
+
export function argvProgramOf(tokens) {
|
|
1620
1684
|
let lead = 0;
|
|
1621
1685
|
while (lead < tokens.folded.length && ASSIGNMENT_OR_APPEND_WORD.test(tokens.folded[lead]))
|
|
1622
1686
|
lead++;
|
|
@@ -1626,7 +1690,7 @@ function argvProgramOf(tokens) {
|
|
|
1626
1690
|
const at = lead + pos.program.index;
|
|
1627
1691
|
return ASSIGNMENT_OR_APPEND_WORD.test(tokens.folded[at] ?? "") ? undefined : { program: pos.program.basename, from: at };
|
|
1628
1692
|
}
|
|
1629
|
-
function attachedOptionPayloads(tok) {
|
|
1693
|
+
export function attachedOptionPayloads(tok) {
|
|
1630
1694
|
const literal = literalTildeSpelling;
|
|
1631
1695
|
const out = [];
|
|
1632
1696
|
if (tok.startsWith("--")) {
|
|
@@ -2309,7 +2373,7 @@ export function classifySimpleCommandReadBoundary(command, boundary) {
|
|
|
2309
2373
|
}
|
|
2310
2374
|
const NOT_GATED = { gated: false, paths: [], checked: [], undecided: [] };
|
|
2311
2375
|
const UNREADABLE_WHITELIST_WORD = /[$`()*?[\]{}]/;
|
|
2312
|
-
const UNMODELLED_BASE_MOVERS = new Set(["pushd", "popd"]);
|
|
2376
|
+
export const UNMODELLED_BASE_MOVERS = new Set(["pushd", "popd"]);
|
|
2313
2377
|
export const COMMAND_LAUNCHERS_FOR_TEST = COMMAND_LAUNCHERS;
|
|
2314
2378
|
const LAUNCHER_SCAN_HEAD = "cat";
|
|
2315
2379
|
export function classifyOutOfRootReadGate(command, allow, boundary) {
|
package/dist/tools/fs/fs-bash.js
CHANGED
|
@@ -10,7 +10,7 @@ import { delimitUntrusted } from "../../core/untrusted-text.js";
|
|
|
10
10
|
import { sliceHeadSafe, sliceTailSafe } from "../../core/surrogate-safe-slice.js";
|
|
11
11
|
import { MCP_IMAGE_MAX_BASE64 } from "../../core/mcp.js";
|
|
12
12
|
import { imageMagicMatches, normalizeAbsPathLexically, violationDetails, withinAnyRoot } from "./safety.js";
|
|
13
|
-
import { boundaryIsTheSandbox,
|
|
13
|
+
import { boundaryIsTheSandbox, hasDestroy, isIsolated } from "../../core/remote-env.js";
|
|
14
14
|
import { ghRateLimitHint } from "./gh-rate-limit.js";
|
|
15
15
|
import { resolveBashTimeoutCaps, bashTimeoutCapsSec, bashTimeoutArgRefusal, bashTimeoutParamDescription, envErrorDetail, bashMaxOutputChars, clipShellOutput, writeShellOverflowFile, createShellOverflowSpoolFence, shellRecoveryHint, CWD_SENTINEL, stripCwdSentinel, BASH_READONLY_CONFINEMENT_NOTE, } from "./fs-shared.js";
|
|
16
16
|
import { PROBE_CAUSE_PATH_MAX, PROBE_REASON_MAX, inlineUntrusted } from "../../core/untrusted-text.js";
|
|
@@ -1037,330 +1037,7 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
|
|
|
1037
1037
|
},
|
|
1038
1038
|
});
|
|
1039
1039
|
}
|
|
1040
|
-
|
|
1041
|
-
const MAX_GLOB_DIRS = 256;
|
|
1042
|
-
const MAX_GLOB_MATCHES = 8192;
|
|
1043
|
-
function compileGlobSegment(pattern) {
|
|
1044
|
-
if (pattern.includes("\\"))
|
|
1045
|
-
return undefined;
|
|
1046
|
-
let src = "";
|
|
1047
|
-
for (let i = 0; i < pattern.length; i++) {
|
|
1048
|
-
const ch = pattern[i];
|
|
1049
|
-
if (ch === "*") {
|
|
1050
|
-
src += "[^/]*";
|
|
1051
|
-
}
|
|
1052
|
-
else if (ch === "?") {
|
|
1053
|
-
src += "[^/]";
|
|
1054
|
-
}
|
|
1055
|
-
else if (ch === "[") {
|
|
1056
|
-
const cls = parseBracketExpression(pattern, i);
|
|
1057
|
-
if (cls === undefined)
|
|
1058
|
-
src += "\\[";
|
|
1059
|
-
else {
|
|
1060
|
-
src += cls.source;
|
|
1061
|
-
i = cls.end;
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
else {
|
|
1065
|
-
src += ch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
try {
|
|
1069
|
-
return new RegExp(`^${src}$`, "s");
|
|
1070
|
-
}
|
|
1071
|
-
catch {
|
|
1072
|
-
return undefined;
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
1075
|
-
function parseBracketExpression(pattern, start) {
|
|
1076
|
-
let i = start + 1;
|
|
1077
|
-
let negate = false;
|
|
1078
|
-
if (pattern[i] === "!" || pattern[i] === "^") {
|
|
1079
|
-
negate = true;
|
|
1080
|
-
i++;
|
|
1081
|
-
}
|
|
1082
|
-
const contentStart = i;
|
|
1083
|
-
let body = "";
|
|
1084
|
-
let anyChar = false;
|
|
1085
|
-
for (; i < pattern.length; i++) {
|
|
1086
|
-
const ch = pattern[i];
|
|
1087
|
-
if (ch === "]" && i > contentStart) {
|
|
1088
|
-
if (body.length === 0 && !anyChar)
|
|
1089
|
-
return undefined;
|
|
1090
|
-
return { source: anyChar ? "[^/]" : `[${negate ? "^" : ""}${body}]`, end: i };
|
|
1091
|
-
}
|
|
1092
|
-
if (ch === "[" && (pattern[i + 1] === ":" || pattern[i + 1] === "." || pattern[i + 1] === "=")) {
|
|
1093
|
-
const close = pattern.indexOf(`${pattern[i + 1]}]`, i + 2);
|
|
1094
|
-
if (close < 0)
|
|
1095
|
-
return undefined;
|
|
1096
|
-
anyChar = true;
|
|
1097
|
-
i = close + 1;
|
|
1098
|
-
continue;
|
|
1099
|
-
}
|
|
1100
|
-
body += ch === "-" ? "-" : ch.replace(/[\]\\^]/g, "\\$&");
|
|
1101
|
-
}
|
|
1102
|
-
return undefined;
|
|
1103
|
-
}
|
|
1104
|
-
function globSegmentMatches(re, pattern, name) {
|
|
1105
|
-
if (name.startsWith(".") && !pattern.startsWith("."))
|
|
1106
|
-
return false;
|
|
1107
|
-
return re.test(name);
|
|
1108
|
-
}
|
|
1109
|
-
function joinCandidate(dir, name) {
|
|
1110
|
-
return `${dir.replace(/[\\/]+$/, "")}/${name}`;
|
|
1111
|
-
}
|
|
1112
|
-
function pathSegmentsFrom(p, from) {
|
|
1113
|
-
const segs = [];
|
|
1114
|
-
let scan = from;
|
|
1115
|
-
while (scan < p.length) {
|
|
1116
|
-
while (scan < p.length && (p[scan] === "/" || p[scan] === "\\"))
|
|
1117
|
-
scan++;
|
|
1118
|
-
const s = scan;
|
|
1119
|
-
while (scan < p.length && p[scan] !== "/" && p[scan] !== "\\")
|
|
1120
|
-
scan++;
|
|
1121
|
-
if (scan > s)
|
|
1122
|
-
segs.push({ text: p.slice(s, scan), start: s });
|
|
1123
|
-
}
|
|
1124
|
-
return segs;
|
|
1125
|
-
}
|
|
1126
|
-
function createCanonicalReadBoundary(env, readRoots) {
|
|
1127
|
-
const lexicalOnly = boundaryIsTheSandbox(env);
|
|
1128
|
-
const resolverIsAuthoritative = canonicalPathIsAuthoritative(env);
|
|
1129
|
-
let rootsOnce;
|
|
1130
|
-
const canonicalRoots = async (signal) => {
|
|
1131
|
-
rootsOnce ??= (async () => {
|
|
1132
|
-
const out = [];
|
|
1133
|
-
for (const r of readRoots) {
|
|
1134
|
-
const c = await env.canonicalPath(r, signal);
|
|
1135
|
-
out.push(c.ok ? c.value : r);
|
|
1136
|
-
}
|
|
1137
|
-
return out;
|
|
1138
|
-
})();
|
|
1139
|
-
return await rootsOnce;
|
|
1140
|
-
};
|
|
1141
|
-
const resolveWithoutParentSteps = async (p, signal) => {
|
|
1142
|
-
const parts = p.split(/[\\/]/);
|
|
1143
|
-
const tail = [];
|
|
1144
|
-
let allAbsent = true;
|
|
1145
|
-
while (parts.length > 0) {
|
|
1146
|
-
const canon = await env.canonicalPath(parts.join("/") || "/", signal);
|
|
1147
|
-
if (canon.ok) {
|
|
1148
|
-
if (tail.length === 0)
|
|
1149
|
-
return { value: canon.value, exact: true };
|
|
1150
|
-
return { value: [canon.value.replace(/[\\/]+$/, ""), ...[...tail].reverse()].join("/"), exact: false, missing: allAbsent };
|
|
1151
|
-
}
|
|
1152
|
-
if (canon.error.code !== "not_found" && canon.error.code !== "not_directory")
|
|
1153
|
-
allAbsent = false;
|
|
1154
|
-
tail.push(parts.pop());
|
|
1155
|
-
}
|
|
1156
|
-
return undefined;
|
|
1157
|
-
};
|
|
1158
|
-
const parentOf = (canonical) => normalizeAbsPathLexically(`${canonical}/..`);
|
|
1159
|
-
const resolve = async (p, signal) => {
|
|
1160
|
-
const parts = p.split(/[\\/]/);
|
|
1161
|
-
const first = parts.findIndex((seg) => seg === "..");
|
|
1162
|
-
if (first < 0)
|
|
1163
|
-
return await resolveWithoutParentSteps(p, signal);
|
|
1164
|
-
const headEnd = parts.slice(0, first).reduce((n, seg) => n + seg.length + 1, 0);
|
|
1165
|
-
let head = await resolveWithoutParentSteps(p.slice(0, headEnd) || "/", signal);
|
|
1166
|
-
if (head === undefined)
|
|
1167
|
-
return undefined;
|
|
1168
|
-
let pending = [];
|
|
1169
|
-
const flush = async () => {
|
|
1170
|
-
if (pending.length === 0)
|
|
1171
|
-
return true;
|
|
1172
|
-
const next = await resolveWithoutParentSteps([head.value.replace(/[\\/]+$/, ""), ...pending].join("/"), signal);
|
|
1173
|
-
pending = [];
|
|
1174
|
-
if (next === undefined)
|
|
1175
|
-
return false;
|
|
1176
|
-
head = next;
|
|
1177
|
-
return true;
|
|
1178
|
-
};
|
|
1179
|
-
for (let i = first; i < parts.length; i++) {
|
|
1180
|
-
const seg = parts[i];
|
|
1181
|
-
if (seg === "" || seg === ".")
|
|
1182
|
-
continue;
|
|
1183
|
-
if (seg !== "..") {
|
|
1184
|
-
pending.push(seg);
|
|
1185
|
-
continue;
|
|
1186
|
-
}
|
|
1187
|
-
if (!(await flush()))
|
|
1188
|
-
return undefined;
|
|
1189
|
-
if (!head.exact)
|
|
1190
|
-
return head;
|
|
1191
|
-
head = { value: parentOf(head.value), exact: true };
|
|
1192
|
-
}
|
|
1193
|
-
return (await flush()) ? head : undefined;
|
|
1194
|
-
};
|
|
1195
|
-
const literalRootHead = (p, roots) => {
|
|
1196
|
-
let best;
|
|
1197
|
-
for (const r of [...readRoots, ...roots]) {
|
|
1198
|
-
if (!r)
|
|
1199
|
-
continue;
|
|
1200
|
-
const withSep = r.endsWith("/") || r.endsWith("\\") ? r : `${r}/`;
|
|
1201
|
-
if (p !== r && !p.startsWith(withSep))
|
|
1202
|
-
continue;
|
|
1203
|
-
if (best === undefined || r.length > best.length)
|
|
1204
|
-
best = r;
|
|
1205
|
-
}
|
|
1206
|
-
return best;
|
|
1207
|
-
};
|
|
1208
|
-
const expandGlobCandidate = async (p, roots, signal) => {
|
|
1209
|
-
const head = literalRootHead(p, roots);
|
|
1210
|
-
let segs = pathSegmentsFrom(p, head?.length ?? 0);
|
|
1211
|
-
let first = segs.findIndex((s) => GLOB_METACHAR_RE.test(s.text));
|
|
1212
|
-
if (first < 0 && head !== undefined && GLOB_METACHAR_RE.test(head)) {
|
|
1213
|
-
segs = pathSegmentsFrom(p, 0);
|
|
1214
|
-
first = segs.findIndex((s) => GLOB_METACHAR_RE.test(s.text));
|
|
1215
|
-
}
|
|
1216
|
-
if (first < 0)
|
|
1217
|
-
return undefined;
|
|
1218
|
-
let frontier = [p.slice(0, segs[first].start).replace(/[\\/]+$/, "") || "/"];
|
|
1219
|
-
let dirsUsed = 0;
|
|
1220
|
-
let matches = 0;
|
|
1221
|
-
const canonicalOfMatch = new Map();
|
|
1222
|
-
for (let j = first; j < segs.length; j++) {
|
|
1223
|
-
const seg = segs[j].text;
|
|
1224
|
-
if (!GLOB_METACHAR_RE.test(seg)) {
|
|
1225
|
-
frontier = frontier.map((d) => joinCandidate(d, seg));
|
|
1226
|
-
continue;
|
|
1227
|
-
}
|
|
1228
|
-
const re = compileGlobSegment(seg);
|
|
1229
|
-
if (re === undefined) {
|
|
1230
|
-
return { unverifiable: `the pattern "${seg}" cannot be expanded to verify what it would read` };
|
|
1231
|
-
}
|
|
1232
|
-
const isLast = j === segs.length - 1;
|
|
1233
|
-
const next = [];
|
|
1234
|
-
for (const dir of frontier) {
|
|
1235
|
-
if (++dirsUsed > MAX_GLOB_DIRS) {
|
|
1236
|
-
return { unverifiable: `"${p}" expands over more than ${MAX_GLOB_DIRS} directories, too many to enumerate to verify` };
|
|
1237
|
-
}
|
|
1238
|
-
const canonDir = await resolve(dir, signal);
|
|
1239
|
-
if (canonDir !== undefined && canonDir.missing === true)
|
|
1240
|
-
continue;
|
|
1241
|
-
if (canonDir !== undefined && !canonDir.exact) {
|
|
1242
|
-
return { unverifiable: `"${dir}" cannot be resolved to verify what "${seg}" would expand to` };
|
|
1243
|
-
}
|
|
1244
|
-
const listed = await env.listDir(canonDir?.value ?? dir, signal);
|
|
1245
|
-
if (!listed.ok) {
|
|
1246
|
-
if (listed.error.code === "not_found" || listed.error.code === "not_directory")
|
|
1247
|
-
continue;
|
|
1248
|
-
return { unverifiable: `"${dir}" cannot be enumerated to verify what "${seg}" would expand to (${listed.error.code})` };
|
|
1249
|
-
}
|
|
1250
|
-
if (seg.startsWith(".")) {
|
|
1251
|
-
for (const dot of [".", ".."])
|
|
1252
|
-
if (re.test(dot))
|
|
1253
|
-
next.push(joinCandidate(dir, dot));
|
|
1254
|
-
}
|
|
1255
|
-
const canonDirHere = isLast ? canonDir?.value : undefined;
|
|
1256
|
-
for (const entry of listed.value) {
|
|
1257
|
-
if (!globSegmentMatches(re, seg, entry.name))
|
|
1258
|
-
continue;
|
|
1259
|
-
if (++matches > MAX_GLOB_MATCHES) {
|
|
1260
|
-
return { unverifiable: `"${p}" expands to more than ${MAX_GLOB_MATCHES} names, too many to enumerate to verify` };
|
|
1261
|
-
}
|
|
1262
|
-
const candidate = joinCandidate(dir, entry.name);
|
|
1263
|
-
if (canonDirHere !== undefined && entry.kind !== "symlink")
|
|
1264
|
-
canonicalOfMatch.set(candidate, joinCandidate(canonDirHere, entry.name));
|
|
1265
|
-
next.push(candidate);
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
frontier = next;
|
|
1269
|
-
if (frontier.length === 0)
|
|
1270
|
-
return { paths: [], canonical: canonicalOfMatch };
|
|
1271
|
-
}
|
|
1272
|
-
return { paths: frontier, canonical: canonicalOfMatch };
|
|
1273
|
-
};
|
|
1274
|
-
return {
|
|
1275
|
-
escapesAfterResolution: async (paths, ask, signal) => {
|
|
1276
|
-
const deny = ask.deny;
|
|
1277
|
-
if (lexicalOnly || (paths.literal.length === 0 && paths.patterns.length === 0))
|
|
1278
|
-
return { escaping: [] };
|
|
1279
|
-
const roots = await canonicalRoots(signal);
|
|
1280
|
-
const escaping = [];
|
|
1281
|
-
let unresolvable;
|
|
1282
|
-
let denied;
|
|
1283
|
-
const denyHit = (p) => {
|
|
1284
|
-
if (deny === undefined || denied !== undefined)
|
|
1285
|
-
return false;
|
|
1286
|
-
const folded = normalizeAbsPathLexically(p);
|
|
1287
|
-
if (ask.denyAnswered.has(folded))
|
|
1288
|
-
return false;
|
|
1289
|
-
const pattern = deny(folded);
|
|
1290
|
-
if (pattern === null)
|
|
1291
|
-
return false;
|
|
1292
|
-
denied = { path: folded, pattern };
|
|
1293
|
-
return true;
|
|
1294
|
-
};
|
|
1295
|
-
const judge = async (p, canonicalKnown, origin) => {
|
|
1296
|
-
if (denyHit(p))
|
|
1297
|
-
return;
|
|
1298
|
-
const real = canonicalKnown !== undefined ? { value: canonicalKnown, exact: true } : await resolve(p, signal);
|
|
1299
|
-
if (real === undefined) {
|
|
1300
|
-
if (resolverIsAuthoritative)
|
|
1301
|
-
unresolvable ??= `"${p}" cannot be resolved to verify where it reads`;
|
|
1302
|
-
return;
|
|
1303
|
-
}
|
|
1304
|
-
if (!real.exact && real.missing !== true) {
|
|
1305
|
-
unresolvable ??= `"${p}" cannot be resolved to verify where it reads`;
|
|
1306
|
-
return;
|
|
1307
|
-
}
|
|
1308
|
-
if (denyHit(real.value))
|
|
1309
|
-
return;
|
|
1310
|
-
if (ask.containmentAnswered.has(normalizeAbsPathLexically(origin ?? p)))
|
|
1311
|
-
return;
|
|
1312
|
-
if (ask.containment && !withinAnyRoot(roots, real.value) && !escaping.includes(real.value))
|
|
1313
|
-
escaping.push(real.value);
|
|
1314
|
-
};
|
|
1315
|
-
for (const p of paths.literal) {
|
|
1316
|
-
if (denied !== undefined)
|
|
1317
|
-
break;
|
|
1318
|
-
await judge(p);
|
|
1319
|
-
}
|
|
1320
|
-
for (const p of paths.patterns) {
|
|
1321
|
-
if (denied !== undefined)
|
|
1322
|
-
break;
|
|
1323
|
-
const expanded = await expandGlobCandidate(p, roots, signal);
|
|
1324
|
-
if (expanded !== undefined && "unverifiable" in expanded) {
|
|
1325
|
-
unresolvable ??= expanded.unverifiable;
|
|
1326
|
-
continue;
|
|
1327
|
-
}
|
|
1328
|
-
if (expanded !== undefined && expanded.paths.length > 0) {
|
|
1329
|
-
for (const t of expanded.paths) {
|
|
1330
|
-
if (denied !== undefined)
|
|
1331
|
-
break;
|
|
1332
|
-
await judge(t, expanded.canonical.get(t), p);
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
else
|
|
1336
|
-
await judge(p);
|
|
1337
|
-
}
|
|
1338
|
-
if (denied !== undefined)
|
|
1339
|
-
return { escaping, denied };
|
|
1340
|
-
return unresolvable === undefined ? { escaping } : { escaping, unverifiable: unresolvable };
|
|
1341
|
-
},
|
|
1342
|
-
canonicalOf: async (path, signal) => {
|
|
1343
|
-
if (lexicalOnly)
|
|
1344
|
-
return undefined;
|
|
1345
|
-
const real = await resolve(path, signal);
|
|
1346
|
-
return real === undefined || (!real.exact && real.missing !== true) ? undefined : real.value;
|
|
1347
|
-
},
|
|
1348
|
-
allResolveInside: async (paths, signal) => {
|
|
1349
|
-
if (lexicalOnly || paths.length === 0)
|
|
1350
|
-
return false;
|
|
1351
|
-
const roots = await canonicalRoots(signal);
|
|
1352
|
-
for (const p of paths) {
|
|
1353
|
-
const real = await resolve(p, signal);
|
|
1354
|
-
if (real === undefined || (!real.exact && real.missing !== true))
|
|
1355
|
-
return false;
|
|
1356
|
-
if (!withinAnyRoot(roots, real.value))
|
|
1357
|
-
return false;
|
|
1358
|
-
}
|
|
1359
|
-
return true;
|
|
1360
|
-
},
|
|
1361
|
-
};
|
|
1362
|
-
}
|
|
1363
|
-
const NOTHING_ANSWERED = new Set();
|
|
1040
|
+
import { createCanonicalReadBoundary, NOTHING_ANSWERED } from "./shell-read-establish.js";
|
|
1364
1041
|
export function createShellCanonicalRecheck(env, readBoundary, face) {
|
|
1365
1042
|
const recheckReaders = new Set(bashClassifyDefaultAllow());
|
|
1366
1043
|
let canonicalForRoots;
|