@sema-agent/core 7.22.0 → 7.23.1
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 +210 -1
- package/dist/core/auto-compaction.d.ts +2 -2
- package/dist/core/checkpoint-store.d.ts +1 -5
- package/dist/core/context-edit.js +1 -1
- package/dist/core/engine-notice.d.ts +13 -0
- package/dist/core/gate-bounded-await.d.ts +14 -4
- package/dist/core/gate-bounded-await.js +1 -1
- package/dist/core/gate-crash-trace.d.ts +7 -0
- package/dist/core/gate-crash-trace.js +3 -0
- package/dist/core/gate-fold.js +5 -4
- package/dist/core/gate-lanes.js +2 -1
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +2 -0
- package/dist/core/hooks.js +2 -1
- package/dist/core/park-selfcheck.js +1 -1
- package/dist/core/path-identity.d.ts +11 -4
- package/dist/core/path-identity.js +5 -2
- package/dist/core/read-root-candidate.js +11 -1
- package/dist/core/runner/assemble-result.d.ts +2 -7
- package/dist/core/runner/assemble-result.js +25 -8
- package/dist/core/runner/contracts.d.ts +15 -15
- package/dist/core/runner/gate-exit.d.ts +0 -5
- package/dist/core/runner/gate-exit.js +0 -3
- package/dist/core/runner/inherited-ask-grants.d.ts +2 -7
- package/dist/core/runner/inherited-ask-grants.js +1 -0
- package/dist/core/runner/prepare-ask-lane.js +1 -0
- package/dist/core/runner/prepare-context-lane.d.ts +43 -1
- package/dist/core/runner/prepare-context-lane.js +25 -4
- package/dist/core/runner/prepare-task.js +2 -2
- package/dist/core/runner/prepare-turn-wiring.js +2 -2
- package/dist/core/runner/run-harness-handlers.js +4 -2
- package/dist/core/runner/run-recovery-lanes.js +6 -3
- package/dist/core/runner/run-telemetry-and-budget-seats.js +11 -1
- package/dist/core/runner/write-protection-station.d.ts +7 -25
- package/dist/core/runner/write-protection-station.js +18 -0
- package/dist/core/task-registry.d.ts +0 -6
- package/dist/core/task-result.d.ts +28 -10
- package/dist/core/types.d.ts +1 -1
- package/dist/core/write-protect.d.ts +64 -12
- package/dist/core/write-protect.js +87 -34
- package/dist/engine/execution-env/node-execution-env.d.ts +19 -2
- package/dist/engine/execution-env/node-execution-env.js +5 -1
- package/dist/index.d.ts +1 -1
- package/dist/tools/fs/bash-lexer.d.ts +12 -5
- package/dist/tools/fs/bash-lexer.js +5 -2
- package/dist/tools/fs/bash-program-position.d.ts +18 -0
- package/dist/tools/fs/bash-program-position.js +13 -1
- package/dist/tools/fs/bash-readonly-classifier.d.ts +10 -3
- package/dist/tools/fs/bash-readonly-classifier.js +70 -41
- package/dist/tools/fs/fs-bash.d.ts +22 -0
- package/dist/tools/fs/fs-bash.js +19 -12
- package/dist/tools/fs/fs-read.js +2 -1
- package/dist/tools/fs/fs-shared.d.ts +33 -7
- package/dist/tools/fs/fs-shared.js +12 -1
- package/dist/tools/fs/safety.d.ts +37 -0
- package/dist/tools/fs/safety.js +31 -3
- package/package.json +2 -2
- package/test/export-surface.snapshot.json +3 -1
- package/wire-changes/core@7.23.0.json +316 -0
- package/wire-changes/core@7.23.1.json +205 -0
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { hasWin32AliasedComponent, isAbsoluteForFamily, isAbsolutePathForm, isBlockedDevicePath, isShellRootedSpellingUnmapped, joinForFamily, nativeUncSpellingOf, normalizeAbsPathLexically, win32NamespaceScreen, pathFamilyOf, withinAnyRoot } from "./safety.js";
|
|
2
2
|
import { lexShellText, literalTildeSpelling, quoteMask, substitutionsInHeredocBody, SUBSTITUTION_PLACEHOLDER, wordsOf, } from "./bash-lexer.js";
|
|
3
|
-
import { ASSIGNMENT_WORD, COMMAND_LAUNCHERS, NOT_AUTO_ALLOWED, SHELL_BLOCK_WORDS, argvReentryUnjudged, isBareControlWord, programBasename, programPositionRefusalSentence, readProgramPosition, } from "./bash-program-position.js";
|
|
4
|
-
export { NOT_AUTO_ALLOWED } from "./bash-program-position.js";
|
|
3
|
+
import { ASSIGNMENT_WORD, COMMAND_LAUNCHERS, NOT_AUTO_ALLOWED, SHELL_BLOCK_WORDS, argvReentryUnjudged, isBareControlWord, programPositionRunsShell, shellScanDeclineOf, programBasename, programPositionRefusalSentence, readProgramPosition, } from "./bash-program-position.js";
|
|
5
4
|
export const BASH_READONLY_DEFAULT_ALLOW = [
|
|
6
5
|
"ls", "cat", "head", "tail", "wc", "pwd", "echo", "whoami", "uname",
|
|
7
6
|
"grep", "cut", "tr", "basename", "dirname", "stat", "du", "df", "which",
|
|
@@ -52,7 +51,8 @@ export function coarseReadonlyCheck(command, allow, options) {
|
|
|
52
51
|
return `command "${parsed.name}" is not in the read-only allowlist`;
|
|
53
52
|
return undefined;
|
|
54
53
|
}
|
|
55
|
-
const
|
|
54
|
+
const SHELL_SEGMENT_QUOTE_BLIND_REJECT = /[<>()`\n\r\\]/;
|
|
55
|
+
const SHELL_EXPANSION_CHARS = /[$]/;
|
|
56
56
|
export function carriesShellRedirection(text) {
|
|
57
57
|
return SHELL_REDIRECTION_OPERATORS.test(text);
|
|
58
58
|
}
|
|
@@ -78,10 +78,25 @@ function rejectedSansRedirection(source) {
|
|
|
78
78
|
if (redirectionPrecedesConnector(source)) {
|
|
79
79
|
return "a redirection followed by `|`, `&` or `;` is not allowed — the operator's own characters cannot be told apart from a command boundary";
|
|
80
80
|
}
|
|
81
|
+
return hardRejectReason(source, true);
|
|
82
|
+
}
|
|
83
|
+
function hardRejectReason(source, tolerateRedirection) {
|
|
84
|
+
const reason = tolerateRedirection
|
|
85
|
+
? "command/variable substitution, subshells, escapes, and line breaks are not allowed"
|
|
86
|
+
: "redirection, command/variable substitution, subshells, escapes, and line breaks are not allowed";
|
|
81
87
|
for (const ch of source) {
|
|
82
|
-
if (
|
|
83
|
-
return
|
|
84
|
-
|
|
88
|
+
if (SHELL_SEGMENT_QUOTE_BLIND_REJECT.test(ch) && !(tolerateRedirection && SHELL_REDIRECTION_OPERATORS.test(ch)))
|
|
89
|
+
return reason;
|
|
90
|
+
}
|
|
91
|
+
if (!SHELL_EXPANSION_CHARS.test(source))
|
|
92
|
+
return undefined;
|
|
93
|
+
const mask = quoteMask(source);
|
|
94
|
+
for (let i = 0; i < source.length; i++) {
|
|
95
|
+
if (!SHELL_EXPANSION_CHARS.test(source[i]))
|
|
96
|
+
continue;
|
|
97
|
+
if (mask.balanced && mask.openedBy[i] === "'")
|
|
98
|
+
continue;
|
|
99
|
+
return reason;
|
|
85
100
|
}
|
|
86
101
|
return undefined;
|
|
87
102
|
}
|
|
@@ -91,8 +106,10 @@ export function splitShellCompoundSegments(source, options) {
|
|
|
91
106
|
if (refused !== undefined)
|
|
92
107
|
return { reject: refused };
|
|
93
108
|
}
|
|
94
|
-
else
|
|
95
|
-
|
|
109
|
+
else {
|
|
110
|
+
const refused = hardRejectReason(source, false);
|
|
111
|
+
if (refused !== undefined)
|
|
112
|
+
return { reject: refused };
|
|
96
113
|
}
|
|
97
114
|
const scanned = options?.trailingTerminator !== "keep" ? source.replace(/;\s*$/, "") : source;
|
|
98
115
|
const lexed = lexShellText(scanned, { mode: "strict" });
|
|
@@ -239,8 +256,8 @@ function tokensMoveBase(toks) {
|
|
|
239
256
|
return pos.refusal !== undefined || pos.program?.basename === "cd";
|
|
240
257
|
}
|
|
241
258
|
const SHELL_REENTRY_PROGRAM_RE = /^(?:(?:ba|z|da|k|fi|a|mk|ya|pdk|c|tc)?sh|eval|source|\.|parallel)$/;
|
|
242
|
-
export function refusedCommandMayRead(command, allow, boundary) {
|
|
243
|
-
return !refusedCommandExempt(command, allow, boundary, 0);
|
|
259
|
+
export function refusedCommandMayRead(command, allow, boundary, declinesAreEvidence) {
|
|
260
|
+
return !refusedCommandExempt(command, allow, boundary, declinesAreEvidence, 0);
|
|
244
261
|
}
|
|
245
262
|
const STDIN_ONLY_READER_FLAGS = new Map([
|
|
246
263
|
["cat", /^-[AbeEnstTuv]+$/],
|
|
@@ -262,7 +279,7 @@ function whitelistSegment(segment) {
|
|
|
262
279
|
redirects: tokens.filter((t) => t.kind === "redirect"),
|
|
263
280
|
};
|
|
264
281
|
}
|
|
265
|
-
function refusedCommandExempt(command, allow, boundary, depth, baseMovedOutside = false) {
|
|
282
|
+
function refusedCommandExempt(command, allow, boundary, declinesAreEvidence, depth, baseMovedOutside = false) {
|
|
266
283
|
if (depth > 4)
|
|
267
284
|
return false;
|
|
268
285
|
const lexed = lexShellText(command, { mode: "lenient" });
|
|
@@ -282,12 +299,21 @@ function refusedCommandExempt(command, allow, boundary, depth, baseMovedOutside
|
|
|
282
299
|
}
|
|
283
300
|
segments.push(whitelistSegment(segment));
|
|
284
301
|
}
|
|
302
|
+
let declined;
|
|
303
|
+
for (const { words } of segments) {
|
|
304
|
+
const pos = readProgramPosition(words);
|
|
305
|
+
if (programPositionRunsShell(pos, SHELL_REENTRY_PROGRAM_RE))
|
|
306
|
+
return false;
|
|
307
|
+
const decline = shellScanDeclineOf(pos, XARGS_TARGETS_ACCEPTED_UNJUDGED);
|
|
308
|
+
if (decline !== undefined && declinesAreEvidence.has(decline))
|
|
309
|
+
return false;
|
|
310
|
+
declined ??= decline;
|
|
311
|
+
}
|
|
312
|
+
if (declined !== undefined)
|
|
313
|
+
return true;
|
|
285
314
|
const baseMoved = baseMovedOutside ||
|
|
286
315
|
segments.some(({ words }) => {
|
|
287
|
-
const
|
|
288
|
-
if (pos.refusal !== undefined)
|
|
289
|
-
return true;
|
|
290
|
-
const runs = pos.program?.basename;
|
|
316
|
+
const runs = readProgramPosition(words).program?.basename;
|
|
291
317
|
return runs === "cd" || (runs !== undefined && UNMODELLED_BASE_MOVERS.has(runs));
|
|
292
318
|
});
|
|
293
319
|
for (const body of bodies) {
|
|
@@ -296,7 +322,7 @@ function refusedCommandExempt(command, allow, boundary, depth, baseMovedOutside
|
|
|
296
322
|
(walked.undecidedPaths?.length ?? 0) === 0 && (walked.unresolvedOperands?.length ?? 0) === 0 && (walked.recursiveReadPaths?.length ?? 0) === 0;
|
|
297
323
|
if (fullyGreen)
|
|
298
324
|
continue;
|
|
299
|
-
if (!refusedCommandExempt(body, allow, boundary, depth + 1, baseMoved))
|
|
325
|
+
if (!refusedCommandExempt(body, allow, boundary, declinesAreEvidence, depth + 1, baseMoved))
|
|
300
326
|
return false;
|
|
301
327
|
}
|
|
302
328
|
const stdinFileJudgedInside = (word) => {
|
|
@@ -353,8 +379,6 @@ function refusedCommandExempt(command, allow, boundary, depth, baseMovedOutside
|
|
|
353
379
|
if (r.op === "<" && !stdinFileJudgedInside(r.target?.folded))
|
|
354
380
|
return false;
|
|
355
381
|
const pos = readProgramPosition(words);
|
|
356
|
-
if (pos.refusal !== undefined)
|
|
357
|
-
return false;
|
|
358
382
|
for (const k of words.folded.keys()) {
|
|
359
383
|
const word = words.folded[k];
|
|
360
384
|
if (ASSIGNMENT_WORD.test(word) && /\$\(|`/.test(word) && (pos.assignments.includes(k) || pos.launchers.some((l) => l.index < k)))
|
|
@@ -365,24 +389,16 @@ function refusedCommandExempt(command, allow, boundary, depth, baseMovedOutside
|
|
|
365
389
|
if (program === undefined) {
|
|
366
390
|
if (pos.launchers.length === 0)
|
|
367
391
|
continue;
|
|
368
|
-
if (argvReentryUnjudged(pos, XARGS_TARGETS_ACCEPTED_UNJUDGED))
|
|
369
|
-
return false;
|
|
370
392
|
if (!unlistedSegmentJudgedInside(words, pos.launchers[0].index, pos.assignments))
|
|
371
393
|
return false;
|
|
372
394
|
continue;
|
|
373
395
|
}
|
|
374
396
|
if (program.name.includes(SUBSTITUTION_PLACEHOLDER) || /[$`]/.test(program.name))
|
|
375
397
|
return false;
|
|
376
|
-
if (
|
|
377
|
-
|
|
378
|
-
if (pos.launchers.length > 0) {
|
|
379
|
-
if (argvReentryUnjudged(pos, XARGS_TARGETS_ACCEPTED_UNJUDGED))
|
|
398
|
+
if (pos.launchers.length > 0 && !listed(program.basename)) {
|
|
399
|
+
if (!unlistedSegmentJudgedInside(words, pos.launchers[0].index, pos.assignments))
|
|
380
400
|
return false;
|
|
381
|
-
|
|
382
|
-
if (!unlistedSegmentJudgedInside(words, pos.launchers[0].index, pos.assignments))
|
|
383
|
-
return false;
|
|
384
|
-
continue;
|
|
385
|
-
}
|
|
401
|
+
continue;
|
|
386
402
|
}
|
|
387
403
|
const head = program.basename;
|
|
388
404
|
if (head === "cd" || UNMODELLED_BASE_MOVERS.has(head))
|
|
@@ -455,12 +471,12 @@ function longOptionNameOf(tok) {
|
|
|
455
471
|
function isLongOptionAbbrevOf(name, full) {
|
|
456
472
|
return full.startsWith(name);
|
|
457
473
|
}
|
|
458
|
-
function
|
|
474
|
+
function suppliesFirstOperandSlot(slot, tok) {
|
|
459
475
|
if (tok.startsWith("--")) {
|
|
460
476
|
const long = longOptionNameOf(tok);
|
|
461
|
-
return long !== undefined && (
|
|
477
|
+
return long !== undefined && slot.suppliedByLongNames.some((full) => isLongOptionAbbrevOf(long, full));
|
|
462
478
|
}
|
|
463
|
-
return
|
|
479
|
+
return new RegExp(`^-[A-Za-z0-9]*[${slot.suppliedByLetters}]`).test(tok);
|
|
464
480
|
}
|
|
465
481
|
function isGrepClusterFileStdin(tok) {
|
|
466
482
|
if (tok.startsWith("--") || !/^-[A-Za-z0-9]/.test(tok))
|
|
@@ -534,6 +550,7 @@ const VERB_OPTION_SLOTS = {
|
|
|
534
550
|
regexp: "pattern",
|
|
535
551
|
},
|
|
536
552
|
longOptionalValue: { color: "label", colour: "label", context: "count" },
|
|
553
|
+
firstOperand: { kind: "pattern", suppliedByLetters: "ef", suppliedByLongNames: ["regexp", "file"] },
|
|
537
554
|
longPlain: [
|
|
538
555
|
"basic-regexp", "byte-offset", "count", "dereference-recursive", "extended-regexp",
|
|
539
556
|
"files-with-matches", "files-without-match", "fixed-strings", "help", "ignore-case", "initial-tab",
|
|
@@ -579,6 +596,7 @@ const VERB_OPTION_SLOTS = {
|
|
|
579
596
|
shortPlain: "nrEszu",
|
|
580
597
|
longValue: { expression: "script", file: "path", "line-length": "count" },
|
|
581
598
|
longOptionalValue: { "in-place": "label" },
|
|
599
|
+
firstOperand: { kind: "script", suppliedByLetters: "ef", suppliedByLongNames: ["expression", "file"] },
|
|
582
600
|
longPlain: [
|
|
583
601
|
"debug", "follow-symlinks", "help", "null-data", "posix", "quiet", "regexp-extended", "sandbox",
|
|
584
602
|
"separate", "silent", "unbuffered", "version", "zero-terminated",
|
|
@@ -1670,7 +1688,8 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
|
|
|
1670
1688
|
}
|
|
1671
1689
|
else {
|
|
1672
1690
|
const eoo = args.indexOf("--");
|
|
1673
|
-
const
|
|
1691
|
+
const firstOperandSlot = VERB_OPTION_SLOTS[name]?.firstOperand;
|
|
1692
|
+
const firstOperandSuppliedByFlag = firstOperandSlot !== undefined && (eoo === -1 ? args : args.slice(0, eoo)).some((t) => suppliesFirstOperandSlot(firstOperandSlot, t));
|
|
1674
1693
|
let sawOperand = false;
|
|
1675
1694
|
let sawArgvOperand = false;
|
|
1676
1695
|
let endOfOptions = false;
|
|
@@ -1718,12 +1737,12 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
|
|
|
1718
1737
|
continue;
|
|
1719
1738
|
}
|
|
1720
1739
|
}
|
|
1721
|
-
const
|
|
1740
|
+
const inFirstOperandSlot = firstOperandSlot !== undefined && !firstOperandSuppliedByFlag && !sawOperand;
|
|
1722
1741
|
sawOperand = true;
|
|
1723
1742
|
if (k >= argvOperandsStartAt)
|
|
1724
1743
|
sawArgvOperand = true;
|
|
1725
1744
|
if (t === "-") {
|
|
1726
|
-
if (
|
|
1745
|
+
if (inFirstOperandSlot)
|
|
1727
1746
|
continue;
|
|
1728
1747
|
const m = RECURSIVE_READ_FORMS[slotVerb];
|
|
1729
1748
|
if (m === undefined || m.dashIsStdin === true) {
|
|
@@ -1734,8 +1753,14 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
|
|
|
1734
1753
|
positionalOperands.push(t);
|
|
1735
1754
|
continue;
|
|
1736
1755
|
}
|
|
1737
|
-
if (
|
|
1756
|
+
if (inFirstOperandSlot) {
|
|
1757
|
+
const named = slotValueCandidates(t, firstOperandSlot.kind);
|
|
1758
|
+
for (const c of named)
|
|
1759
|
+
judgeOperand(c, argGlobs(k));
|
|
1760
|
+
if (named.length > 0 && k >= argvOperandsStartAt)
|
|
1761
|
+
positionalOperands.push(t);
|
|
1738
1762
|
continue;
|
|
1763
|
+
}
|
|
1739
1764
|
if (k >= argvOperandsStartAt)
|
|
1740
1765
|
positionalOperands.push(t);
|
|
1741
1766
|
judgeOperand(t, argGlobs(k));
|
|
@@ -1987,7 +2012,8 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
1987
2012
|
if (!(pipeFed[si] ?? false)) {
|
|
1988
2013
|
const floor = STDIN_FILE_FLOOR[name];
|
|
1989
2014
|
const restArgs = toks.slice(1);
|
|
1990
|
-
const
|
|
2015
|
+
const floorFirstOperandSlot = VERB_OPTION_SLOTS[name]?.firstOperand;
|
|
2016
|
+
const floorSlotSuppliedByFlag = floorFirstOperandSlot !== undefined && restArgs.some((t) => suppliesFirstOperandSlot(floorFirstOperandSlot, t));
|
|
1991
2017
|
let sawNonFlagOperand = false;
|
|
1992
2018
|
let hasStdinDash = false;
|
|
1993
2019
|
let operandCount = 0;
|
|
@@ -2013,10 +2039,10 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
2013
2039
|
if (t.startsWith("-") && t !== "-")
|
|
2014
2040
|
continue;
|
|
2015
2041
|
}
|
|
2016
|
-
const
|
|
2042
|
+
const inFirstOperandSlot = floorFirstOperandSlot !== undefined && !floorSlotSuppliedByFlag && !sawNonFlagOperand;
|
|
2017
2043
|
sawNonFlagOperand = true;
|
|
2018
2044
|
operandCount++;
|
|
2019
|
-
if (
|
|
2045
|
+
if (inFirstOperandSlot)
|
|
2020
2046
|
continue;
|
|
2021
2047
|
if (t === "-") {
|
|
2022
2048
|
hasStdinDash = true;
|
|
@@ -2329,10 +2355,13 @@ export function classifyOutOfRootReadGate(command, allow, boundary) {
|
|
|
2329
2355
|
if (baseMoved)
|
|
2330
2356
|
return { gated: true, paths: [] };
|
|
2331
2357
|
const verdict = evaluateReadBoundary(readers, containmentOnly, false, readerReach);
|
|
2332
|
-
|
|
2358
|
+
const denied = boundary.denyMatch === undefined ? false : evaluateReadBoundary(readers, boundary, false, readerReach).readDenied === true;
|
|
2359
|
+
if (verdict.outOfRootRead === true)
|
|
2360
|
+
return { gated: true, paths: verdict.outOfRootPaths ?? [], ...(denied ? { denied: true } : {}) };
|
|
2361
|
+
return denied ? { gated: false, paths: [], denied: true } : NOT_GATED;
|
|
2333
2362
|
};
|
|
2334
2363
|
const direct = judge(command);
|
|
2335
|
-
if (direct.gated)
|
|
2364
|
+
if (direct.gated || direct.denied === true)
|
|
2336
2365
|
return direct;
|
|
2337
2366
|
const shape = POLL_LOOP_SHAPE.exec(command.trim());
|
|
2338
2367
|
return shape !== null ? judge(shape[3]) : NOT_GATED;
|
|
@@ -3,6 +3,7 @@ import { type TaskRegistry } from "../../core/task-registry.js";
|
|
|
3
3
|
import { type CwdRef } from "./fs-shared.js";
|
|
4
4
|
import type { ReversibilityVerdict } from "../../core/types.js";
|
|
5
5
|
import { type BashReadonlyRootBoundary } from "./bash-readonly-classifier.js";
|
|
6
|
+
import { type ShellScanDecline } from "./bash-program-position.js";
|
|
6
7
|
/**
|
|
7
8
|
* A parsed-command classifier for the `bash` tool, exposed as a
|
|
8
9
|
* `ToolSpec.reversibilityProbe`. A full shell is treated as egress+irreversible by DEFAULT; this probe is the
|
|
@@ -60,6 +61,27 @@ import { type BashReadonlyRootBoundary } from "./bash-readonly-classifier.js";
|
|
|
60
61
|
* shape classifier here changes what happens to the residual shell risk, never that reading.
|
|
61
62
|
*/
|
|
62
63
|
export declare function bashReversibilityProbe(allow?: readonly string[], boundary?: BashReadonlyRootBoundary | (() => BashReadonlyRootBoundary | undefined)): (args: unknown) => ReversibilityVerdict;
|
|
64
|
+
/**
|
|
65
|
+
* ONE rule, TWO TABLES: which {@link ShellScanDecline} a SEAT reads as read evidence. A decline names no word,
|
|
66
|
+
* so it is never the boundary DECLARING anything about a path.
|
|
67
|
+
*
|
|
68
|
+
* The READ-BOUNDARY seat is what the default/off doctrine and a bypass mode mount ALONE: there "not mandated"
|
|
69
|
+
* is VOUCHED, so a decline must count as evidence or a read nobody judged runs silently (`<out-of-root> |
|
|
70
|
+
* xargs cat` at zero asks — a closed P0). It keeps both. The CLASSIFY seat composes the same boundary with the
|
|
71
|
+
* SHAPE classifier, which refuses all these shapes anyway: the only question left is whether a stored answer
|
|
72
|
+
* may retire the ask. It keeps NEITHER — empty by the argument, not by omission.
|
|
73
|
+
* THE COST, stated rather than mitigated: once that ask is retired, an out-of-root or deny-listed read HIDDEN
|
|
74
|
+
* inside such a command is not asked again. A command's words are not the argv its programs receive (a stream
|
|
75
|
+
* is re-split by its consumer, an unread option may move the working directory first, stdin text is a command
|
|
76
|
+
* line nobody parsed), so a scan of them cannot be the guarantee it was claimed to be — three independently
|
|
77
|
+
* exploitable shapes measured that. "A read the boundary objects to is never retired by a stored rule" does
|
|
78
|
+
* NOT hold on the classify seat for this class, and holds unchanged elsewhere; below it is the execution
|
|
79
|
+
* environment's sandbox. One word-level check survives on both seats and is not part of the trade: a token the
|
|
80
|
+
* scan DID read naming a path outside the roots still gates the call ({@link classifyOutOfRootReadGate}).
|
|
81
|
+
*/
|
|
82
|
+
export declare const BOUNDARY_SEAT_DECLINE_EVIDENCE: ReadonlySet<ShellScanDecline>;
|
|
83
|
+
/** The classify doctrine's table — see above; empty by the argument. */
|
|
84
|
+
export declare const CLASSIFY_SEAT_DECLINE_EVIDENCE: ReadonlySet<ShellScanDecline>;
|
|
63
85
|
/**
|
|
64
86
|
* The READ-BOUNDARY seat of the full shell: a `ToolSpec.reversibilityProbe` that answers exactly ONE
|
|
65
87
|
* question — does the deployment's read boundary object to where this command reads? — and nothing about
|
package/dist/tools/fs/fs-bash.js
CHANGED
|
@@ -13,7 +13,8 @@ import { boundaryIsTheSandbox, canonicalPathIsAuthoritative, hasDestroy, isIsola
|
|
|
13
13
|
import { ghRateLimitHint } from "./gh-rate-limit.js";
|
|
14
14
|
import { resolveBashTimeoutCaps, bashTimeoutCapsSec, bashTimeoutArgRefusal, bashTimeoutParamDescription, envErrorDetail, bashMaxOutputChars, clipShellOutput, writeShellOverflowFile, createShellOverflowSpoolFence, shellRecoveryHint, CWD_SENTINEL, stripCwdSentinel, BASH_READONLY_CONFINEMENT_NOTE, } from "./fs-shared.js";
|
|
15
15
|
import { PROBE_CAUSE_PATH_MAX, inlineUntrusted } from "../../core/untrusted-text.js";
|
|
16
|
-
import { bashClassifyDefaultAllow, BASH_READONLY_DEFAULT_ALLOW, coarseReadonlyCheck, classifyBoundedReadonlyPollLoopDetailed, classifyCompoundReadonlyDetailed, classifyOutOfRootReadGate, refusedCommandMayRead, classifySimpleCommandReadBoundary,
|
|
16
|
+
import { bashClassifyDefaultAllow, BASH_READONLY_DEFAULT_ALLOW, coarseReadonlyCheck, classifyBoundedReadonlyPollLoopDetailed, classifyCompoundReadonlyDetailed, classifyOutOfRootReadGate, refusedCommandMayRead, classifySimpleCommandReadBoundary, } from "./bash-readonly-classifier.js";
|
|
17
|
+
import { NOT_AUTO_ALLOWED, SHELL_SCAN_DECLINES } from "./bash-program-position.js";
|
|
17
18
|
import { toolFace } from "../../core/tool-catalog-entries.js";
|
|
18
19
|
const RECURSIVE_CAUSE_MAX_PATHS = 3;
|
|
19
20
|
const RECURSIVE_READ_CAUSE_CODE = "shell.recursive_read_unbounded";
|
|
@@ -27,8 +28,8 @@ function operandFamily(paths) {
|
|
|
27
28
|
export function bashReversibilityProbe(allow, boundary) {
|
|
28
29
|
const allowSet = new Set(allow ?? bashClassifyDefaultAllow());
|
|
29
30
|
const judge = (args, command, resolved, offers) => {
|
|
30
|
-
const boundaryGate = (verdict) => readBoundaryMandate(command, allowSet, resolved, verdict);
|
|
31
|
-
const outOfRootGate = () => readBoundaryMandate(command, allowSet, resolved, {});
|
|
31
|
+
const boundaryGate = (verdict) => readBoundaryMandate(command, allowSet, resolved, verdict, CLASSIFY_SEAT_DECLINE_EVIDENCE);
|
|
32
|
+
const outOfRootGate = () => readBoundaryMandate(command, allowSet, resolved, {}, CLASSIFY_SEAT_DECLINE_EVIDENCE);
|
|
32
33
|
const backgrounded = isBackgroundShellCall(args);
|
|
33
34
|
const detailed = classifyCompoundReadonlyDetailed(command, allowSet, resolved);
|
|
34
35
|
if (backgrounded)
|
|
@@ -46,7 +47,7 @@ export function bashReversibilityProbe(allow, boundary) {
|
|
|
46
47
|
if (loop.reason === undefined)
|
|
47
48
|
return { reversible: true };
|
|
48
49
|
const seatOffer = (verdict) => (offers ? readRootCandidateOf(verdict) : {});
|
|
49
|
-
if (boundaryDeclared(loop) || unjudgedRead(command, allowSet, resolved, loop))
|
|
50
|
+
if (boundaryDeclared(loop) || unjudgedRead(command, allowSet, resolved, loop, CLASSIFY_SEAT_DECLINE_EVIDENCE))
|
|
50
51
|
return { reversible: false, mandated: true, ...seatOffer(loop) };
|
|
51
52
|
const refusedCause = recursiveReadCause(detailed);
|
|
52
53
|
return { reversible: false, ...boundaryGate(detailed), ...seatOffer(detailed), ...(refusedCause !== undefined ? { cause: refusedCause } : {}) };
|
|
@@ -86,24 +87,30 @@ function shellCommandOf(args) {
|
|
|
86
87
|
function isBackgroundShellCall(args) {
|
|
87
88
|
return args?.run_in_background === true;
|
|
88
89
|
}
|
|
89
|
-
|
|
90
|
+
export const BOUNDARY_SEAT_DECLINE_EVIDENCE = new Set(SHELL_SCAN_DECLINES);
|
|
91
|
+
export const CLASSIFY_SEAT_DECLINE_EVIDENCE = new Set();
|
|
92
|
+
function readBoundaryMandate(command, allowSet, resolved, verdict, declineEvidence) {
|
|
90
93
|
if (resolved === undefined)
|
|
91
94
|
return {};
|
|
92
|
-
if (denyJudgeSpoke(verdict) || unjudgedRead(command, allowSet, resolved, verdict))
|
|
95
|
+
if (denyJudgeSpoke(verdict) || unjudgedRead(command, allowSet, resolved, verdict, declineEvidence))
|
|
93
96
|
return { mandated: true };
|
|
94
|
-
|
|
97
|
+
const named = classifyOutOfRootReadGate(command, allowSet, resolved);
|
|
98
|
+
return named.gated || named.denied === true ? { mandated: true } : {};
|
|
95
99
|
}
|
|
96
100
|
function boundaryUnjudged(verdict) {
|
|
97
101
|
return verdict.refusedWhole === true || (verdict.unresolvedOperands !== undefined && verdict.unresolvedOperands.length > 0);
|
|
98
102
|
}
|
|
99
|
-
function unjudgedRead(command, allowSet, resolved, verdict) {
|
|
103
|
+
function unjudgedRead(command, allowSet, resolved, verdict, declineEvidence) {
|
|
100
104
|
if (verdict.unresolvedOperands !== undefined && verdict.unresolvedOperands.length > 0)
|
|
101
105
|
return true;
|
|
102
106
|
if (verdict.refusedWhole !== true)
|
|
103
107
|
return false;
|
|
104
|
-
if (refusedCommandMayRead(command, allowSet, resolved))
|
|
108
|
+
if (refusedCommandMayRead(command, allowSet, resolved, declineEvidence))
|
|
105
109
|
return true;
|
|
106
|
-
|
|
110
|
+
if (resolved === undefined)
|
|
111
|
+
return false;
|
|
112
|
+
const named = classifyOutOfRootReadGate(command, allowSet, resolved);
|
|
113
|
+
return named.gated || named.denied === true;
|
|
107
114
|
}
|
|
108
115
|
function recursiveReadCause(detailed) {
|
|
109
116
|
const recursive = detailed.recursiveReadPaths;
|
|
@@ -121,7 +128,7 @@ export function bashReadBoundaryProbe(boundary) {
|
|
|
121
128
|
const allowSet = new Set(bashClassifyDefaultAllow());
|
|
122
129
|
const judge = (command, resolved, offers) => {
|
|
123
130
|
const seatVerdict = (verdict) => {
|
|
124
|
-
if (boundaryDeclared(verdict) || unjudgedRead(command, allowSet, resolved, verdict)) {
|
|
131
|
+
if (boundaryDeclared(verdict) || unjudgedRead(command, allowSet, resolved, verdict, BOUNDARY_SEAT_DECLINE_EVIDENCE)) {
|
|
125
132
|
const cause = recursiveReadCause(verdict);
|
|
126
133
|
return { reversible: false, mandated: true, ...(cause !== undefined ? { cause } : {}) };
|
|
127
134
|
}
|
|
@@ -145,7 +152,7 @@ export function bashReadBoundaryProbe(boundary) {
|
|
|
145
152
|
const offerFrom = (verdict) => (offers ? readRootCandidateOf(verdict) : {});
|
|
146
153
|
if (mandated !== undefined)
|
|
147
154
|
return { ...mandated, ...offerFrom(detailed), ...offerFrom(loop) };
|
|
148
|
-
if (readBoundaryMandate(command, allowSet, resolved, detailed).mandated === true)
|
|
155
|
+
if (readBoundaryMandate(command, allowSet, resolved, detailed, BOUNDARY_SEAT_DECLINE_EVIDENCE).mandated === true)
|
|
149
156
|
return { reversible: false, mandated: true };
|
|
150
157
|
return compoundVerdict ?? loopVerdict ?? { reversible: true };
|
|
151
158
|
};
|
package/dist/tools/fs/fs-read.js
CHANGED
|
@@ -231,7 +231,7 @@ export function createReadFileTool(env, state, rootCanonical, cwdRef, additional
|
|
|
231
231
|
if (prevNb && !prevNb.isPartialView && prevNb.hash === hash && prevNb.view && prevNb.view.start === 1 && prevNb.view.end === total) {
|
|
232
232
|
return fileUnchangedResult(`[${path}: unchanged since you last read it (lines 1-${total} of ${total}); content omitted to save context]`, "unchanged-since-last-read", 1, total, total);
|
|
233
233
|
}
|
|
234
|
-
state.set(r.key, { hash, totalLines: countLines(content), truncated: false, view: { start: 1, end: total }, lastReadAt: Date.now() });
|
|
234
|
+
state.set(r.key, { hash, totalLines: countLines(content), truncated: false, view: { start: 1, end: total }, lastReadAt: Date.now(), transmittedByToolCallId: ctx.toolCallId });
|
|
235
235
|
const bodyBlocks = rendered.blocks.length > 0 ? rendered.blocks : [{ type: "text", text: "[notebook has 0 cells]" }];
|
|
236
236
|
const nbDisclosure = discloseReminderShaped({
|
|
237
237
|
segments: rendered.blocks.filter((b) => b.type === "text").map((b) => b.text),
|
|
@@ -307,6 +307,7 @@ export function createReadFileTool(env, state, rootCanonical, cwdRef, additional
|
|
|
307
307
|
...(pageMarker !== undefined ? { isPartialView: true } : {}),
|
|
308
308
|
view: { start, end },
|
|
309
309
|
lastReadAt: Date.now(),
|
|
310
|
+
transmittedByToolCallId: ctx.toolCallId,
|
|
310
311
|
});
|
|
311
312
|
if (total === 0)
|
|
312
313
|
return `${openSystemReminder(reminderMark)}Warning: the file exists but the contents are empty.</system-reminder>`;
|
|
@@ -375,9 +375,7 @@ export declare const CWD_SENTINEL = "__cc_cwd_9f2c1b__";
|
|
|
375
375
|
* TWO CALLERS, ONE RULE, which is why the marker is optional. The foreground result path KNOWS the
|
|
376
376
|
* marker it minted and passes it (exact, unambiguous). A backgrounded command's spool is assembled by a
|
|
377
377
|
* different lane entirely — the shell row outlives the run that launched it and never had the marker —
|
|
378
|
-
* so it passes none and the marker is recovered per span from the sentinel plus its hex suffix.
|
|
379
|
-
* this was shared, only the foreground path stripped, and the completion notification handed a person
|
|
380
|
-
* the raw internal marker with the directory inside it.
|
|
378
|
+
* so it passes none and the marker is recovered per span from the sentinel plus its hex suffix.
|
|
381
379
|
*/
|
|
382
380
|
export declare function stripCwdSentinel(stderr: string, marker?: string): {
|
|
383
381
|
stderr: string;
|
|
@@ -504,10 +502,38 @@ export declare function inheritSessionReadFileState(target: ReadFileState, inher
|
|
|
504
502
|
* coordinate (canonicalized the same way the Read/Edit/Write tools populate it), so a plain lookup
|
|
505
503
|
* suffices; no re-hash/re-register needed since the entry's content genuinely has not changed.
|
|
506
504
|
*/
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
505
|
+
/**
|
|
506
|
+
* The SECOND way a transmission leaves the model's view, and why this function is named for context
|
|
507
|
+
* SHRINK rather than compaction: the request-layer stale-result pass replaces an old tool result's
|
|
508
|
+
* content with a cleared marker — no summary, no re-attachment, nothing changed in the session — and
|
|
509
|
+
* for a Read result the effect on the entry is identical: the body its dedup stub points at is no
|
|
510
|
+
* longer anywhere the model can see, so the entry must retire with it.
|
|
511
|
+
*
|
|
512
|
+
* Selection is by the TRANSMISSION, not by path — an entry a LATER Read re-transmitted carries that
|
|
513
|
+
* later call's id and so survives a clear of the earlier one, which a path-keyed retirement could not
|
|
514
|
+
* tell apart. KNOWN IMPRECISION: a call id is not unique over a run (the clear ledger keys occurrences
|
|
515
|
+
* by id + message timestamp + ordinal for that reason) and the finer key is not reachable at the Read
|
|
516
|
+
* mint, which does not know the timestamp of the message its result will land in. A reused id can
|
|
517
|
+
* therefore retire one entry too many, costing a re-transmission and a tightened read-before-edit gate
|
|
518
|
+
* — the same tightening the summarized arm accepts wholesale. The converse is not reachable: an entry
|
|
519
|
+
* carries the id of the last Read that transmitted it, so if that result was cleared its id is in the list.
|
|
520
|
+
*/
|
|
521
|
+
export type ReadFileStateShrink =
|
|
522
|
+
/** A landed compaction: `attachedComplete` re-entered the context whole; `preserveKeys` stayed visible in the kept tail. */
|
|
523
|
+
{
|
|
524
|
+
kind: "summarized";
|
|
525
|
+
attachedComplete: ReadonlyArray<{
|
|
526
|
+
path: string;
|
|
527
|
+
content: string;
|
|
528
|
+
}>;
|
|
529
|
+
preserveKeys?: ReadonlyArray<string>;
|
|
530
|
+
}
|
|
531
|
+
/** A request-layer stale-result clear: these tool calls' results are markers now. Nothing re-entered. */
|
|
532
|
+
| {
|
|
533
|
+
kind: "tool-results-cleared";
|
|
534
|
+
clearedToolCallIds: ReadonlyArray<string>;
|
|
535
|
+
};
|
|
536
|
+
export declare function applyContextShrinkToReadFileState(state: ReadFileState, shrink: ReadFileStateShrink): void;
|
|
511
537
|
/** CC 2.1.198 verbatim skeleton: the ENOENT error body for a
|
|
512
538
|
* missing Read/Edit target — `File does not exist. Note: your current working directory is <cwd>.`
|
|
513
539
|
* plus a `Did you mean <sibling>?` correction when the parent directory holds a near-name (case
|
|
@@ -318,7 +318,18 @@ export async function inheritSessionReadFileState(target, inherited, judgeKey) {
|
|
|
318
318
|
target.set(judged, entry);
|
|
319
319
|
}
|
|
320
320
|
}
|
|
321
|
-
export function
|
|
321
|
+
export function applyContextShrinkToReadFileState(state, shrink) {
|
|
322
|
+
if (shrink.kind === "tool-results-cleared") {
|
|
323
|
+
if (shrink.clearedToolCallIds.length === 0)
|
|
324
|
+
return;
|
|
325
|
+
const cleared = new Set(shrink.clearedToolCallIds);
|
|
326
|
+
for (const [k, v] of [...state]) {
|
|
327
|
+
if (v.transmittedByToolCallId !== undefined && cleared.has(v.transmittedByToolCallId))
|
|
328
|
+
state.delete(k);
|
|
329
|
+
}
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const { attachedComplete, preserveKeys = [] } = shrink;
|
|
322
333
|
const preserve = new Set(preserveKeys);
|
|
323
334
|
for (const [k, v] of [...state]) {
|
|
324
335
|
if (v.seededFromContext !== true && !preserve.has(k))
|
|
@@ -77,6 +77,13 @@ export interface ReadEntry {
|
|
|
77
77
|
* disk (hash mismatch) always serves real content and the dedup disarms permanently.
|
|
78
78
|
* Optional/additive: absent = normal entry. */
|
|
79
79
|
seededFromContext?: boolean;
|
|
80
|
+
/** The id of the tool call whose RESULT carried the body this entry dedups against: when the
|
|
81
|
+
* request-layer stale-result pass clears THAT transmission, the entry retires with it
|
|
82
|
+
* (`applyContextShrinkToReadFileState`, `tool-results-cleared` arm). Recorded only by the Read
|
|
83
|
+
* lanes that TRANSMIT a body — a write-back's content is the model's own arguments and a
|
|
84
|
+
* seeded/re-attached entry never rode a tool result, so neither carries this and neither is
|
|
85
|
+
* retired by a clear. Optional/additive: absent (an older checkpoint) never matches a cleared id. */
|
|
86
|
+
transmittedByToolCallId?: string;
|
|
80
87
|
}
|
|
81
88
|
/** Per-task record of which files have been read, keyed by canonical path: owned by prepare-task,
|
|
82
89
|
* closure-captured by the tools, and serialized into the durable checkpoint. */
|
|
@@ -385,6 +392,36 @@ export declare function isShellRootedSpellingUnmapped(family: PathFamily | undef
|
|
|
385
392
|
* ⇒ `\\\\host\\share\\x`), for a lexical normalizer that recognises the backslash form only; every other spelling is
|
|
386
393
|
* returned as is. */
|
|
387
394
|
export declare function nativeUncSpellingOf(family: PathFamily | undefined, spelled: string): string;
|
|
395
|
+
/**
|
|
396
|
+
* Fold a TRAILING separator run off a spelling that is about to become a KEY: `<dir>/adir`, `<dir>/adir/`
|
|
397
|
+
* and `<dir>/adir//` name one directory and must mint one key. The existing-path arm folds them already,
|
|
398
|
+
* as a side effect of asking `realpath`; the arm that mints a key for a path that does NOT exist yet
|
|
399
|
+
* rejoined the model's own tail verbatim, so the same not-yet-created directory arrived under two and
|
|
400
|
+
* three keys — and a byte-wise reader (the per-file read state, the write gate's per-key ledger) treats
|
|
401
|
+
* two keys as two files.
|
|
402
|
+
*
|
|
403
|
+
* Called on the ANSWER, at the canonicalizer's single exit, and never on a spelling about to be probed.
|
|
404
|
+
* Two things break if it moves earlier. A trailing separator is an ASSERTION that the target is a
|
|
405
|
+
* directory and the platform enforces it (`<existing regular file>/` answers `not_directory`, reported
|
|
406
|
+
* here as a refusal), so folding before the probe spends that refusal and the slash-suffixed spelling of
|
|
407
|
+
* a file resolves to the file. And the ancestor walk chooses which ancestor to canonicalize by SPLITTING
|
|
408
|
+
* the spelling it was handed: a leaf stripped of its separator first is skipped as though it had been
|
|
409
|
+
* resolved, so `<root>/link/` — a link to a directory outside the root — comes back as the in-root
|
|
410
|
+
* spelling instead of the destination. Folding the answer costs neither.
|
|
411
|
+
*
|
|
412
|
+
* A spelling whose remainder is the bare ANCHOR keeps the separator the platform writes on a root — `/`,
|
|
413
|
+
* `C:\`, `\\host\share\`. That run IS the root's own spelling rather than a tail nobody wrote, and
|
|
414
|
+
* stripping it renames the spelling to nothing, to a DRIVE-RELATIVE location, or to a share root written
|
|
415
|
+
* unlike every other root. The question is asked in the module's own vocabulary ({@link pathIdentityOf} +
|
|
416
|
+
* {@link isIdentityDir}: is anything left BELOW the anchor?), so the three families need no arm each.
|
|
417
|
+
* Interior runs are NOT touched here: the ancestor walk folds them by probing, and a run in the middle of
|
|
418
|
+
* a name is the platform's to read.
|
|
419
|
+
*
|
|
420
|
+
* Scanned backwards rather than matched: `/[\\/]+$/` against a model-supplied spelling is quadratic when a
|
|
421
|
+
* long separator run precedes a non-separator (measured: ~5s for 32k leading slashes, on the caller's
|
|
422
|
+
* thread), and a path spelling is unbounded input.
|
|
423
|
+
*/
|
|
424
|
+
export declare function foldTrailingSeparators(spelled: string, family: PathFamily): string;
|
|
388
425
|
/**
|
|
389
426
|
* Join a RELATIVE-OR-ROOTED spelling to a base under the family, without folding (`..` across a symlinked
|
|
390
427
|
* parent is the env's to resolve). win = `path.win32` semantics: `/x`/`\x` lands on the base's drive or
|
package/dist/tools/fs/safety.js
CHANGED
|
@@ -371,11 +371,17 @@ function mintIdentity(anchor, segments, stripTail) {
|
|
|
371
371
|
out.pop();
|
|
372
372
|
continue;
|
|
373
373
|
}
|
|
374
|
-
const stripped = stripTail ? seg.
|
|
374
|
+
const stripped = stripTail ? seg.slice(0, trailingDotSpaceStart(seg)) : seg;
|
|
375
375
|
out.push(stripped.length > 0 ? stripped : seg);
|
|
376
376
|
}
|
|
377
377
|
return (out.length === 0 ? anchor || "/" : `${anchor}/${out.join("/")}`);
|
|
378
378
|
}
|
|
379
|
+
function trailingDotSpaceStart(seg) {
|
|
380
|
+
let end = seg.length;
|
|
381
|
+
while (end > 0 && (seg[end - 1] === "." || seg[end - 1] === " "))
|
|
382
|
+
end -= 1;
|
|
383
|
+
return end;
|
|
384
|
+
}
|
|
379
385
|
function identityAnchorOf(identity) {
|
|
380
386
|
return identity.startsWith("//") ? identity.split("/").slice(0, 4).join("/") : identity.slice(0, 2);
|
|
381
387
|
}
|
|
@@ -403,6 +409,18 @@ export function isShellRootedSpellingUnmapped(family, spelled) {
|
|
|
403
409
|
export function nativeUncSpellingOf(family, spelled) {
|
|
404
410
|
return family === "win" && isUncSpelling(spelled) ? `\\\\${spelled.slice(2).replace(/\//g, "\\")}` : spelled;
|
|
405
411
|
}
|
|
412
|
+
export function foldTrailingSeparators(spelled, family) {
|
|
413
|
+
let end = spelled.length;
|
|
414
|
+
while (end > 0 && (spelled[end - 1] === "/" || (family === "win" && spelled[end - 1] === "\\")))
|
|
415
|
+
end -= 1;
|
|
416
|
+
if (end === spelled.length)
|
|
417
|
+
return spelled;
|
|
418
|
+
const stripped = spelled.slice(0, end);
|
|
419
|
+
const identity = pathIdentityOf(stripped, family);
|
|
420
|
+
if (!identity.ok || !isIdentityDir(identity.identity, family))
|
|
421
|
+
return spelled;
|
|
422
|
+
return stripped;
|
|
423
|
+
}
|
|
406
424
|
export function joinForFamily(family, base, rel) {
|
|
407
425
|
if (family === "posix")
|
|
408
426
|
return `${base.replace(/\/+$/, "")}/${rel}`;
|
|
@@ -500,7 +518,8 @@ export async function canonicalizeTarget(env, path, signal, family, baseCwd) {
|
|
|
500
518
|
throw e;
|
|
501
519
|
});
|
|
502
520
|
try {
|
|
503
|
-
|
|
521
|
+
const answer = await withGateTimeout(probe, DEFAULT_GATE_AWAIT_TIMEOUT_MS, signal, "pathCanonicalization");
|
|
522
|
+
return answer.ok ? { ...answer, key: foldTrailingSeparators(answer.key, family) } : answer;
|
|
504
523
|
}
|
|
505
524
|
catch (err) {
|
|
506
525
|
if (seenThrow && err === thrownByProbe)
|
|
@@ -546,7 +565,16 @@ async function probeRealTarget(env, path, target, family, signal) {
|
|
|
546
565
|
if (!link.ok) {
|
|
547
566
|
return { ok: false, message: `cannot read symlink "${path}": ${link.error.message}`, unresolvedSymlink: true };
|
|
548
567
|
}
|
|
549
|
-
const
|
|
568
|
+
const screenedTarget = win32NamespaceScreen(link.value, family);
|
|
569
|
+
if (!screenedTarget.ok) {
|
|
570
|
+
return {
|
|
571
|
+
ok: false,
|
|
572
|
+
message: `cannot resolve symlink "${path}": its target "${clipSpelling(link.value)}" ${WIN32_NAMESPACE_REFUSAL_TEXT[screenedTarget.refuse]}; refused (${screenedTarget.refuse}).`,
|
|
573
|
+
refused: screenedTarget.refuse,
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
const hopSpelling = family === "win" ? screenedTarget.spelled : link.value;
|
|
577
|
+
const linkTarget = isAbsoluteForFamily(family, hopSpelling) ? hopSpelling : joinForFamily(family, parentDir(abs), hopSpelling);
|
|
550
578
|
const reabs = await env.absolutePath(linkTarget, signal);
|
|
551
579
|
if (!reabs.ok) {
|
|
552
580
|
return { ok: false, message: `cannot resolve symlink target "${linkTarget}" for "${path}": ${reabs.error.message}`, unresolvedSymlink: true };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/core",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.23.1",
|
|
4
4
|
"description": "Stateless, task-oriented AI agent core",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BUSL-1.1",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
|
50
50
|
"build": "rm -rf dist && tsc -p tsconfig.build.json && tsc -p tsconfig.build.json --emitDeclarationOnly --removeComments false",
|
|
51
|
-
"typecheck": "tsc --noEmit",
|
|
51
|
+
"typecheck": "tsc --noEmit && echo \"typecheck OK (tsc --noEmit clean)\"",
|
|
52
52
|
"pretest": "npm run gate:debts",
|
|
53
53
|
"test": "vitest run --exclude \"**/*.local.test.ts\"",
|
|
54
54
|
"test:watch": "vitest",
|
|
@@ -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": 2395,
|
|
5
5
|
"exports": {
|
|
6
6
|
"A2ATaskState": "type",
|
|
7
7
|
"A2ATaskStateReversal": "type",
|
|
@@ -1615,6 +1615,7 @@
|
|
|
1615
1615
|
"WriteProtectionMount": "interface",
|
|
1616
1616
|
"WriteProtectionTargetView": "type",
|
|
1617
1617
|
"WriteProtectionVerdict": "type",
|
|
1618
|
+
"WriteProtectionViewAbsence": "interface",
|
|
1618
1619
|
"WriteReceipt": "interface",
|
|
1619
1620
|
"XARGS_READ_ONLY_TARGETS": "variable",
|
|
1620
1621
|
"ackAdoptionConfig": "function",
|
|
@@ -4011,6 +4012,7 @@
|
|
|
4011
4012
|
"WriteProtectionMount": "advanced",
|
|
4012
4013
|
"WriteProtectionTargetView": "advanced",
|
|
4013
4014
|
"WriteProtectionVerdict": "advanced",
|
|
4015
|
+
"WriteProtectionViewAbsence": "advanced",
|
|
4014
4016
|
"WriteReceipt": "internal",
|
|
4015
4017
|
"XARGS_READ_ONLY_TARGETS": "advanced",
|
|
4016
4018
|
"ackAdoptionConfig": "advanced",
|