@sema-agent/core 7.33.7 → 7.34.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 +357 -0
- package/dist/agents/cross-session-judge.d.ts +16 -7
- package/dist/agents/cross-session-judge.js +18 -15
- package/dist/agents/peer-session-drain.d.ts +4 -0
- package/dist/agents/peer-session-drain.js +5 -2
- package/dist/agents/repair-loop.d.ts +1 -1
- package/dist/agents/subagent.d.ts +3 -0
- package/dist/agents/subagent.js +12 -3
- package/dist/brain/anthropic.js +1 -0
- package/dist/brain/open-responses.js +1 -0
- package/dist/brain/openai.js +1 -0
- package/dist/brain/stream-engine.d.ts +3 -0
- package/dist/brain/stream-engine.js +2 -1
- package/dist/core/a2a.d.ts +3 -1
- package/dist/core/a2a.js +1 -1
- package/dist/core/ask-origin.d.ts +351 -154
- package/dist/core/ask-origin.js +223 -45
- package/dist/core/ask-unresolvable-notice.d.ts +5 -2
- package/dist/core/ask-unresolvable-notice.js +4 -3
- package/dist/core/auto-mode-arming.d.ts +17 -4
- package/dist/core/auto-mode-arming.js +31 -6
- package/dist/core/auto-mode-defaults.d.ts +16 -7
- package/dist/core/auto-mode-defaults.js +2 -1
- package/dist/core/auto-mode-directive.d.ts +18 -30
- package/dist/core/auto-mode-directive.js +9 -18
- package/dist/core/auto-mode-prompt.d.ts +62 -1
- package/dist/core/auto-mode-prompt.js +42 -4
- package/dist/core/auto-mode-rebuild.d.ts +2 -1
- package/dist/core/auto-mode-rebuild.js +1 -0
- package/dist/core/auto-mode.d.ts +32 -3
- package/dist/core/auto-mode.js +80 -41
- package/dist/core/checkpoint-store.d.ts +50 -35
- package/dist/core/checkpoint-store.js +58 -3
- package/dist/core/child-clamp-seats.js +1 -0
- package/dist/core/classifier-unavailable-arm.d.ts +57 -0
- package/dist/core/classifier-unavailable-arm.js +17 -0
- package/dist/core/effective-path-target.d.ts +12 -0
- package/dist/core/effective-path-target.js +18 -7
- package/dist/core/engine-notice.d.ts +53 -2
- package/dist/core/fs-write-gate-policy.d.ts +5 -1
- package/dist/core/fs-write-gate-policy.js +4 -1
- package/dist/core/gate-fold.js +17 -1
- package/dist/core/gate-lanes.js +111 -46
- package/dist/core/gate-outcome.d.ts +40 -7
- package/dist/core/gate-outcome.js +26 -4
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +12 -0
- package/dist/core/hooks.d.ts +50 -29
- package/dist/core/hooks.js +1 -3
- package/dist/core/mcp.js +1 -1
- package/dist/core/mode-policy.d.ts +182 -0
- package/dist/core/mode-policy.js +179 -0
- package/dist/core/oracle-isolation.d.ts +3 -3
- package/dist/core/permission-rule-model.d.ts +16 -3
- package/dist/core/permission-rule-model.js +36 -3
- package/dist/core/permission-rule-provider.d.ts +22 -11
- package/dist/core/permission-rule-provider.js +17 -7
- package/dist/core/permission-rules.d.ts +23 -1
- package/dist/core/permission-rules.js +35 -8
- package/dist/core/persisted-rule-arms.d.ts +10 -9
- package/dist/core/persisted-rule-arms.js +9 -6
- package/dist/core/present-plan-tool.d.ts +1 -1
- package/dist/core/present-plan-tool.js +3 -3
- package/dist/core/read-only-shell.d.ts +19 -19
- package/dist/core/read-only-shell.js +38 -12
- package/dist/core/retired-keys.d.ts +9 -3
- package/dist/core/retired-keys.js +5 -1
- package/dist/core/runner/classifier-ladder.d.ts +43 -1
- package/dist/core/runner/classifier-ladder.js +29 -0
- package/dist/core/runner/contracts.d.ts +37 -35
- package/dist/core/runner/denial-limit-arms.d.ts +30 -7
- package/dist/core/runner/denial-limit-arms.js +15 -9
- package/dist/core/runner/env-capability-census.js +1 -1
- package/dist/core/runner/gate-exit.d.ts +30 -4
- package/dist/core/runner/gate-exit.js +28 -7
- package/dist/core/runner/permission-rule-lanes.d.ts +9 -5
- package/dist/core/runner/permission-rule-lanes.js +44 -40
- package/dist/core/runner/persisted-path-rows.d.ts +64 -0
- package/dist/core/runner/persisted-path-rows.js +89 -0
- package/dist/core/runner/prepare-artifact.d.ts +5 -1
- package/dist/core/runner/prepare-artifact.js +9 -3
- package/dist/core/runner/prepare-ask-lane.d.ts +5 -3
- package/dist/core/runner/prepare-ask-lane.js +1 -1
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +39 -13
- package/dist/core/runner/prepare-caps-and-workflow.js +58 -29
- package/dist/core/runner/prepare-gate-stations.d.ts +33 -6
- package/dist/core/runner/prepare-gate-stations.js +32 -19
- package/dist/core/runner/prepare-hands-readface.d.ts +40 -17
- package/dist/core/runner/prepare-hands-readface.js +64 -53
- package/dist/core/runner/prepare-inherited-gate.d.ts +22 -22
- package/dist/core/runner/prepare-inherited-gate.js +37 -23
- package/dist/core/runner/prepare-park-ask.d.ts +2 -2
- package/dist/core/runner/prepare-park-ask.js +2 -1
- package/dist/core/runner/prepare-policy-chain.d.ts +22 -2
- package/dist/core/runner/prepare-policy-chain.js +39 -12
- package/dist/core/runner/prepare-protocol-tools.d.ts +28 -0
- package/dist/core/runner/prepare-protocol-tools.js +13 -3
- package/dist/core/runner/prepare-run-refs.d.ts +6 -6
- package/dist/core/runner/prepare-run-refs.js +9 -4
- package/dist/core/runner/prepare-safety-scan.d.ts +5 -0
- package/dist/core/runner/prepare-safety-scan.js +19 -3
- package/dist/core/runner/prepare-suspend-saga.d.ts +5 -5
- package/dist/core/runner/prepare-suspend-saga.js +6 -6
- package/dist/core/runner/prepare-task.js +51 -22
- package/dist/core/runner/prepare-wiring-manifest.d.ts +10 -6
- package/dist/core/runner/prepare-wiring-manifest.js +5 -3
- package/dist/core/runner/read-root-grant.d.ts +9 -0
- package/dist/core/runner/read-root-grant.js +12 -0
- package/dist/core/runner/retired-runner-deps.js +1 -0
- package/dist/core/runner/run-identity-wiring.js +1 -1
- package/dist/core/runner/run-leg.js +24 -15
- package/dist/core/runner/run-notification-lane.js +1 -0
- package/dist/core/runner/run-telemetry-and-budget-seats.d.ts +2 -2
- package/dist/core/runner/run-telemetry-and-budget-seats.js +1 -1
- package/dist/core/runner/run-turn-boundary.js +1 -1
- package/dist/core/runner/runtask.js +3 -0
- package/dist/core/runner/stream-mode-verb.d.ts +27 -0
- package/dist/core/runner/stream-mode-verb.js +19 -0
- package/dist/core/runner/turn-attachments.d.ts +3 -3
- package/dist/core/runner/turn-attachments.js +1 -1
- package/dist/core/runner/write-protection-station.d.ts +7 -1
- package/dist/core/runner/write-protection-station.js +6 -3
- package/dist/core/runner-deps.d.ts +31 -34
- package/dist/core/runtime-caps.d.ts +12 -3
- package/dist/core/safety-axis-vocab.d.ts +2 -2
- package/dist/core/safety-axis-vocab.js +1 -1
- package/dist/core/secret-env.d.ts +16 -2
- package/dist/core/secret-env.js +25 -1
- package/dist/core/sensitive-path-policy.d.ts +4 -2
- package/dist/core/sensitive-path-policy.js +39 -30
- package/dist/core/spec-contract.d.ts +15 -0
- package/dist/core/spec-contract.js +19 -10
- package/dist/core/task-event.d.ts +1 -1
- package/dist/core/task-result.d.ts +1 -1
- package/dist/core/task-spec.d.ts +41 -89
- package/dist/core/task-stream.d.ts +16 -0
- package/dist/core/terminal-cause.js +1 -0
- package/dist/core/tighten-task-spec.d.ts +2 -2
- package/dist/core/tighten-task-spec.js +12 -14
- package/dist/core/tool-catalog-entries.js +4 -3
- package/dist/core/tool-policy.d.ts +54 -25
- package/dist/core/tool-policy.js +43 -2
- package/dist/core/tool-registry.d.ts +2 -0
- package/dist/core/tool-registry.js +3 -0
- package/dist/core/tool-spec.d.ts +8 -9
- package/dist/core/trace.d.ts +7 -6
- package/dist/core/types.d.ts +2 -1
- package/dist/core/wiring-manifest.d.ts +31 -5
- package/dist/core/wiring-manifest.js +28 -1
- package/dist/core/write-protect.d.ts +11 -1
- package/dist/core/write-protect.js +26 -15
- package/dist/engine/execution-env/kill-tree.js +4 -1
- package/dist/engine/execution-env/node-execution-env.d.ts +3 -0
- package/dist/engine/execution-env/node-execution-env.js +34 -35
- package/dist/engine/harness/types.d.ts +6 -7
- package/dist/engine/llm/types.d.ts +2 -4
- package/dist/engine/loop/types.d.ts +7 -0
- package/dist/index.d.ts +15 -5
- package/dist/index.js +12 -3
- package/dist/orchestration/run-spec.d.ts +4 -0
- package/dist/orchestration/run-spec.js +4 -3
- package/dist/orchestration/workflow-governance.js +1 -1
- package/dist/orchestration/workflow-primitives.d.ts +1 -1
- package/dist/orchestration/workflow-size-guideline.js +2 -2
- package/dist/stores/file/fs-atomic.js +2 -1
- package/dist/tools/artifact/artifact-tool.d.ts +17 -0
- package/dist/tools/artifact/artifact-tool.js +16 -1
- package/dist/tools/fs/bash-readonly-classifier.d.ts +28 -7
- package/dist/tools/fs/bash-readonly-classifier.js +88 -0
- package/dist/tools/fs/containment-face.d.ts +9 -3
- package/dist/tools/fs/containment-face.js +2 -0
- package/dist/tools/fs/file-read-check.d.ts +49 -0
- package/dist/tools/fs/file-read-check.js +71 -0
- package/dist/tools/fs/fs-bash.d.ts +3 -3
- package/dist/tools/fs/fs-read.d.ts +1 -0
- package/dist/tools/fs/fs-search-tools.d.ts +2 -2
- package/dist/tools/fs/fs-search-tools.js +4 -4
- package/dist/tools/fs/fs-shared.js +20 -6
- package/dist/tools/fs/fs-write.d.ts +4 -3
- package/dist/tools/fs/fs-write.js +9 -9
- package/dist/tools/fs/index.d.ts +39 -3
- package/dist/tools/fs/index.js +38 -15
- package/dist/tools/fs/read-deny-home.d.ts +93 -0
- package/dist/tools/fs/read-deny-home.js +225 -0
- package/dist/tools/fs/read-deny.d.ts +176 -34
- package/dist/tools/fs/read-deny.js +268 -61
- package/dist/tools/fs/safety.d.ts +6 -1
- package/dist/tools/fs/safety.js +18 -5
- package/dist/tools/fs/search.d.ts +3 -0
- package/dist/tools/fs/search.js +47 -9
- package/dist/tools/fs/shell-read-check.d.ts +42 -11
- package/dist/tools/fs/shell-read-check.js +188 -13
- package/dist/tools/fs/shell-read-establish.d.ts +7 -3
- package/dist/tools/fs/shell-read-establish.js +134 -21
- package/dist/tools/fs/shell-read-identity.d.ts +7 -0
- package/dist/tools/fs/shell-read-identity.js +21 -0
- package/dist/tools/fs/shell-read-judge.d.ts +92 -32
- package/dist/tools/fs/shell-read-judge.js +43 -18
- package/dist/tools/fs/shell-read-plan.d.ts +4 -0
- package/dist/tools/fs/shell-read-plan.js +26 -4
- package/dist/tools/fs/shell-read-write-targets.d.ts +73 -0
- package/dist/tools/fs/shell-read-write-targets.js +173 -0
- package/package.json +3 -1
- package/test/export-surface.snapshot.json +103 -11
- package/wire-changes/core@7.33.8.json +165 -0
- package/wire-changes/core@7.34.0.json +2621 -0
|
@@ -22,19 +22,22 @@ import { violationDetails } from "./safety.js";
|
|
|
22
22
|
import { type ShellReadPlan } from "./shell-read-plan.js";
|
|
23
23
|
import { type EstablishedRead, type ShellReadBoundary, type ShellReadDisposition, type ShellReadFinding, type ShellReadSeat } from "./shell-read-judge.js";
|
|
24
24
|
import type { ReadRootGrantCandidate } from "../../core/tool-spec.js";
|
|
25
|
-
/** The ONE mint of the containment card —
|
|
26
|
-
*
|
|
25
|
+
/** The ONE mint of the containment card — the targets a finding established OUTSIDE the roots (an out-of-root read, a
|
|
26
|
+
* target that resolves elsewhere by the spawn). */
|
|
27
27
|
declare function containmentDetails(paths: readonly string[]): {
|
|
28
28
|
readonly type: "readonly_out_of_root";
|
|
29
29
|
readonly code: "readonly_out_of_root";
|
|
30
30
|
readonly paths: readonly string[];
|
|
31
31
|
};
|
|
32
|
-
/** The
|
|
33
|
-
*
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
code: "
|
|
37
|
-
}
|
|
32
|
+
/** The ONE mint of the card of a finding that established nothing: where the command reads could not be established
|
|
33
|
+
* (an unreadable or re-entered program text, a program named from its input, a station that could not answer). */
|
|
34
|
+
declare function unestablishedDetails(): {
|
|
35
|
+
readonly type: "read_unestablished";
|
|
36
|
+
readonly code: "read_unestablished";
|
|
37
|
+
};
|
|
38
|
+
/** The structured details a refusal or a mandated card carries: the deny card is the read faces' own (`violationDetails`,
|
|
39
|
+
* safety.ts), the containment and the unestablished cards are minted above. */
|
|
40
|
+
export type ShellReadRefusalDetails = NonNullable<ReturnType<typeof violationDetails>> | ReturnType<typeof containmentDetails> | ReturnType<typeof unestablishedDetails>;
|
|
38
41
|
/** What the station answers for ONE call. */
|
|
39
42
|
export interface ShellReadVerdict {
|
|
40
43
|
readonly disposition: ShellReadDisposition;
|
|
@@ -43,12 +46,40 @@ export interface ShellReadVerdict {
|
|
|
43
46
|
* sentence). `mandated` / `ordinary`: the ACCOUNT of the finding, for the approval card's disclosure seat
|
|
44
47
|
* (never the ask's own message — that is the auto-mode classifier's input). Absent for `vouch`. */
|
|
45
48
|
readonly sentence?: string;
|
|
46
|
-
/** `refuse`: the refusal's structured details. `mandated` on an `outside_roots` / `unestablishable` finding
|
|
47
|
-
*
|
|
48
|
-
* its `tool_end`
|
|
49
|
+
/** `refuse`: the refusal's structured details. `mandated` on an `outside_roots` / `unestablishable` finding (and
|
|
50
|
+
* `ordinary` on the file tools' out-of-root read): the finding's card, so an ask a person refuses or nobody can answer
|
|
51
|
+
* still carries WHAT was found on its `tool_end` — the targets outside the roots (`readonly_out_of_root`, or the file
|
|
52
|
+
* tools' `path_not_in_root`), or that nothing could be established (`read_unestablished`). */
|
|
49
53
|
readonly details?: ShellReadRefusalDetails;
|
|
50
54
|
/** `mandated` on an `outside_roots` finding: the directory whose grant would clear this ask. */
|
|
51
55
|
readonly readRootCandidate?: ReadRootGrantCandidate;
|
|
56
|
+
/** A FACT beside the disposition, for the mode that answers a file command itself: every write target this command
|
|
57
|
+
* names was established inside the boundary's WRITE roots at every seat the program writes it (the entry, where it
|
|
58
|
+
* lands, or both — `WriteSeat`, shell-read-write-targets.ts), off the write-protection table and off every deployment
|
|
59
|
+
* write guard (`ShellReadBoundary.writeProtectionJudge` / `writeTargetGuard`), no removal (`rm` / `rmdir`) targets a
|
|
60
|
+
* write root or a directory containing one, every program the command runs is in its text (no `sed -f` file, no
|
|
61
|
+
* re-entered shell, no unreadable text), and the line's order keeps each segment's establishment true
|
|
62
|
+
* ({@link lineOrderPlaced}). Absent ⇒ not established — the mode does not answer; the station's own table is unchanged. */
|
|
63
|
+
readonly writesPlaced?: true;
|
|
64
|
+
/** A FACT beside the disposition: a write target this command names lands on the write-protection table — judged by
|
|
65
|
+
* the table's own judge, as a write tool's target is — so the command's ask carries that table's class, whichever
|
|
66
|
+
* mode reads it. Absent ⇒ no target the station established is covered (or no judge is mounted). */
|
|
67
|
+
readonly writeProtected?: true;
|
|
68
|
+
/** A FACT beside `writesPlaced`'s absence: the command is one of the mode's file commands whose writes the station would
|
|
69
|
+
* have placed but for an OPTION WORD on a `cp` / `mv` segment ({@link UNPLACED_OPTIONS}: any word starting with `-` — the
|
|
70
|
+
* reference implementation's rule, "with flags requires manual approval to ensure path safety"). Names the first such
|
|
71
|
+
* program, so the approval card and the headless refusal can say WHY the mode did not answer. Absent ⇒ not that reason
|
|
72
|
+
* (or the writes were placed). */
|
|
73
|
+
readonly unplacedOptions?: {
|
|
74
|
+
readonly program: "cp" | "mv";
|
|
75
|
+
};
|
|
76
|
+
/** A FACT beside the disposition: some read of the command could not be established (an `unestablishable` or `declined`
|
|
77
|
+
* finding anywhere in it, whichever finding won). A mode that releases the call releases it past the read-deny table. */
|
|
78
|
+
readonly unestablished?: true;
|
|
79
|
+
/** The station's admission of this call's judged target, activated only by the caller that APPLIES this verdict (the
|
|
80
|
+
* gate's fold, the resumed leg's belt): a verdict nobody received — a wait that elapsed, a judgment that threw —
|
|
81
|
+
* admits nothing, whenever it completes. */
|
|
82
|
+
readonly admit?: () => void;
|
|
52
83
|
}
|
|
53
84
|
/** The station seat one tool call is judged on (the `rejudge` seat is asked for by the caller). */
|
|
54
85
|
export type ShellReadStationSeat = Exclude<ShellReadSeat, "rejudge">;
|
|
@@ -1,17 +1,182 @@
|
|
|
1
1
|
import { errorResult } from "../../core/tools.js";
|
|
2
2
|
import { BASH_READONLY_CONFINEMENT_NOTE } from "./fs-shared.js";
|
|
3
|
-
import { violationDetails } from "./safety.js";
|
|
3
|
+
import { isAbsoluteForFamily, normalizeAbsPathLexically, pathFamilyOf, violationDetails, withinAnyRoot } from "./safety.js";
|
|
4
4
|
import { fitMiddle, READ_DENY_SENTENCE_PART_MAX, readDenySentence } from "./read-deny.js";
|
|
5
5
|
import { NOT_AUTO_ALLOWED, programPositionRefusalSentence, readProgramPosition } from "./bash-program-position.js";
|
|
6
6
|
import { bashClassifyDefaultAllow } from "./bash-readonly-classifier.js";
|
|
7
7
|
import { planShellReads } from "./shell-read-plan.js";
|
|
8
8
|
import { createShellReadEstablisher, establishShellReadsLexically } from "./shell-read-establish.js";
|
|
9
|
-
import {
|
|
9
|
+
import { programArgs, spellsOption } from "./shell-read-write-targets.js";
|
|
10
|
+
import { allFindings, decideShellRead, denyRowAtOrBelow, foldShellReadFindings, judgeEstablishedPlan, readRootGrantCandidateOf, } from "./shell-read-judge.js";
|
|
10
11
|
import { normalizeReadRootCandidate } from "../../core/read-root-candidate.js";
|
|
11
12
|
import { argvOf, lexShellText } from "../../shell/lex.js";
|
|
12
13
|
function containmentDetails(paths) {
|
|
13
14
|
return { type: "readonly_out_of_root", code: "readonly_out_of_root", paths };
|
|
14
15
|
}
|
|
16
|
+
function unestablishedDetails() {
|
|
17
|
+
return { type: "read_unestablished", code: "read_unestablished" };
|
|
18
|
+
}
|
|
19
|
+
function findingDetails(finding, paths) {
|
|
20
|
+
return finding === "unestablishable" || finding === "declined" ? unestablishedDetails() : containmentDetails(paths);
|
|
21
|
+
}
|
|
22
|
+
const REMOVAL_COMMANDS = new Set(["rm", "rmdir"]);
|
|
23
|
+
const WRITES_BENEATH = {
|
|
24
|
+
cp: () => true,
|
|
25
|
+
mv: () => true,
|
|
26
|
+
rm: (s) => s.argv.some((w) => spellsOption(w.folded, "recursive", "[rR]")),
|
|
27
|
+
};
|
|
28
|
+
const isPattern = (f) => f.read.candidate.expands !== false && f.read.candidate.expands !== "unresolvable";
|
|
29
|
+
const RESHAPES_TREE = new Set(["mv"]);
|
|
30
|
+
const anyOptionWord = (words) => words.some((w) => w.startsWith("-"));
|
|
31
|
+
const UNPLACED_OPTIONS = {
|
|
32
|
+
rmdir: (ws) => ws.some((w) => spellsOption(w, "parents", "p")),
|
|
33
|
+
sed: (ws) => ws.some((w) => spellsOption(w, "file", "f")),
|
|
34
|
+
cp: anyOptionWord,
|
|
35
|
+
mv: anyOptionWord,
|
|
36
|
+
};
|
|
37
|
+
function runOrderOf(judged) {
|
|
38
|
+
const runs = [];
|
|
39
|
+
const plans = [];
|
|
40
|
+
let concurrent = false;
|
|
41
|
+
const walk = (j) => {
|
|
42
|
+
plans.push(j);
|
|
43
|
+
for (const s of j.segments) {
|
|
44
|
+
s.nested.forEach(walk);
|
|
45
|
+
runs.push(s);
|
|
46
|
+
if (j.plan.shape.backgrounded || s.segment.connector === "|")
|
|
47
|
+
concurrent = true;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
walk(judged);
|
|
51
|
+
return { runs, plans, concurrent };
|
|
52
|
+
}
|
|
53
|
+
function lineOrderPlaced(runs, concurrent) {
|
|
54
|
+
const reshapes = (s) => RESHAPES_TREE.has(s.segment.verb?.basename ?? "");
|
|
55
|
+
const writes = (s) => s.findings.some((f) => f.read.candidate.face === "write");
|
|
56
|
+
const stepsUp = (s) => s.findings.some((f) => f.read.candidate.spelled.split(/[\\/]/).includes(".."));
|
|
57
|
+
return runs.every((later, j) => runs.every((earlier, i) => i === j || (i > j && !concurrent) || (!reshapes(earlier) && !(writes(earlier) && stepsUp(later)))));
|
|
58
|
+
}
|
|
59
|
+
function writesPlacedOf(judged, boundary) {
|
|
60
|
+
const writeRoots = boundary.writeRoots;
|
|
61
|
+
if (writeRoots === undefined || writeRoots.length === 0)
|
|
62
|
+
return false;
|
|
63
|
+
const { runs, plans, concurrent } = runOrderOf(judged);
|
|
64
|
+
const placed = (s) => {
|
|
65
|
+
const verb = s.segment.verb?.basename ?? "";
|
|
66
|
+
if (s.segment.reentry === "shell" || s.segment.unreadProgramText !== undefined)
|
|
67
|
+
return false;
|
|
68
|
+
if (UNPLACED_OPTIONS[verb]?.(programArgs(s.segment).map((w) => w.spelled)) === true)
|
|
69
|
+
return false;
|
|
70
|
+
const beneath = WRITES_BENEATH[verb]?.(s.segment) === true;
|
|
71
|
+
for (const f of s.findings) {
|
|
72
|
+
if (f.read.candidate.face !== "write")
|
|
73
|
+
continue;
|
|
74
|
+
const target = f.read.target;
|
|
75
|
+
if (f.read.established === "unestablishable" || target === undefined || isPattern(f) || !withinAnyRoot(writeRoots, target))
|
|
76
|
+
return false;
|
|
77
|
+
if (beneath && denyRowAtOrBelow(f.read, boundary, !REMOVAL_COMMANDS.has(verb)))
|
|
78
|
+
return false;
|
|
79
|
+
if (REMOVAL_COMMANDS.has(verb) && writeRoots.some((root) => withinAnyRoot([target], root)))
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
return true;
|
|
83
|
+
};
|
|
84
|
+
return plans.every((p) => p.plan.unreadable === undefined) && runs.every(placed) && lineOrderPlaced(runs, concurrent);
|
|
85
|
+
}
|
|
86
|
+
function unplacedOptionsOf(judged) {
|
|
87
|
+
for (const s of judged.segments) {
|
|
88
|
+
const nested = s.nested.map(unplacedOptionsOf).find((u) => u !== undefined);
|
|
89
|
+
if (nested !== undefined)
|
|
90
|
+
return nested;
|
|
91
|
+
const verb = s.segment.verb?.basename;
|
|
92
|
+
if ((verb === "cp" || verb === "mv") && anyOptionWord(programArgs(s.segment).map((w) => w.spelled)))
|
|
93
|
+
return { program: verb };
|
|
94
|
+
}
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
async function writeGuardsOf(judged, boundary, placed, env, signal) {
|
|
98
|
+
const protection = boundary.writeProtectionJudge;
|
|
99
|
+
const guard = placed ? boundary.writeTargetGuard : undefined;
|
|
100
|
+
if (protection === undefined && guard === undefined)
|
|
101
|
+
return { unplaced: false, writeProtected: false };
|
|
102
|
+
const { runs, concurrent } = runOrderOf(judged);
|
|
103
|
+
const asKey = new Map();
|
|
104
|
+
const asSpelling = new Map();
|
|
105
|
+
const landingKeys = new Set();
|
|
106
|
+
const writtenBy = runs.map(() => []);
|
|
107
|
+
const add = (into, view, child, at) => {
|
|
108
|
+
if (!isAbsoluteForFamily(pathFamilyOf({ root: view }), view))
|
|
109
|
+
return;
|
|
110
|
+
const seen = into.get(view);
|
|
111
|
+
into.set(view, seen === undefined ? { child, at } : { child: seen.child && child, at: Math.max(seen.at, at) });
|
|
112
|
+
};
|
|
113
|
+
runs.forEach((s, at) => {
|
|
114
|
+
for (const f of s.findings) {
|
|
115
|
+
const written = f.read.written;
|
|
116
|
+
if (written === undefined || f.read.established === "unestablishable" || f.read.target === undefined)
|
|
117
|
+
continue;
|
|
118
|
+
const child = written.child === true;
|
|
119
|
+
const key = f.read.established === "canonical" || f.read.established === "expansion";
|
|
120
|
+
add(key ? asKey : asSpelling, f.read.target, child, at);
|
|
121
|
+
if (key && written.seat === "landing")
|
|
122
|
+
landingKeys.add(f.read.target);
|
|
123
|
+
if (written.seat === "landing")
|
|
124
|
+
for (const spelling of f.read.spellings)
|
|
125
|
+
add(asSpelling, spelling, child, at);
|
|
126
|
+
writtenBy[at].push(f.read.target, ...f.read.spellings);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
const probes = new Map();
|
|
130
|
+
const probe = (p) => {
|
|
131
|
+
let r = probes.get(p);
|
|
132
|
+
if (r === undefined) {
|
|
133
|
+
r = env.canonicalPath(p, signal).catch(() => undefined);
|
|
134
|
+
probes.set(p, r);
|
|
135
|
+
}
|
|
136
|
+
return r;
|
|
137
|
+
};
|
|
138
|
+
const parentOf = (view) => normalizeAbsPathLexically(`${view}/..`);
|
|
139
|
+
const writable = async (view, v) => {
|
|
140
|
+
const at = await probe(view);
|
|
141
|
+
if (at === undefined || at.ok)
|
|
142
|
+
return true;
|
|
143
|
+
if (at.error.code === "not_directory")
|
|
144
|
+
return false;
|
|
145
|
+
if (!v.child || at.error.code !== "not_found")
|
|
146
|
+
return true;
|
|
147
|
+
const dest = await probe(parentOf(view));
|
|
148
|
+
return dest === undefined || dest.ok || (dest.error.code !== "not_found" && dest.error.code !== "not_directory");
|
|
149
|
+
};
|
|
150
|
+
const changedBefore = (view, at) => {
|
|
151
|
+
const fold = (p) => p.normalize("NFC").toLowerCase();
|
|
152
|
+
const dir = fold(parentOf(view));
|
|
153
|
+
return writtenBy.some((paths, i) => i !== at && (i < at || concurrent) && paths.some((w) => withinAnyRoot([fold(w)], dir) || withinAnyRoot([dir], fold(w))));
|
|
154
|
+
};
|
|
155
|
+
const answer = async (ask) => {
|
|
156
|
+
try {
|
|
157
|
+
return (await ask()) === true;
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
const asked = [...[...asKey].map(([view, v]) => [view, v, true]), ...[...asSpelling].filter(([view]) => !landingKeys.has(view)).map(([view, v]) => [view, v, false])];
|
|
164
|
+
let unplaced = false;
|
|
165
|
+
let writeProtected = false;
|
|
166
|
+
for (const [view, v, key] of asked) {
|
|
167
|
+
if (!(await writable(view, v)) && !changedBefore(view, v.at))
|
|
168
|
+
continue;
|
|
169
|
+
const covered = protection === undefined ? false : await answer(() => protection(view, { ...(signal !== undefined ? { signal } : {}), ...(key ? { canonical: true } : {}) }));
|
|
170
|
+
const guarded = guard === undefined ? false : await answer(() => guard(view, signal));
|
|
171
|
+
if (covered === true)
|
|
172
|
+
writeProtected = true;
|
|
173
|
+
if (covered !== false || guarded !== false)
|
|
174
|
+
unplaced = true;
|
|
175
|
+
if (writeProtected)
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
return { unplaced, writeProtected };
|
|
179
|
+
}
|
|
15
180
|
const ADMISSION_MAX = 1024;
|
|
16
181
|
function shellCommandOf(args) {
|
|
17
182
|
const command = args?.command;
|
|
@@ -76,14 +241,17 @@ function refusalOf(tool, fold, roots, paths) {
|
|
|
76
241
|
const w = fold.witness;
|
|
77
242
|
if (fold.finding === "deny_hit" && w?.deny !== undefined) {
|
|
78
243
|
const target = w.read.target ?? w.deny.matchedView;
|
|
79
|
-
const
|
|
80
|
-
const
|
|
81
|
-
|
|
244
|
+
const write = w.deny.face === "write";
|
|
245
|
+
const core = readDenySentence({ target, pattern: w.deny.pattern, matchedView: w.deny.matchedView, ...(write ? { face: "write" } : {}) });
|
|
246
|
+
const sentence = `Error (${tool}): ${core} ${write ? WRITE_DENY_REFUSAL_NOTE : DENY_REFUSAL_NOTE}`;
|
|
247
|
+
return { sentence, ...(write ? {} : { details: violationDetails({ code: "read_path_denied", message: sentence, target, pattern: w.deny.pattern }) }) };
|
|
82
248
|
}
|
|
83
249
|
const account = findingAccount(fold, roots);
|
|
84
250
|
const sentence = `Error (${tool}): ${account}. ${BASH_READONLY_CONFINEMENT_NOTE}`;
|
|
85
|
-
return { sentence, details:
|
|
251
|
+
return { sentence, details: findingDetails(fold.finding, paths) };
|
|
86
252
|
}
|
|
253
|
+
const WRITE_DENY_REFUSAL_NOTE = "The boundary judges this command's write targets — its output redirections and the operands of mkdir, touch, rm, " +
|
|
254
|
+
"rmdir, mv, cp, tee and an in-place sed — against the write deny list; a denied write is refused here rather than escalated.";
|
|
87
255
|
const DENY_REFUSAL_NOTE = "The read boundary judges this command's written spelling and, where a filesystem can be consulted, the " +
|
|
88
256
|
"paths it would actually read; a deny-listed read is refused here rather than escalated.";
|
|
89
257
|
function verdictOf(tool, seat, fold, judged, boundary) {
|
|
@@ -91,17 +259,19 @@ function verdictOf(tool, seat, fold, judged, boundary) {
|
|
|
91
259
|
if (disposition === "vouch")
|
|
92
260
|
return { disposition, finding: fold.finding };
|
|
93
261
|
const paths = outsidePathsOf(judged);
|
|
262
|
+
const unestablished = fold.unestablished === true ? { unestablished: true } : {};
|
|
94
263
|
if (disposition === "refuse") {
|
|
95
264
|
const r = refusalOf(tool, fold, boundary.roots, paths);
|
|
96
|
-
return { disposition, finding: fold.finding, sentence: r.sentence, details: r.details };
|
|
265
|
+
return { disposition, finding: fold.finding, sentence: r.sentence, ...(r.details !== undefined ? { details: r.details } : {}) };
|
|
97
266
|
}
|
|
98
267
|
const grant = fold.finding === "outside_roots" ? readRootGrantCandidateOf(judged, boundary) : undefined;
|
|
99
268
|
const containment = fold.finding === "outside_roots" || fold.finding === "unestablishable";
|
|
100
269
|
return {
|
|
101
270
|
disposition,
|
|
102
271
|
finding: fold.finding,
|
|
272
|
+
...unestablished,
|
|
103
273
|
sentence: findingAccount(fold, boundary.roots),
|
|
104
|
-
...(disposition === "mandated" && containment ? { details:
|
|
274
|
+
...(disposition === "mandated" && containment ? { details: findingDetails(fold.finding, paths) } : {}),
|
|
105
275
|
...(grant !== undefined ? { readRootCandidate: { dir: grant.dir, ...(grant.covers !== undefined ? { covers: grant.covers } : {}), clearsThisAsk: true } } : {}),
|
|
106
276
|
};
|
|
107
277
|
}
|
|
@@ -137,17 +307,22 @@ async function applyAlsoInside(judged, boundary) {
|
|
|
137
307
|
});
|
|
138
308
|
return rewrite(judged);
|
|
139
309
|
}
|
|
140
|
-
async function runShellReadStation(tool, command, declared, seat, readers, establisher, signal) {
|
|
310
|
+
async function runShellReadStation(tool, command, declared, seat, readers, establisher, env, signal) {
|
|
141
311
|
const plan = planShellReads(command, declarationsOf(declared, readers));
|
|
142
312
|
const established = await establisher.establish(plan, declared, signal);
|
|
143
313
|
const boundary = { ...declared, roots: established.roots };
|
|
144
314
|
const judged = await applyAlsoInside(judgePlan(plan, established, boundary), boundary);
|
|
145
|
-
const
|
|
315
|
+
const judgedVerdict = verdictOf(tool, seat, foldShellReadFindings(judged), judged, boundary);
|
|
316
|
+
const unplaced = unplacedOptionsOf(judged);
|
|
317
|
+
const placed = writesPlacedOf(judged, boundary);
|
|
318
|
+
const guarded = judgedVerdict.disposition === "refuse" ? { unplaced: false, writeProtected: false } : await writeGuardsOf(judged, boundary, placed, env, signal);
|
|
319
|
+
const facts = guarded.writeProtected ? { ...judgedVerdict, writeProtected: true } : judgedVerdict;
|
|
320
|
+
const verdict = placed && !guarded.unplaced ? { ...facts, writesPlaced: true } : unplaced !== undefined ? { ...facts, unplacedOptions: unplaced } : facts;
|
|
146
321
|
return { plan, established, judged, boundary, verdict };
|
|
147
322
|
}
|
|
148
323
|
export async function judgeShellRead(input) {
|
|
149
324
|
const readers = input.readers ?? new Set(bashClassifyDefaultAllow());
|
|
150
|
-
const run = await runShellReadStation(input.tool ?? "Bash", input.command, input.boundary, input.seat, readers, createShellReadEstablisher(input.env, input.boundary.roots), input.signal);
|
|
325
|
+
const run = await runShellReadStation(input.tool ?? "Bash", input.command, input.boundary, input.seat, readers, createShellReadEstablisher(input.env, input.boundary.roots), input.env, input.signal);
|
|
151
326
|
const offer = normalizeReadRootCandidate(run.verdict.readRootCandidate);
|
|
152
327
|
return {
|
|
153
328
|
plan: run.plan,
|
|
@@ -196,7 +371,7 @@ export function createShellReadStation(opts) {
|
|
|
196
371
|
return null;
|
|
197
372
|
const seat = callOpts.seat ?? opts.seatOf(toolName);
|
|
198
373
|
return (async () => {
|
|
199
|
-
const run = await runShellReadStation(toolName, command, boundary, seat, readers, establisherFor(boundary.roots), signal);
|
|
374
|
+
const run = await runShellReadStation(toolName, command, boundary, seat, readers, establisherFor(boundary.roots), opts.env, signal);
|
|
200
375
|
if (run.verdict.disposition !== "refuse" && callOpts.toolCallId !== undefined)
|
|
201
376
|
remember(callOpts.toolCallId, { toolName, boundary: run.boundary, established: run.established, command });
|
|
202
377
|
return run.verdict;
|
|
@@ -207,7 +382,7 @@ export function createShellReadStation(opts) {
|
|
|
207
382
|
const fold = { finding: "unestablishable" };
|
|
208
383
|
const disposition = decideShellRead("unestablishable", seat);
|
|
209
384
|
const account = `${cause}, so where this command reads could not be established ${NOT_AUTO_ALLOWED}`;
|
|
210
|
-
const details =
|
|
385
|
+
const details = unestablishedDetails();
|
|
211
386
|
if (disposition === "refuse")
|
|
212
387
|
return { disposition, finding: "unestablishable", sentence: `Error (${toolName}): ${account}. ${BASH_READONLY_CONFINEMENT_NOTE}`, details };
|
|
213
388
|
return { disposition, finding: fold.finding, sentence: account, ...(disposition === "mandated" ? { details } : {}) };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ESTABLISHMENT — where a shell command's read candidates LAND once the filesystem has answered, as one
|
|
3
3
|
* value the gate's shell read station holds per execution environment. The plan (`shell-read-plan.ts`)
|
|
4
|
-
* decides WHICH words a command may read
|
|
4
|
+
* decides WHICH words a command may read, `shell-read-write-targets.ts` which words it writes and at which seat;
|
|
5
|
+
* this module decides WHERE each of them lands: the canonical
|
|
5
6
|
* target of a spelling (symlinks resolved, a `..` applied the way the kernel applies it), the expansion of
|
|
6
7
|
* a pattern (shell pathname semantics over `env.listDir`), the members of a traversal root (a bounded
|
|
7
8
|
* recursive enumeration, dot files included, symlinked directories not followed), and the fail-closed arm
|
|
@@ -11,7 +12,10 @@
|
|
|
11
12
|
* Properties every consumer relies on:
|
|
12
13
|
* 1. A NON-EXISTENT target is not a refusal — `cat nope.txt` keeps the shell's own "No such file". The
|
|
13
14
|
* deepest EXISTING ancestor is canonicalized and the missing tail rejoined, so a symlinked PARENT still
|
|
14
|
-
* cannot carry a target out.
|
|
15
|
+
* cannot carry a target out. For a WRITE target a component the resolver cannot find but the filesystem
|
|
16
|
+
* still holds (a link whose target does not exist) is not absent: a write through it creates the link's
|
|
17
|
+
* target, so the target is established where the link lands (read one hop at a time, bounded) — or not at
|
|
18
|
+
* all when the environment cannot read the link.
|
|
15
19
|
* 2. An unanswerable filesystem manufactures no refusal on its own: a chain no ancestor answered degrades
|
|
16
20
|
* to the lexical spelling on an environment that declares no authority for its resolver, and is
|
|
17
21
|
* `unestablishable` on one that does (`canonicalPathIsAuthoritative`).
|
|
@@ -35,7 +39,7 @@
|
|
|
35
39
|
*/
|
|
36
40
|
import type { ExecutionEnv } from "../../internal/harness-types.js";
|
|
37
41
|
import type { ShellReadPlan, ShellReadSegment } from "./shell-read-plan.js";
|
|
38
|
-
import {
|
|
42
|
+
import type { EstablishedRead, ShellReadBoundary } from "./shell-read-judge.js";
|
|
39
43
|
/**
|
|
40
44
|
* How many directories one glob operand (or one traversal root) may enumerate, and how many names it may
|
|
41
45
|
* match, before the leg gives up and answers `unestablishable`.
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { normalizeAbsPathLexically, pathFamilyOf } from "./safety.js";
|
|
1
|
+
import { isAbsoluteForFamily, normalizeAbsPathLexically, pathFamilyOf } from "./safety.js";
|
|
2
2
|
import { boundaryIsTheSandbox, canonicalPathIsAuthoritative } from "../../core/remote-env.js";
|
|
3
3
|
import { resolveOperandSpellings } from "./bash-readonly-classifier.js";
|
|
4
|
-
import {
|
|
5
|
-
import { movedReads } from "./shell-read-identity.js";
|
|
4
|
+
import { judgedCandidatesOf, sedBackupNames } from "./shell-read-write-targets.js";
|
|
5
|
+
import { movedReads, withHomeViews } from "./shell-read-identity.js";
|
|
6
6
|
const GLOB_METACHAR_RE = /[*?[]/;
|
|
7
7
|
export const MAX_GLOB_DIRS = 256;
|
|
8
8
|
export const MAX_GLOB_MATCHES = 8192;
|
|
9
|
+
const WRITE_LINK_HOPS = 32;
|
|
9
10
|
function compileGlobSegment(pattern) {
|
|
10
11
|
if (pattern.includes("\\"))
|
|
11
12
|
return undefined;
|
|
@@ -131,15 +132,25 @@ function lexicalReadsOf(candidate, boundary) {
|
|
|
131
132
|
}
|
|
132
133
|
return out;
|
|
133
134
|
}
|
|
134
|
-
function
|
|
135
|
-
const
|
|
136
|
-
return [
|
|
135
|
+
function unfollowedBackupReads(j, boundary, cause) {
|
|
136
|
+
const spellings = sedBackupNames(j.followed.operand.spelled, j.followed.suffix).flatMap((name) => lexicalReadsOf({ ...j.candidate, spelled: name }, boundary).flatMap((r) => [...(r.target !== undefined ? [r.target] : []), ...r.spellings]));
|
|
137
|
+
return [{ candidate: j.candidate, established: "unestablishable", spellings: [...new Set(spellings)], cause }];
|
|
138
|
+
}
|
|
139
|
+
function writtenAt(read, j, seat) {
|
|
140
|
+
if (read.candidate.face !== "write" || !(j.written ?? j.seats ?? ["landing"]).includes(seat))
|
|
141
|
+
return read;
|
|
142
|
+
return { ...read, written: { seat, ...(j.child === true ? { child: true } : {}) } };
|
|
137
143
|
}
|
|
138
144
|
export function establishShellReadsLexically(plan, boundary) {
|
|
139
145
|
const reads = new Map();
|
|
140
146
|
const nested = new Map();
|
|
141
147
|
for (const segment of plan.segments) {
|
|
142
|
-
reads.set(segment, judgedCandidatesOf(segment).flatMap((
|
|
148
|
+
reads.set(segment, judgedCandidatesOf(segment).flatMap((j) => {
|
|
149
|
+
if (j.followed !== undefined)
|
|
150
|
+
return unfollowedBackupReads({ ...j, followed: j.followed }, boundary, `"${j.followed.operand.raw}" names its backup through a link chain only the filesystem can follow`);
|
|
151
|
+
const seat = (j.written ?? j.seats ?? ["landing"]).includes("landing") ? "landing" : "entry";
|
|
152
|
+
return lexicalReadsOf(j.candidate, boundary).map((r) => writtenAt(r, j, seat));
|
|
153
|
+
}));
|
|
143
154
|
for (const n of segment.nested)
|
|
144
155
|
nested.set(n, establishShellReadsLexically(n, boundary));
|
|
145
156
|
}
|
|
@@ -164,12 +175,23 @@ export function createShellReadEstablisher(env, readRoots, opts) {
|
|
|
164
175
|
})();
|
|
165
176
|
return await rootsOnce;
|
|
166
177
|
};
|
|
167
|
-
const resolveWithoutParentSteps = async (p, signal) => {
|
|
178
|
+
const resolveWithoutParentSteps = async (p, signal, writeTarget = false, linkHops = WRITE_LINK_HOPS) => {
|
|
168
179
|
const parts = p.split(SEP);
|
|
169
180
|
const tail = [];
|
|
170
181
|
let allAbsent = true;
|
|
171
182
|
while (parts.length > 0) {
|
|
172
|
-
const
|
|
183
|
+
const at = parts.join("/") || "/";
|
|
184
|
+
const canon = await env.canonicalPath(at, signal);
|
|
185
|
+
if (!canon.ok && canon.error.code === "not_found" && writeTarget && allAbsent) {
|
|
186
|
+
const entry = await env.fileInfo(at, signal);
|
|
187
|
+
const link = entry.ok && entry.value.kind === "symlink" && linkHops > 0 ? await env.readLink?.(at, signal) : undefined;
|
|
188
|
+
if (link?.ok === true) {
|
|
189
|
+
const landing = isAbsoluteForFamily(family, link.value) ? link.value : joinUnder(parts.slice(0, -1).join("/") || "/", link.value);
|
|
190
|
+
return await resolve([landing, ...[...tail].reverse()].join("/"), signal, true, linkHops - 1);
|
|
191
|
+
}
|
|
192
|
+
if (entry.ok || entry.error.code !== "not_found")
|
|
193
|
+
allAbsent = false;
|
|
194
|
+
}
|
|
173
195
|
if (canon.ok) {
|
|
174
196
|
if (tail.length === 0)
|
|
175
197
|
return { value: canon.value, exact: true };
|
|
@@ -182,20 +204,20 @@ export function createShellReadEstablisher(env, readRoots, opts) {
|
|
|
182
204
|
return undefined;
|
|
183
205
|
};
|
|
184
206
|
const parentOf = (canonical) => normalizeAbsPathLexically(`${canonical}/..`);
|
|
185
|
-
const resolve = async (p, signal) => {
|
|
207
|
+
const resolve = async (p, signal, writeTarget = false, linkHops = WRITE_LINK_HOPS) => {
|
|
186
208
|
const parts = p.split(SEP);
|
|
187
209
|
const first = parts.findIndex((seg) => seg === "..");
|
|
188
210
|
if (first < 0)
|
|
189
|
-
return await resolveWithoutParentSteps(p, signal);
|
|
211
|
+
return await resolveWithoutParentSteps(p, signal, writeTarget, linkHops);
|
|
190
212
|
const headEnd = parts.slice(0, first).reduce((n, seg) => n + seg.length + 1, 0);
|
|
191
|
-
let head = await resolveWithoutParentSteps(p.slice(0, headEnd) || "/", signal);
|
|
213
|
+
let head = await resolveWithoutParentSteps(p.slice(0, headEnd) || "/", signal, writeTarget, linkHops);
|
|
192
214
|
if (head === undefined)
|
|
193
215
|
return undefined;
|
|
194
216
|
let pending = [];
|
|
195
217
|
const flush = async () => {
|
|
196
218
|
if (pending.length === 0)
|
|
197
219
|
return true;
|
|
198
|
-
const next = await resolveWithoutParentSteps([head.value.replace(TRAILING_SEP, ""), ...pending].join("/"), signal);
|
|
220
|
+
const next = await resolveWithoutParentSteps([head.value.replace(TRAILING_SEP, ""), ...pending].join("/"), signal, writeTarget, linkHops);
|
|
199
221
|
pending = [];
|
|
200
222
|
if (next === undefined)
|
|
201
223
|
return false;
|
|
@@ -342,8 +364,18 @@ export function createShellReadEstablisher(env, readRoots, opts) {
|
|
|
342
364
|
const members = found.map(({ path, canonical, symlink }) => ({ path, ...(canonical !== undefined ? { canonical } : {}), node: (symlink && canonical !== undefined && listedDirs.has(canonical) ? "directory" : "file") }));
|
|
343
365
|
return { ...(root !== undefined ? { root } : {}), members };
|
|
344
366
|
};
|
|
345
|
-
const
|
|
346
|
-
const
|
|
367
|
+
const entryOf = async (spelling, signal) => {
|
|
368
|
+
const parts = spelling.split(SEP);
|
|
369
|
+
const leaf = parts.pop() ?? "";
|
|
370
|
+
if (leaf === "" || leaf === "." || leaf === ".." || parts.length === 0)
|
|
371
|
+
return await resolve(spelling, signal, true);
|
|
372
|
+
const parent = await resolve(parts.join("/") || "/", signal, true);
|
|
373
|
+
if (parent === undefined)
|
|
374
|
+
return undefined;
|
|
375
|
+
return { value: joinUnder(parent.value, leaf), exact: parent.exact, ...(parent.missing !== undefined ? { missing: parent.missing } : {}) };
|
|
376
|
+
};
|
|
377
|
+
const canonicalReadOf = async (candidate, spelling, spellings, established, origin, canonicalKnown, signal, seat = "landing") => {
|
|
378
|
+
const real = canonicalKnown !== undefined ? { value: canonicalKnown, exact: true } : seat === "entry" ? await entryOf(spelling, signal) : await resolve(spelling, signal, candidate.face === "write");
|
|
347
379
|
if (real === undefined) {
|
|
348
380
|
if (resolverIsAuthoritative)
|
|
349
381
|
return { candidate, established: "unestablishable", spellings, ...(origin !== undefined ? { origin } : {}), cause: `"${spelling}" cannot be resolved to verify where it reads` };
|
|
@@ -354,10 +386,88 @@ export function createShellReadEstablisher(env, readRoots, opts) {
|
|
|
354
386
|
}
|
|
355
387
|
return { candidate, established, target: real.value, spellings, ...(origin !== undefined ? { origin } : {}) };
|
|
356
388
|
};
|
|
389
|
+
const followedName = async (spelled, base, homeDir, signal) => {
|
|
390
|
+
let name = spelled;
|
|
391
|
+
for (let hop = 0; hop <= WRITE_LINK_HOPS; hop++) {
|
|
392
|
+
const at = resolveOperandSpellings(base === "" ? undefined : base, name, homeDir);
|
|
393
|
+
if (at === undefined)
|
|
394
|
+
return undefined;
|
|
395
|
+
const walked = at.spelled ?? at.folded;
|
|
396
|
+
if (TRAILING_SEP.test(walked))
|
|
397
|
+
return name;
|
|
398
|
+
const parts = walked.split(SEP);
|
|
399
|
+
const leaf = parts.pop() ?? "";
|
|
400
|
+
if (leaf === "" || leaf === "." || leaf === "..")
|
|
401
|
+
return name;
|
|
402
|
+
const parent = await resolve(parts.join("/") || "/", signal);
|
|
403
|
+
if (parent === undefined)
|
|
404
|
+
return undefined;
|
|
405
|
+
if (!parent.exact)
|
|
406
|
+
return parent.missing === true ? name : undefined;
|
|
407
|
+
const entry = await env.fileInfo(joinUnder(parent.value, leaf), signal);
|
|
408
|
+
if (!entry.ok)
|
|
409
|
+
return entry.error.code === "not_found" || entry.error.code === "not_directory" ? name : undefined;
|
|
410
|
+
if (entry.value.kind !== "symlink")
|
|
411
|
+
return name;
|
|
412
|
+
const link = await env.readLink?.(joinUnder(parent.value, leaf), signal);
|
|
413
|
+
if (link?.ok !== true)
|
|
414
|
+
return undefined;
|
|
415
|
+
const dir = name.slice(0, name.search(/[^/]*$/));
|
|
416
|
+
name = isAbsoluteForFamily(family, link.value) ? link.value : `${dir}${link.value}`;
|
|
417
|
+
}
|
|
418
|
+
return undefined;
|
|
419
|
+
};
|
|
420
|
+
const followedBackupReads = async (j, boundary, signal) => {
|
|
421
|
+
const { operand, suffix } = j.followed;
|
|
422
|
+
const bases = basesFor(operand, boundary);
|
|
423
|
+
const unfollowed = (cause) => unfollowedBackupReads(j, boundary, cause);
|
|
424
|
+
if (bases === "unmodelled")
|
|
425
|
+
return unfollowed(`"${operand.raw}" is relative to a working directory this command's text does not settle`);
|
|
426
|
+
const out = [];
|
|
427
|
+
for (const base of bases) {
|
|
428
|
+
const name = await followedName(operand.spelled, base, boundary.homeDir, signal);
|
|
429
|
+
if (name === undefined) {
|
|
430
|
+
out.push(...unfollowed(`"${operand.raw}" names its backup through a link chain that could not be read`));
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
for (const backup of sedBackupNames(name, suffix)) {
|
|
434
|
+
const at = resolveOperandSpellings(base === "" ? undefined : base, backup, boundary.homeDir);
|
|
435
|
+
if (at === undefined) {
|
|
436
|
+
out.push(...unfollowed(`"${backup}" cannot be placed against "${base}"`));
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
const spellings = at.spelled !== undefined ? [at.folded, at.spelled] : [at.folded];
|
|
440
|
+
for (const seat of j.seats ?? ["landing"])
|
|
441
|
+
out.push(writtenAt(await canonicalReadOf(j.candidate, at.spelled ?? at.folded, spellings, "canonical", undefined, undefined, signal, seat), j, seat));
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return out;
|
|
445
|
+
};
|
|
357
446
|
const establishSegment = async (segment, boundary, roots, signal) => {
|
|
358
447
|
const out = [];
|
|
359
|
-
for (const
|
|
448
|
+
for (const judged of judgedCandidatesOf(segment)) {
|
|
449
|
+
if (judged.followed !== undefined) {
|
|
450
|
+
out.push(...(await followedBackupReads({ ...judged, followed: judged.followed }, boundary, signal)));
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
const candidate = judged.candidate;
|
|
360
454
|
const traversal = candidate.provenance === "traversal_root";
|
|
455
|
+
const resolvesItself = TRAILING_SEP.test(candidate.spelled) || [".", ".."].includes(candidate.spelled.split(SEP).pop() ?? "");
|
|
456
|
+
const landsToo = (ss) => (resolvesItself && ss.length > 0 && !ss.includes("landing") ? [...ss, "landing"] : ss);
|
|
457
|
+
const seats = judged.seats === undefined ? ["landing"] : landsToo(judged.seats);
|
|
458
|
+
const writtenSeats = { ...judged, written: judged.written === undefined ? seats : landsToo(judged.written) };
|
|
459
|
+
const atSeats = async (spelling, spellings, established, origin, canonicalKnown) => {
|
|
460
|
+
const reads = [];
|
|
461
|
+
for (const seat of seats) {
|
|
462
|
+
const read = writtenAt(await canonicalReadOf(candidate, spelling, spellings, established, origin, canonicalKnown, signal, seat), writtenSeats, seat);
|
|
463
|
+
const same = reads.findIndex((r) => r.established === read.established && r.target === read.target);
|
|
464
|
+
if (same < 0)
|
|
465
|
+
reads.push(read);
|
|
466
|
+
else if (reads[same].written === undefined && read.written !== undefined)
|
|
467
|
+
reads[same] = read;
|
|
468
|
+
}
|
|
469
|
+
return reads;
|
|
470
|
+
};
|
|
361
471
|
const land = async (read, spellings, folded) => {
|
|
362
472
|
if (!traversal || (read.established !== "canonical" && read.established !== "expansion") || read.target === undefined) {
|
|
363
473
|
out.push(read);
|
|
@@ -371,7 +481,7 @@ export function createShellReadEstablisher(env, readRoots, opts) {
|
|
|
371
481
|
}
|
|
372
482
|
out.push({ ...read, ...(walkedTree.root !== undefined ? { node: walkedTree.root } : {}) });
|
|
373
483
|
for (const m of walkedTree.members)
|
|
374
|
-
out.push({ ...(await canonicalReadOf(candidate, m.path, [m.path], "expansion", folded, m.canonical, signal)), node: m.node });
|
|
484
|
+
out.push({ ...(await canonicalReadOf(candidate, m.path, [m.path], "expansion", folded, m.canonical, signal)), node: m.node, walked: true });
|
|
375
485
|
};
|
|
376
486
|
for (const lexical of lexicalReadsOf(candidate, boundary)) {
|
|
377
487
|
if (lexical.established !== "lexical") {
|
|
@@ -393,14 +503,17 @@ export function createShellReadEstablisher(env, readRoots, opts) {
|
|
|
393
503
|
if (expanded !== undefined && expanded.paths.length > 0) {
|
|
394
504
|
matched = true;
|
|
395
505
|
for (const t of expanded.paths)
|
|
396
|
-
|
|
506
|
+
for (const read of await atSeats(t, [t, folded], "expansion", folded, expanded.canonical.get(t)))
|
|
507
|
+
await land(read, spellings, folded);
|
|
397
508
|
}
|
|
398
509
|
}
|
|
399
510
|
if (!matched)
|
|
400
|
-
|
|
511
|
+
for (const read of await atSeats(walked, spellings, "canonical", undefined, undefined))
|
|
512
|
+
await land(read, spellings, folded);
|
|
401
513
|
continue;
|
|
402
514
|
}
|
|
403
|
-
|
|
515
|
+
for (const read of await atSeats(walked, spellings, "canonical", undefined, undefined))
|
|
516
|
+
await land(read, spellings, folded);
|
|
404
517
|
}
|
|
405
518
|
}
|
|
406
519
|
return out;
|
|
@@ -421,7 +534,7 @@ export function createShellReadEstablisher(env, readRoots, opts) {
|
|
|
421
534
|
if (lexicalOnly)
|
|
422
535
|
return establishShellReadsLexically(plan, boundary);
|
|
423
536
|
const roots = await canonicalRoots(signal);
|
|
424
|
-
return await establishPlan(plan, { ...boundary, roots }, roots, signal);
|
|
537
|
+
return await withHomeViews(env, await establishPlan(plan, { ...boundary, roots }, roots, signal), signal);
|
|
425
538
|
},
|
|
426
539
|
reestablish: async (previous, boundary, signal) => {
|
|
427
540
|
if (lexicalOnly)
|
|
@@ -9,5 +9,12 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import type { EstablishedPlan } from "./shell-read-establish.js";
|
|
11
11
|
import type { EstablishedRead } from "./shell-read-judge.js";
|
|
12
|
+
import { type HomeViewEnv } from "./read-deny-home.js";
|
|
12
13
|
/** Every read of either establishment whose candidate's settled target set differs between the two. */
|
|
13
14
|
export declare function movedReads(previous: EstablishedPlan, again: EstablishedPlan): EstablishedRead[];
|
|
15
|
+
/**
|
|
16
|
+
* The establishment with every read that landed carrying its HOME VIEW (read-deny-home.ts) among its spellings, nested
|
|
17
|
+
* plans included: the deny judge then places a home-anchored row wherever the target IS below the home, whatever it is
|
|
18
|
+
* spelled through. A read that did not land (lexical, unestablishable) is left as it is.
|
|
19
|
+
*/
|
|
20
|
+
export declare function withHomeViews(env: HomeViewEnv, established: EstablishedPlan, signal?: AbortSignal): Promise<EstablishedPlan>;
|