@sema-agent/core 5.62.0 → 5.64.0
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 +96 -0
- package/dist/agents/cascade.d.ts +5 -1
- package/dist/agents/cascade.js +6 -1
- package/dist/agents/subagent.d.ts +12 -2
- package/dist/agents/subagent.js +4 -2
- package/dist/agents/verify.d.ts +5 -1
- package/dist/agents/verify.js +5 -2
- package/dist/core/auto-compaction.d.ts +6 -4
- package/dist/core/auto-compaction.js +3 -0
- package/dist/core/checkpoint-store.d.ts +5 -1
- package/dist/core/context-edit.d.ts +36 -29
- package/dist/core/context-edit.js +3 -3
- package/dist/core/fs-write-gate-policy.d.ts +21 -0
- package/dist/core/fs-write-gate-policy.js +14 -3
- package/dist/core/hooks.d.ts +8 -5
- package/dist/core/memory-engine/engine.d.ts +11 -0
- package/dist/core/memory-engine/engine.js +29 -3
- package/dist/core/memory-engine/index.d.ts +1 -1
- package/dist/core/memory-engine/origin-clearance.d.ts +28 -0
- package/dist/core/remote-env.d.ts +34 -2
- package/dist/core/runner/prepare-config-doors.d.ts +2 -2
- package/dist/core/runner/prepare-config-doors.js +11 -8
- package/dist/core/runner/prepare-task.d.ts +87 -5
- package/dist/core/runner/prepare-task.js +174 -92
- package/dist/core/runner/prepare-workspace-restore.js +13 -0
- package/dist/core/runner/runtask.js +203 -152
- package/dist/core/trace.d.ts +5 -4
- package/dist/core/types.d.ts +38 -20
- package/dist/core/usage-window-store.d.ts +44 -12
- package/dist/core/usage-window-store.js +11 -3
- package/dist/core/workflow-run-store-contract.js +17 -0
- package/dist/core/workflow-run-store.d.ts +22 -1
- package/dist/core/workflow-run-store.js +1 -0
- package/dist/engine/harness/agent-harness.d.ts +8 -3
- package/dist/engine/harness/agent-harness.js +29 -9
- package/dist/engine/harness/types.d.ts +127 -3
- package/dist/engine/loop/agent-loop.js +47 -4
- package/dist/engine/loop/types.d.ts +67 -6
- package/dist/index.d.ts +2 -2
- package/dist/internal/harness-types.d.ts +1 -1
- package/dist/orchestration/run-workflow-tool.d.ts +6 -0
- package/dist/orchestration/run-workflow-tool.js +1 -0
- package/dist/orchestration/workflow-types.d.ts +48 -1
- package/dist/orchestration/workflow-types.js +12 -4
- package/dist/orchestration/workflow.d.ts +18 -1
- package/dist/orchestration/workflow.js +45 -19
- package/dist/prompts/default.js +1 -1
- package/dist/tools/fs/bash-readonly-classifier.d.ts +44 -1
- package/dist/tools/fs/bash-readonly-classifier.js +132 -5
- package/dist/tools/fs/fs-bash.js +9 -2
- package/dist/tools/fs/fs-write.js +19 -8
- package/dist/tools/fs/index.d.ts +1 -0
- package/dist/tools/fs/index.js +1 -0
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +7 -1
|
@@ -3,7 +3,10 @@ export const NOT_AUTO_ALLOWED = "— not auto-allowed";
|
|
|
3
3
|
export const BASH_READONLY_DEFAULT_ALLOW = [
|
|
4
4
|
"ls", "cat", "head", "tail", "wc", "pwd", "echo", "whoami", "uname",
|
|
5
5
|
"grep", "cut", "tr", "basename", "dirname", "stat", "du", "df", "which",
|
|
6
|
+
"cal", "uptime", "id", "groups", "nproc", "locale", "free", "realpath", "readlink",
|
|
7
|
+
"true", "false", "seq", "expr", "type", "sleep", "diff", "cmp", "comm",
|
|
6
8
|
];
|
|
9
|
+
export const BASH_CLASSIFY_DEFAULT_ALLOW = [...BASH_READONLY_DEFAULT_ALLOW, "find", "sed", "cd"];
|
|
7
10
|
const SHELL_OPERATORS = /[;&|<>$()`\n\r\\]/;
|
|
8
11
|
function quoteMask(s) {
|
|
9
12
|
const quoted = new Array(s.length).fill(false);
|
|
@@ -166,7 +169,10 @@ export function formatOutOfRootReadApprovalOption(directory) {
|
|
|
166
169
|
const leaf = cut >= 0 ? trimmed.slice(cut + 1) : trimmed;
|
|
167
170
|
return `Yes, allow reading from ${leaf || directory}/ from this project`;
|
|
168
171
|
}
|
|
169
|
-
const NO_PATH_OPERAND_COMMANDS = new Set([
|
|
172
|
+
const NO_PATH_OPERAND_COMMANDS = new Set([
|
|
173
|
+
"pwd", "echo", "whoami", "uname", "which", "tr", "basename", "dirname",
|
|
174
|
+
"cal", "uptime", "id", "groups", "nproc", "locale", "free", "true", "false", "seq", "expr", "type", "sleep",
|
|
175
|
+
]);
|
|
170
176
|
function isPathShapedToken(tok) {
|
|
171
177
|
return tok.includes("/") || tok.startsWith("~") || tok === "." || tok === "..";
|
|
172
178
|
}
|
|
@@ -311,6 +317,8 @@ const SEPARATED_VALUE_OPTIONS = {
|
|
|
311
317
|
"exclude-dir",
|
|
312
318
|
],
|
|
313
319
|
},
|
|
320
|
+
cmp: { short: "ni", long: ["bytes", "ignore-initial"] },
|
|
321
|
+
diff: { short: "UCWIFxL", ownsValueButNotSkippable: "SX", long: ["unified", "context", "width", "ignore-matching-lines", "show-function-line", "exclude", "label", "horizon-lines", "tabsize"] },
|
|
314
322
|
};
|
|
315
323
|
function takesSeparatedValue(name, tok) {
|
|
316
324
|
const model = SEPARATED_VALUE_OPTIONS[name];
|
|
@@ -353,6 +361,98 @@ const RECURSIVE_READ_FORMS = {
|
|
|
353
361
|
tar: { shortLetters: "cru", valueOwners: "fCTXbg", longNames: ["create", "append", "update"], bundledModeLetters: "cru", dashIsStdin: true },
|
|
354
362
|
diff: { shortLetters: "r", valueOwners: "UCWISFXx", longNames: ["recursive"], dashIsStdin: true },
|
|
355
363
|
};
|
|
364
|
+
const FIND_DANGEROUS_PREDICATES = new Set([
|
|
365
|
+
"-delete", "-exec", "-execdir", "-ok", "-okdir", "-fprint", "-fprint0", "-fls", "-fprintf", "-files0-from",
|
|
366
|
+
]);
|
|
367
|
+
const SED_READONLY_FLAGS = new Set([
|
|
368
|
+
"-n", "--quiet", "--silent", "-E", "--regexp-extended", "-r", "-z", "--zero-terminated", "--posix",
|
|
369
|
+
]);
|
|
370
|
+
const SED_READONLY_CLUSTER_LETTERS = new Set(["n", "E", "r", "z"]);
|
|
371
|
+
const SED_READONLY_SCRIPT_PIECE = /^(?:\d+|\d+,\d+)?p$/;
|
|
372
|
+
function guardedVerbFormReason(name, toks, raw) {
|
|
373
|
+
if (name === "find") {
|
|
374
|
+
for (const t of toks) {
|
|
375
|
+
if (FIND_DANGEROUS_PREDICATES.has(t)) {
|
|
376
|
+
return `\`find ${t}\` executes a command or writes a file — not a read ${NOT_AUTO_ALLOWED}`;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
for (const rt of raw.slice(1)) {
|
|
380
|
+
if (hasUnquotedGlobMetachar(rt) || hasUnquotedExpansionMetachar(rt)) {
|
|
381
|
+
return `\`find\` is given the unquoted pattern "${rt}", which the shell may expand into other arguments before find runs ${NOT_AUTO_ALLOWED}`;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return undefined;
|
|
385
|
+
}
|
|
386
|
+
if (name === "sed") {
|
|
387
|
+
for (const rt of raw.slice(1)) {
|
|
388
|
+
if (hasUnquotedGlobMetachar(rt) || hasUnquotedExpansionMetachar(rt)) {
|
|
389
|
+
return `\`sed\` is given the unquoted pattern "${rt}", which the shell may expand into other arguments (a file named like a flag becomes a live option) before sed runs ${NOT_AUTO_ALLOWED}`;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
const flags = [];
|
|
393
|
+
const operands = [];
|
|
394
|
+
let endOfOptions = false;
|
|
395
|
+
for (const t of toks.slice(1)) {
|
|
396
|
+
if (!endOfOptions && t === "--") {
|
|
397
|
+
endOfOptions = true;
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
if (!endOfOptions && t.startsWith("-")) {
|
|
401
|
+
flags.push(t);
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
operands.push(t);
|
|
405
|
+
}
|
|
406
|
+
for (const f of flags) {
|
|
407
|
+
if (SED_READONLY_FLAGS.has(f))
|
|
408
|
+
continue;
|
|
409
|
+
if (/^-[A-Za-z]+$/.test(f) && f.length > 2 && [...f.slice(1)].every((ch) => SED_READONLY_CLUSTER_LETTERS.has(ch)))
|
|
410
|
+
continue;
|
|
411
|
+
return `\`sed ${f}\` is outside the read-only sed form (-n plus print-only script) ${NOT_AUTO_ALLOWED}`;
|
|
412
|
+
}
|
|
413
|
+
const quiet = flags.some((f) => f === "-n" || f === "--quiet" || f === "--silent" || (/^-[A-Za-z]+$/.test(f) && f.includes("n")));
|
|
414
|
+
if (!quiet)
|
|
415
|
+
return `\`sed\` without \`-n\` echoes its whole input — only the -n print-only form is read-classified ${NOT_AUTO_ALLOWED}`;
|
|
416
|
+
const script = operands[0];
|
|
417
|
+
if (script === undefined)
|
|
418
|
+
return `\`sed\` with no script ${NOT_AUTO_ALLOWED}`;
|
|
419
|
+
for (const piece of script.split(";")) {
|
|
420
|
+
if (!SED_READONLY_SCRIPT_PIECE.test(piece.trim())) {
|
|
421
|
+
return `\`sed\` script "${script}" is not a pure print command (N[,M]p) ${NOT_AUTO_ALLOWED}`;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return undefined;
|
|
425
|
+
}
|
|
426
|
+
if (name === "free") {
|
|
427
|
+
for (const t of toks.slice(1)) {
|
|
428
|
+
if (/^--seconds(=.*)?$/.test(t) || /^-[A-Za-z0-9]*s[A-Za-z0-9]*$/.test(t)) {
|
|
429
|
+
return `\`free ${t}\` repeats until killed — it never terminates on its own ${NOT_AUTO_ALLOWED}`;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return undefined;
|
|
433
|
+
}
|
|
434
|
+
if (name === "sleep") {
|
|
435
|
+
return pollLoopSleepReason(toks.join(" "));
|
|
436
|
+
}
|
|
437
|
+
if (name === "seq") {
|
|
438
|
+
for (const t of toks.slice(1)) {
|
|
439
|
+
if (t.startsWith("-") && t !== "-" && !/^-\d/.test(t))
|
|
440
|
+
continue;
|
|
441
|
+
if (!/^-?\d{1,6}$/.test(t)) {
|
|
442
|
+
return `\`seq ${t}\` is not a small literal integer bound — the output cannot be bounded statically ${NOT_AUTO_ALLOWED}`;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return undefined;
|
|
446
|
+
}
|
|
447
|
+
if (name === "diff") {
|
|
448
|
+
for (const t of toks.slice(1)) {
|
|
449
|
+
if (/^--[A-Za-z-]+=-$/.test(t) || /^-[A-Za-z]*[SX]-$/.test(t)) {
|
|
450
|
+
return `\`diff ${t}\` names stdin inside a fused option — a read this check cannot see through ${NOT_AUTO_ALLOWED}`;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return undefined;
|
|
455
|
+
}
|
|
356
456
|
function segmentSelectsRecursiveRead(name, args) {
|
|
357
457
|
const model = RECURSIVE_READ_FORMS[name];
|
|
358
458
|
if (model === undefined)
|
|
@@ -577,7 +677,7 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
|
|
|
577
677
|
}
|
|
578
678
|
return findings;
|
|
579
679
|
}
|
|
580
|
-
export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
|
|
680
|
+
export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts) {
|
|
581
681
|
const trimmed = command.trim();
|
|
582
682
|
if (!trimmed)
|
|
583
683
|
return { reason: "empty command" };
|
|
@@ -622,7 +722,7 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
|
|
|
622
722
|
}
|
|
623
723
|
return byteBound;
|
|
624
724
|
};
|
|
625
|
-
const STDIN_FILE_FLOOR = { cat: 1, grep: 2, head: 1, tail: 1, wc: 1, cut: 1, tr: Infinity };
|
|
725
|
+
const STDIN_FILE_FLOOR = { cat: 1, grep: 2, head: 1, tail: 1, wc: 1, cut: 1, tr: Infinity, diff: 2, cmp: 2, comm: 2, sed: 2 };
|
|
626
726
|
const foldedSegments = [];
|
|
627
727
|
for (let si = 0; si < segments.length; si++) {
|
|
628
728
|
const segmentTokens = tokenizeSegment(segments[si]);
|
|
@@ -631,6 +731,9 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
|
|
|
631
731
|
continue;
|
|
632
732
|
foldedSegments.push(segmentTokens);
|
|
633
733
|
const name = toks[0];
|
|
734
|
+
const guardReason = guardedVerbFormReason(name, toks, segmentTokens.raw);
|
|
735
|
+
if (guardReason !== undefined)
|
|
736
|
+
return { reason: guardReason };
|
|
634
737
|
if (!(pipeFed[si] ?? false)) {
|
|
635
738
|
const floor = STDIN_FILE_FLOOR[name];
|
|
636
739
|
const restArgs = toks.slice(1);
|
|
@@ -696,8 +799,17 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
|
|
|
696
799
|
return { reason: "reads a device/special file that is either unbounded (/dev/zero, /dev/stdin, /proc/<pid>/fd/0, … — blocks the pipeline until the tool timeout) or process-private (/proc/<pid>/environ, /proc/<pid>/mem, …) " + NOT_AUTO_ALLOWED };
|
|
697
800
|
}
|
|
698
801
|
}
|
|
802
|
+
if (opts?.iterated !== true) {
|
|
803
|
+
const cdSegments = foldedSegments.filter((st) => st.folded[0] === "cd");
|
|
804
|
+
if (cdSegments.length > 1) {
|
|
805
|
+
return { reason: `multiple directory changes in one command require approval for clarity ${NOT_AUTO_ALLOWED}` };
|
|
806
|
+
}
|
|
807
|
+
}
|
|
699
808
|
if (boundary !== undefined)
|
|
700
809
|
return evaluateReadBoundary(foldedSegments, boundary);
|
|
810
|
+
if (foldedSegments.some((st) => st.folded[0] === "cd")) {
|
|
811
|
+
return { reason: `\`cd\` cannot be judged without a read boundary (no base to resolve the directory change against) ${NOT_AUTO_ALLOWED}` };
|
|
812
|
+
}
|
|
701
813
|
return {};
|
|
702
814
|
}
|
|
703
815
|
function evaluateReadBoundary(foldedSegments, boundary) {
|
|
@@ -705,8 +817,10 @@ function evaluateReadBoundary(foldedSegments, boundary) {
|
|
|
705
817
|
const inside = [];
|
|
706
818
|
const undecided = [];
|
|
707
819
|
const recursive = [];
|
|
820
|
+
let currentBase = boundary.cwd ?? boundary.roots[0];
|
|
708
821
|
for (const toks of foldedSegments) {
|
|
709
|
-
|
|
822
|
+
const segBoundary = currentBase === (boundary.cwd ?? boundary.roots[0]) ? boundary : { ...boundary, cwd: currentBase };
|
|
823
|
+
for (const finding of collectSegmentBoundaryFindings(toks, segBoundary)) {
|
|
710
824
|
if (finding.kind === "unresolvable")
|
|
711
825
|
return { reason: finding.reason };
|
|
712
826
|
const denied = boundary.denyMatch?.(finding.path);
|
|
@@ -738,6 +852,14 @@ function evaluateReadBoundary(foldedSegments, boundary) {
|
|
|
738
852
|
if (!outside.some((o) => o.path === finding.path))
|
|
739
853
|
outside.push(finding);
|
|
740
854
|
}
|
|
855
|
+
if (toks.folded[0] === "cd") {
|
|
856
|
+
const cdArgs = toks.folded.slice(1);
|
|
857
|
+
const cdTargetIdx = cdArgs.findIndex((t) => !t.startsWith("-") || t === "-");
|
|
858
|
+
const cdTarget = cdTargetIdx === -1 ? undefined : cdArgs[cdTargetIdx];
|
|
859
|
+
if (cdTarget !== undefined && cdTarget !== "-") {
|
|
860
|
+
currentBase = resolveOperandLexically(currentBase, cdTarget, boundary.homeDir) ?? currentBase;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
741
863
|
}
|
|
742
864
|
const undecidedAll = [...undecided, ...recursive.filter((p) => !undecided.includes(p))];
|
|
743
865
|
const undecidedField = {
|
|
@@ -851,7 +973,12 @@ export function classifyBoundedReadonlyPollLoop(command, allow, boundary) {
|
|
|
851
973
|
if (readSegments.length === 0) {
|
|
852
974
|
return "the loop body has no read command — a sleep-only loop observes nothing and is not auto-allowed";
|
|
853
975
|
}
|
|
854
|
-
const
|
|
976
|
+
const bodyHasCd = readSegments.some((seg) => {
|
|
977
|
+
const p = parseLeadingCommandName(seg);
|
|
978
|
+
return "name" in p && p.name === "cd";
|
|
979
|
+
});
|
|
980
|
+
const modelled = bodyHasCd ? Array.from({ length: beats }, () => readSegments.join("; ")).join("; ") : readSegments.join("; ");
|
|
981
|
+
const verdict = classifyCompoundReadonlyDetailed(modelled, allow, boundary, { iterated: bodyHasCd });
|
|
855
982
|
if (verdict.reason !== undefined)
|
|
856
983
|
return verdict.reason;
|
|
857
984
|
if (verdict.recursiveReadPaths !== undefined) {
|
package/dist/tools/fs/fs-bash.js
CHANGED
|
@@ -11,7 +11,7 @@ import { isRemoteExecutionEnv, hasDestroy, isIsolated } from "../../core/remote-
|
|
|
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";
|
|
14
|
-
import { BASH_READONLY_DEFAULT_ALLOW, coarseReadonlyCheck, classifyBoundedReadonlyPollLoop, classifyCompoundReadonlyDetailed, classifySimpleCommandReadBoundary, NOT_AUTO_ALLOWED, } from "./bash-readonly-classifier.js";
|
|
14
|
+
import { BASH_CLASSIFY_DEFAULT_ALLOW, BASH_READONLY_DEFAULT_ALLOW, coarseReadonlyCheck, classifyBoundedReadonlyPollLoop, classifyCompoundReadonlyDetailed, classifySimpleCommandReadBoundary, NOT_AUTO_ALLOWED, } from "./bash-readonly-classifier.js";
|
|
15
15
|
const RECURSIVE_CAUSE_MAX_PATHS = 3;
|
|
16
16
|
const RECURSIVE_READ_CAUSE_CODE = "shell.recursive_read_unbounded";
|
|
17
17
|
function operandFamily(paths) {
|
|
@@ -22,7 +22,7 @@ function operandFamily(paths) {
|
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
24
|
export function bashReversibilityProbe(allow, boundary) {
|
|
25
|
-
const allowSet = new Set(allow ??
|
|
25
|
+
const allowSet = new Set(allow ?? BASH_CLASSIFY_DEFAULT_ALLOW);
|
|
26
26
|
return (args) => {
|
|
27
27
|
const a = args;
|
|
28
28
|
const command = a?.command;
|
|
@@ -312,6 +312,13 @@ async function runShell(env, cwd, toolName, command, timeoutSec, caps, signal, c
|
|
|
312
312
|
isError: true,
|
|
313
313
|
};
|
|
314
314
|
}
|
|
315
|
+
if (res.error.code === "target_unavailable") {
|
|
316
|
+
return {
|
|
317
|
+
content: `Error (${toolName.slice(0, 64)}): the execution target is unavailable (offline/busy/not bound), so the command did NOT start and no effects landed. Retrying the same command is safe once the target is reachable again. (${res.error.message.slice(0, 512)})`,
|
|
318
|
+
details: { type: "bash", targetUnavailable: true },
|
|
319
|
+
isError: true,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
315
322
|
if (res.error.code === "timeout" || res.error.code === "aborted" || res.error.code === "callback_error") {
|
|
316
323
|
const rawStdout = res.error.partialStdout ?? "";
|
|
317
324
|
const rawStderr = res.error.partialStderr ?? "";
|
|
@@ -4,6 +4,16 @@ import { defineTool, errorResult } from "../../core/tools.js";
|
|
|
4
4
|
import { sha256, resolveKey, violationText, violationDetails, requireRead, checkStale, checkEditMatch, checkNoChange, fileArgPath, resolveQuoteMatch, adaptNewStringQuotes, resolveEscapeMatch, adaptNewStringEscapes, escapeMatchWasAttempted, ESCAPE_MATCH_MISS_NOTE, deletionOldString, countOccurrences, READ_REFUSED_ESCAPE_HINT, } from "./safety.js";
|
|
5
5
|
import { decodeTextBytes, encodeTextForFile, normalizeEditText, normalizeFileText } from "./encoding.js";
|
|
6
6
|
import { MAX_EDIT_BYTES, decodeEditBytes, tooLargeToEditMessage, truncatedUtf16BodyMessage, persistedTextOf, notReadRefusalText, enoentMessage, FILE_STATE_TRAILER, FILE_PATH_PARAMS, ipynbRedirect, countLines, } from "./fs-shared.js";
|
|
7
|
+
async function envFinalWrite(env, key, content, signal, opts) {
|
|
8
|
+
if (env.writeFileGuarded !== undefined) {
|
|
9
|
+
const r = await env.writeFileGuarded(key, content, { canonicalPath: key, ...(opts?.exclusive === true ? { exclusive: true } : {}) }, signal);
|
|
10
|
+
return r.ok ? { ok: true } : r;
|
|
11
|
+
}
|
|
12
|
+
if (opts?.exclusive === true && env.writeFileExclusive !== undefined) {
|
|
13
|
+
return env.writeFileExclusive(key, content, signal);
|
|
14
|
+
}
|
|
15
|
+
return env.writeFile(key, content, signal);
|
|
16
|
+
}
|
|
7
17
|
async function gateToolWrite(hook, tool, path, key, content) {
|
|
8
18
|
if (hook === undefined)
|
|
9
19
|
return undefined;
|
|
@@ -71,13 +81,14 @@ export function createEditFileTool(env, state, rootCanonical, cwdRef, additional
|
|
|
71
81
|
const gated = await gateToolWrite(beforeWrite, "Edit", path, r.key, created);
|
|
72
82
|
if (gated !== undefined)
|
|
73
83
|
return errorResult(gated);
|
|
74
|
-
const write = env.
|
|
75
|
-
? await env.writeFileExclusive(r.key, created, ctx.signal)
|
|
76
|
-
: await env.writeFile(r.key, created, ctx.signal);
|
|
84
|
+
const write = await envFinalWrite(env, r.key, created, ctx.signal, { exclusive: true });
|
|
77
85
|
if (!write.ok) {
|
|
78
86
|
if (write.error.code === "already_exists") {
|
|
79
87
|
return errorResult(`Error (Edit): cannot create "${path}": file already exists (created concurrently since the existence check). Read the file first, then edit it normally (or Write after the Read to overwrite it).`);
|
|
80
88
|
}
|
|
89
|
+
if (write.error.code === "precondition_failed") {
|
|
90
|
+
return errorResult(`Error (Edit): cannot create "${path.slice(0, 512)}": the atomic write refused its precondition (${write.error.message.slice(0, 512)}) — the on-disk state at that path changed between the existence check and the write (something may now exist there, or the path may now resolve somewhere else, e.g. through a symlink). Nothing was written. Read the path to see what is actually there before acting on it.`);
|
|
91
|
+
}
|
|
81
92
|
return errorResult(`Error (Edit): cannot write "${path}": ${write.error.message}`);
|
|
82
93
|
}
|
|
83
94
|
const createdNorm = normalizeFileText(created);
|
|
@@ -121,7 +132,7 @@ export function createEditFileTool(env, state, rootCanonical, cwdRef, additional
|
|
|
121
132
|
if (gated !== undefined)
|
|
122
133
|
return errorResult(gated);
|
|
123
134
|
const encodedOverwrite = encodeTextForFile(newContent, preDec.encoding, preDec.endings);
|
|
124
|
-
const write = await env
|
|
135
|
+
const write = await envFinalWrite(env, r.key, encodedOverwrite, ctx.signal);
|
|
125
136
|
if (!write.ok)
|
|
126
137
|
return errorResult(`Error (Edit): cannot write "${path}": ${write.error.message}`);
|
|
127
138
|
const persistedOverwrite = persistedTextOf(encodedOverwrite);
|
|
@@ -194,7 +205,7 @@ export function createEditFileTool(env, state, rootCanonical, cwdRef, additional
|
|
|
194
205
|
if (gated !== undefined)
|
|
195
206
|
return errorResult(gated);
|
|
196
207
|
const encodedEdit = encodeTextForFile(working, decoded.encoding, decoded.endings);
|
|
197
|
-
const write = await env
|
|
208
|
+
const write = await envFinalWrite(env, r.key, encodedEdit, ctx.signal);
|
|
198
209
|
if (!write.ok)
|
|
199
210
|
return errorResult(`Error (Edit): cannot write "${path}": ${write.error.message}`);
|
|
200
211
|
const persistedEdit = persistedTextOf(encodedEdit);
|
|
@@ -265,7 +276,7 @@ export function createWriteFileTool(env, state, rootCanonical, cwdRef, additiona
|
|
|
265
276
|
if (gated !== undefined)
|
|
266
277
|
return errorResult(gated);
|
|
267
278
|
const encodedWrite = encodeTextForFile(content, decodedPrev.encoding, "preserve");
|
|
268
|
-
const write = await env
|
|
279
|
+
const write = await envFinalWrite(env, r.key, encodedWrite, ctx.signal);
|
|
269
280
|
if (!write.ok)
|
|
270
281
|
return errorResult(`Error (Write): cannot write "${path}": ${write.error.message}`);
|
|
271
282
|
const persistedWrite = persistedTextOf(encodedWrite);
|
|
@@ -278,7 +289,7 @@ export function createWriteFileTool(env, state, rootCanonical, cwdRef, additiona
|
|
|
278
289
|
const gatedCreate = await gateToolWrite(beforeWrite, "Write", path, r.key, content);
|
|
279
290
|
if (gatedCreate !== undefined)
|
|
280
291
|
return errorResult(gatedCreate);
|
|
281
|
-
const write = await env
|
|
292
|
+
const write = await envFinalWrite(env, r.key, content, ctx.signal);
|
|
282
293
|
if (!write.ok)
|
|
283
294
|
return errorResult(`Error (Write): cannot write "${path}": ${write.error.message}`);
|
|
284
295
|
const totalLines = countLines(content);
|
|
@@ -409,7 +420,7 @@ export function createNotebookEditTool(env, state, rootCanonical, cwdRef, additi
|
|
|
409
420
|
const gated = await gateToolWrite(beforeWrite, "NotebookEdit", notebook_path, r.key, updated);
|
|
410
421
|
if (gated !== undefined)
|
|
411
422
|
return errorResult(gated);
|
|
412
|
-
const w = await env
|
|
423
|
+
const w = await envFinalWrite(env, r.key, encodeTextForFile(updated, decodedNb.encoding, decodedNb.endings), ctx.signal);
|
|
413
424
|
if (!w.ok)
|
|
414
425
|
return errorResult(`Error (NotebookEdit): cannot write "${notebook_path}": ${w.error.message}`);
|
|
415
426
|
state.set(r.key, { hash: sha256(updated), totalLines: countLines(updated), truncated: false, lastReadAt: Date.now() });
|
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
|
+
export { BASH_CLASSIFY_DEFAULT_ALLOW } from "./bash-readonly-classifier.js";
|
|
14
15
|
import { type ReadDenyEntry } from "./read-deny.js";
|
|
15
16
|
export * from "./read-face.js";
|
|
16
17
|
import { type CwdRef, type ReadImageDownsamplerOption } from "./fs-shared.js";
|
package/dist/tools/fs/index.js
CHANGED
|
@@ -12,6 +12,7 @@ export * from "./bash-readonly-classifier.js";
|
|
|
12
12
|
export * from "./fs-bash.js";
|
|
13
13
|
export * from "./read-deny.js";
|
|
14
14
|
import { BASH_READONLY_DEFAULT_ALLOW, } from "./bash-readonly-classifier.js";
|
|
15
|
+
export { BASH_CLASSIFY_DEFAULT_ALLOW } from "./bash-readonly-classifier.js";
|
|
15
16
|
import { compileReadDeny } from "./read-deny.js";
|
|
16
17
|
import { deploymentReadFaceClampNotice, resolveReadFace } from "./read-face.js";
|
|
17
18
|
export * from "./read-face.js";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_comment": "design/87 L3 — frozen public export surface of src/index.ts (name -> kind). DO NOT edit by hand to silence a red test. A removed/changed entry = a SemVer-BREAKING change; bump MAJOR and update this fixture in the SAME commit (design/87 §4.2 / §5.2). Regenerate via REGEN in test/export-surface.test.ts.",
|
|
3
3
|
"_tierComment": "#435 v1 — machine-readable layering of the public surface: stable = demonstrated by README.md / src/examples; internal = an `Internal`-marked name or a runner/engine deep-subtree declaration (the model seam src/engine/llm is excluded — it is the BYOM contract, not an engine internal); advanced = a supported export the front door does not walk you through. THIS IS AN INITIAL HEURISTIC, derived mechanically and expected to be refined ticket by ticket: no human reviewed these 1700+ entries one by one, and nothing here claims otherwise. Known bias: a short or English-word export name (ok, err, Result, Usage) can match ordinary prose in README.md and land `stable` on a coincidence. Every export MUST carry a tier — a new export with no row fails the gate in export-surface.test.ts.",
|
|
4
|
-
"count":
|
|
4
|
+
"count": 1766,
|
|
5
5
|
"exports": {
|
|
6
6
|
"A2ATaskState": "type",
|
|
7
7
|
"A2ATaskStateReversal": "type",
|
|
@@ -637,6 +637,7 @@
|
|
|
637
637
|
"OrgRuleStatePersistence": "interface",
|
|
638
638
|
"OriginClearanceEvent": "interface",
|
|
639
639
|
"OriginClearanceRow": "interface",
|
|
640
|
+
"OriginClearanceShadow": "interface",
|
|
640
641
|
"OrphanToolCall": "interface",
|
|
641
642
|
"OutputChunk": "type",
|
|
642
643
|
"OwnOrgAdmissionVerdict": "interface",
|
|
@@ -1191,11 +1192,13 @@
|
|
|
1191
1192
|
"WorktreeSessionRef": "interface",
|
|
1192
1193
|
"WorktreeToolsOptions": "interface",
|
|
1193
1194
|
"WritablePermissionRuleStore": "interface",
|
|
1195
|
+
"WriteExpectation": "interface",
|
|
1194
1196
|
"WriteProtectedEntry": "type",
|
|
1195
1197
|
"WriteProtectedHit": "interface",
|
|
1196
1198
|
"WriteProtectedKind": "type",
|
|
1197
1199
|
"WriteProtectedRow": "interface",
|
|
1198
1200
|
"WriteProtectionMatcher": "interface",
|
|
1201
|
+
"WriteReceipt": "interface",
|
|
1199
1202
|
"ackAdoptionConfig": "function",
|
|
1200
1203
|
"acquireCcLock": "function",
|
|
1201
1204
|
"addDotsOf": "function",
|
|
@@ -2402,6 +2405,7 @@
|
|
|
2402
2405
|
"OrgRuleStatePersistence": "stable",
|
|
2403
2406
|
"OriginClearanceEvent": "advanced",
|
|
2404
2407
|
"OriginClearanceRow": "advanced",
|
|
2408
|
+
"OriginClearanceShadow": "advanced",
|
|
2405
2409
|
"OrphanToolCall": "advanced",
|
|
2406
2410
|
"OutputChunk": "advanced",
|
|
2407
2411
|
"OwnOrgAdmissionVerdict": "advanced",
|
|
@@ -2956,11 +2960,13 @@
|
|
|
2956
2960
|
"WorktreeSessionRef": "advanced",
|
|
2957
2961
|
"WorktreeToolsOptions": "advanced",
|
|
2958
2962
|
"WritablePermissionRuleStore": "advanced",
|
|
2963
|
+
"WriteExpectation": "internal",
|
|
2959
2964
|
"WriteProtectedEntry": "advanced",
|
|
2960
2965
|
"WriteProtectedHit": "advanced",
|
|
2961
2966
|
"WriteProtectedKind": "advanced",
|
|
2962
2967
|
"WriteProtectedRow": "advanced",
|
|
2963
2968
|
"WriteProtectionMatcher": "advanced",
|
|
2969
|
+
"WriteReceipt": "internal",
|
|
2964
2970
|
"ackAdoptionConfig": "advanced",
|
|
2965
2971
|
"acquireCcLock": "advanced",
|
|
2966
2972
|
"addDotsOf": "advanced",
|