@sema-agent/core 7.24.4 → 7.25.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 +100 -0
- package/dist/agents/peer-session-drain.d.ts +16 -1
- package/dist/agents/peer-session-drain.js +16 -1
- package/dist/agents/send-message-tool.d.ts +10 -2
- package/dist/agents/send-message-tool.js +18 -7
- package/dist/agents/subagent.js +13 -10
- package/dist/core/arg-summary.d.ts +0 -7
- package/dist/core/arg-summary.js +2 -1
- package/dist/core/background-agent-store.d.ts +3 -3
- package/dist/core/checkpoint-store.d.ts +1 -0
- package/dist/core/delegation-frames.d.ts +5 -0
- package/dist/core/gate-fold.js +85 -0
- package/dist/core/gate-lanes.js +23 -1
- package/dist/core/gate-outcome.d.ts +8 -2
- package/dist/core/gate-outcome.js +3 -1
- package/dist/core/hooks.d.ts +29 -0
- package/dist/core/hooks.js +2 -0
- package/dist/core/mailbox-store.d.ts +5 -0
- package/dist/core/mailbox-store.js +1 -1
- package/dist/core/parent-spec-seat.d.ts +7 -1
- package/dist/core/parent-spec-seat.js +2 -0
- package/dist/core/permission-rule-consent.js +29 -47
- package/dist/core/permission-rule-provider.js +4 -3
- package/dist/core/permission-rule-store.d.ts +143 -7
- package/dist/core/permission-rule-store.js +165 -15
- package/dist/core/runner/contracts.d.ts +36 -2
- package/dist/core/runner/gate-exit.d.ts +14 -0
- package/dist/core/runner/gate-exit.js +4 -0
- package/dist/core/runner/prepare-delegation-surface.d.ts +4 -4
- package/dist/core/runner/prepare-delegation-surface.js +2 -1
- package/dist/core/runner/prepare-gate-stations.d.ts +4 -0
- package/dist/core/runner/prepare-gate-stations.js +7 -4
- package/dist/core/runner/prepare-hands-readface.d.ts +8 -4
- package/dist/core/runner/prepare-hands-readface.js +25 -10
- package/dist/core/runner/prepare-inherited-gate.d.ts +23 -0
- package/dist/core/runner/prepare-inherited-gate.js +11 -6
- package/dist/core/runner/prepare-policy-chain.d.ts +15 -1
- package/dist/core/runner/prepare-policy-chain.js +14 -4
- package/dist/core/runner/prepare-suspend-saga.d.ts +5 -2
- package/dist/core/runner/prepare-suspend-saga.js +4 -3
- package/dist/core/runner/prepare-task.js +10 -9
- package/dist/core/runner/prepare-wiring-manifest.d.ts +11 -4
- package/dist/core/runner/prepare-wiring-manifest.js +8 -4
- package/dist/core/runner/resume-apply.js +2 -0
- package/dist/core/runner/run-harness-handlers.js +1 -0
- package/dist/core/runner/run-identity-wiring.d.ts +2 -2
- package/dist/core/runner/run-leg.js +26 -0
- package/dist/core/runner/run-notification-lane.d.ts +2 -2
- package/dist/core/runner/run-notification-lane.js +2 -1
- package/dist/core/runner/run-settle-and-teardown.js +1 -0
- package/dist/core/runner/session-rule-policy.d.ts +3 -4
- package/dist/core/runner/session-rule-policy.js +11 -3
- package/dist/core/session-policy-store.d.ts +27 -3
- package/dist/core/session-policy-store.js +12 -4
- package/dist/core/store-contracts/background-agent-store-contract.js +10 -0
- package/dist/core/store-contracts/peer-directory-contract.d.ts +23 -0
- package/dist/core/store-contracts/peer-directory-contract.js +82 -0
- package/dist/core/task-event.d.ts +6 -0
- package/dist/core/task-notification.d.ts +79 -4
- package/dist/core/task-notification.js +90 -1
- package/dist/core/task-registry-agent.js +1 -2
- package/dist/core/task-registry-monitor.js +2 -1
- package/dist/core/task-registry-shared.d.ts +53 -5
- package/dist/core/task-registry-shared.js +13 -2
- package/dist/core/task-registry.js +8 -5
- package/dist/core/task-spec.d.ts +12 -5
- package/dist/core/tool-catalog-entries.js +1 -0
- package/dist/core/tool-policy.js +30 -23
- package/dist/core/tool-spec.d.ts +1 -1
- package/dist/core/wiring-manifest.d.ts +2 -2
- package/dist/engine/loop/agent-loop.js +1 -1
- package/dist/engine/loop/types.d.ts +4 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +4 -3
- package/dist/prompts/coordinator.d.ts +2 -1
- package/dist/stores/file/adoption/adopt.js +23 -1
- package/dist/stores/file/permission-rule-store.js +3 -1
- package/dist/stores/file/session-policy-store.d.ts +31 -0
- package/dist/stores/file/session-policy-store.js +87 -7
- package/dist/tools/fs/bash-lexer.d.ts +13 -0
- package/dist/tools/fs/bash-lexer.js +61 -0
- package/dist/tools/fs/bash-program-position.d.ts +36 -29
- package/dist/tools/fs/bash-program-position.js +33 -13
- package/dist/tools/fs/bash-readonly-classifier.d.ts +26 -313
- package/dist/tools/fs/bash-readonly-classifier.js +81 -782
- package/dist/tools/fs/fs-bash.d.ts +43 -207
- package/dist/tools/fs/fs-bash.js +20 -341
- package/dist/tools/fs/index.d.ts +12 -13
- package/dist/tools/fs/index.js +3 -4
- package/dist/tools/fs/read-deny.d.ts +25 -0
- package/dist/tools/fs/read-deny.js +19 -0
- package/dist/tools/fs/safety.js +2 -6
- package/dist/tools/fs/shell-read-check.d.ts +99 -0
- package/dist/tools/fs/shell-read-check.js +229 -0
- package/dist/tools/fs/shell-read-establish.d.ts +70 -115
- package/dist/tools/fs/shell-read-establish.js +216 -102
- package/dist/tools/fs/shell-read-judge.d.ts +231 -0
- package/dist/tools/fs/shell-read-judge.js +170 -0
- package/dist/tools/fs/shell-read-plan.d.ts +3 -0
- package/dist/tools/fs/shell-read-plan.js +88 -27
- package/dist/tools/monitor.d.ts +5 -5
- package/dist/tools/monitor.js +3 -5
- package/dist/tools/task-list.d.ts +4 -0
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +19 -3
- package/wire-changes/core@7.25.0.json +671 -0
- package/wire-changes/core@7.25.1.json +64 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shell read JUDGE and its DECISION TABLE — the one place a shell command's read candidates become a
|
|
3
|
+
* verdict, for every shell face (the full `Bash`, the read-only `Bash`, `Monitor`) and both moments (the
|
|
4
|
+
* gate station before approval, the rewrite re-judge after it).
|
|
5
|
+
*
|
|
6
|
+
* · `judgeRead` judges ONE established candidate: the deny half first, over every view the candidate has
|
|
7
|
+
* (its canonical target, its folded spelling, the kernel spelling of a `..` operand, the folded spelling
|
|
8
|
+
* of a candidate that could not be established), then containment over the canonical target alone. The
|
|
9
|
+
* deny judge is the boundary's own `denyMatch` closure (`readDenySeat`, read-deny.ts) — the same function
|
|
10
|
+
* object every read face mounts; nothing here compiles a pattern or re-derives a view set. The containment
|
|
11
|
+
* judge is `withinAnyRoot` over the roots the boundary declares.
|
|
12
|
+
* · `foldShellReadFindings` folds a command's findings — every segment, every nested plan — to ONE word by
|
|
13
|
+
* the priority `identity_changed > deny_hit > outside_roots > unestablishable > declined > none`.
|
|
14
|
+
* · `SHELL_READ_DECISION` is the total table (finding × seat → disposition) that is the ONLY source of
|
|
15
|
+
* "refuse / mandated ask / ordinary ask / vouch" for a shell read. A seat is a TRUST POLICY word, not a
|
|
16
|
+
* tool name: the four seats differ on one row (`declined`) and nowhere else.
|
|
17
|
+
*
|
|
18
|
+
* The floor this module is (`shell.read_boundary.friction_floor`) is stated on {@link judgeRead}.
|
|
19
|
+
*/
|
|
20
|
+
import type { BashReadonlyRootBoundary } from "./bash-readonly-classifier.js";
|
|
21
|
+
import type { ReadCandidate, ShellReadPlan, ShellReadSegment } from "./shell-read-plan.js";
|
|
22
|
+
import type { AssertAllKeysHandled } from "../../core/ask-origin.js";
|
|
23
|
+
/** WHAT the judge found about a command — a closed set, registered in `docs/CLOSED-SETS.md`. */
|
|
24
|
+
export declare const SHELL_READ_FINDINGS: readonly ["deny_hit", "outside_roots", "unestablishable", "declined", "identity_changed", "none"];
|
|
25
|
+
export type ShellReadFinding = (typeof SHELL_READ_FINDINGS)[number];
|
|
26
|
+
/** WHO is asking — the trust-policy seats the decision table is keyed on. `bash_off` covers the `off` and
|
|
27
|
+
* `always` doctrines (and a bypass mode); `bash_classify` the `classify` doctrine; `readonly` the read-only
|
|
28
|
+
* shell face (`handsReadOnly`); `rejudge` the post-approval re-judgment of rewritten bytes and the
|
|
29
|
+
* pre-spawn identity re-check. */
|
|
30
|
+
export declare const SHELL_READ_SEATS: readonly ["bash_off", "bash_classify", "readonly", "rejudge"];
|
|
31
|
+
export type ShellReadSeat = (typeof SHELL_READ_SEATS)[number];
|
|
32
|
+
/** The fold priority: a command's finding is the highest-ranked finding of any candidate in it. */
|
|
33
|
+
export declare const SHELL_READ_FINDING_RANK: {
|
|
34
|
+
readonly identity_changed: 6;
|
|
35
|
+
readonly deny_hit: 5;
|
|
36
|
+
readonly outside_roots: 4;
|
|
37
|
+
readonly unestablishable: 3;
|
|
38
|
+
readonly declined: 2;
|
|
39
|
+
readonly none: 0;
|
|
40
|
+
};
|
|
41
|
+
/** Compile-time fence: `never` while every finding has a rank. */
|
|
42
|
+
export type RankTableCoversEveryFinding = AssertAllKeysHandled<Exclude<ShellReadFinding, keyof typeof SHELL_READ_FINDING_RANK>>;
|
|
43
|
+
/** What a seat does with a finding. `refuse` = a deny at the station (or a veto after a person's yes);
|
|
44
|
+
* `mandated` = an ask no automatic lane may clear; `ordinary` = an ask any answerer may clear (a person,
|
|
45
|
+
* the auto-mode classifier, a stored allow rule) but no answerless engine table may; `vouch` = the station
|
|
46
|
+
* has no question. */
|
|
47
|
+
export type ShellReadDisposition = "refuse" | "mandated" | "ordinary" | "vouch";
|
|
48
|
+
/**
|
|
49
|
+
* THE decision table — total over findings × seats; no cell is n/a. Row by row:
|
|
50
|
+
* · `deny_hit` — refused on every seat: a deny row is a floor no root grant and no approval moves.
|
|
51
|
+
* · `outside_roots` — asked where a person can be asked (the two full-shell seats), refused where none
|
|
52
|
+
* can (the read-only face has no approval channel; a rewrite that leaves the roots after approval was
|
|
53
|
+
* seen by nobody).
|
|
54
|
+
* · `unestablishable` — the same shape: the floor could not say where the command reads, so it is not
|
|
55
|
+
* vouched anywhere; a person may still answer it on the full shell.
|
|
56
|
+
* · `declined` — the one row the seats disagree on, and the one deliberate fail-open of this table: the
|
|
57
|
+
* scan could not read the program bash will run or the operands arrive on a stream. The `off` doctrine
|
|
58
|
+
* mounts nothing else that could ask, so it mandates; the `classify` doctrine composes a shape classifier
|
|
59
|
+
* beside this station and the ruled trade is an ORDINARY ask there — an answerer may settle it, and an
|
|
60
|
+
* out-of-root read hidden inside such a command is then not asked again. The read-only face refuses:
|
|
61
|
+
* its allowlist is a deployment knob, so this cell is reachable and must not be n/a.
|
|
62
|
+
* · `identity_changed` — a candidate established at approval time resolves elsewhere at spawn time:
|
|
63
|
+
* refused everywhere (it is only ever produced on the `rejudge` seat).
|
|
64
|
+
* · `none` — vouched: the station has no question (the classify seat's shape classifier may still ask).
|
|
65
|
+
*/
|
|
66
|
+
export declare const SHELL_READ_DECISION: {
|
|
67
|
+
readonly deny_hit: {
|
|
68
|
+
readonly bash_off: "refuse";
|
|
69
|
+
readonly bash_classify: "refuse";
|
|
70
|
+
readonly readonly: "refuse";
|
|
71
|
+
readonly rejudge: "refuse";
|
|
72
|
+
};
|
|
73
|
+
readonly outside_roots: {
|
|
74
|
+
readonly bash_off: "mandated";
|
|
75
|
+
readonly bash_classify: "mandated";
|
|
76
|
+
readonly readonly: "refuse";
|
|
77
|
+
readonly rejudge: "refuse";
|
|
78
|
+
};
|
|
79
|
+
readonly unestablishable: {
|
|
80
|
+
readonly bash_off: "mandated";
|
|
81
|
+
readonly bash_classify: "mandated";
|
|
82
|
+
readonly readonly: "refuse";
|
|
83
|
+
readonly rejudge: "refuse";
|
|
84
|
+
};
|
|
85
|
+
readonly declined: {
|
|
86
|
+
readonly bash_off: "mandated";
|
|
87
|
+
readonly bash_classify: "ordinary";
|
|
88
|
+
readonly readonly: "refuse";
|
|
89
|
+
readonly rejudge: "refuse";
|
|
90
|
+
};
|
|
91
|
+
readonly identity_changed: {
|
|
92
|
+
readonly bash_off: "refuse";
|
|
93
|
+
readonly bash_classify: "refuse";
|
|
94
|
+
readonly readonly: "refuse";
|
|
95
|
+
readonly rejudge: "refuse";
|
|
96
|
+
};
|
|
97
|
+
readonly none: {
|
|
98
|
+
readonly bash_off: "vouch";
|
|
99
|
+
readonly bash_classify: "vouch";
|
|
100
|
+
readonly readonly: "vouch";
|
|
101
|
+
readonly rejudge: "vouch";
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
/** Compile-time fence: `never` while every finding has a row. */
|
|
105
|
+
export type DecisionTableCoversEveryFinding = AssertAllKeysHandled<Exclude<ShellReadFinding, keyof typeof SHELL_READ_DECISION>>;
|
|
106
|
+
/** Compile-time fence: `never` while every seat has a cell on the `none` row (the `satisfies` above holds
|
|
107
|
+
* every other row to the same shape). */
|
|
108
|
+
export type DecisionTableCoversEverySeat = AssertAllKeysHandled<Exclude<ShellReadSeat, keyof (typeof SHELL_READ_DECISION)["none"]>>;
|
|
109
|
+
/** The disposition of one command-level finding on one seat — the table, read. */
|
|
110
|
+
export declare function decideShellRead(finding: ShellReadFinding, seat: ShellReadSeat): ShellReadDisposition;
|
|
111
|
+
/**
|
|
112
|
+
* The boundary a shell read is judged against: the declared roots (canonical), the working directory and
|
|
113
|
+
* home the plan's bases start from, the deny seat (the SAME `denyMatch` closure every read face mounts),
|
|
114
|
+
* the containment face, and — for the read-only face — the engine's own overflow-spool fence as one more
|
|
115
|
+
* member of "inside" (`alsoInside`), asked once per target that landed outside the roots.
|
|
116
|
+
*/
|
|
117
|
+
export interface ShellReadBoundary extends BashReadonlyRootBoundary {
|
|
118
|
+
readonly alsoInside?: (canonicalTarget: string) => Promise<boolean>;
|
|
119
|
+
}
|
|
120
|
+
/** How far one candidate got before it was judged. `lexical` = the folded spelling is the judged target
|
|
121
|
+
* (an isolated sandbox, or a resolver that declares no authority and did not answer); `canonical` = the
|
|
122
|
+
* environment resolved it; `expansion` = one path a pattern expanded to or a traversal enumerated;
|
|
123
|
+
* `unestablishable` = the floor could not say where it lands (`cause` says why). */
|
|
124
|
+
export type ReadEstablishment = "lexical" | "canonical" | "expansion" | "unestablishable";
|
|
125
|
+
export interface EstablishedRead {
|
|
126
|
+
readonly candidate: ReadCandidate;
|
|
127
|
+
readonly established: ReadEstablishment;
|
|
128
|
+
/** The judged target: an absolute path (canonical where established so, the folded spelling otherwise).
|
|
129
|
+
* Absent exactly when `established === "unestablishable"`. */
|
|
130
|
+
readonly target?: string;
|
|
131
|
+
/** Every spelling the deny judge sees BESIDE the target: the folded spelling, the kernel spelling of a
|
|
132
|
+
* `..` operand, the pattern an expansion came from, the placeholder-carrying fold of a substitution. */
|
|
133
|
+
readonly spellings: readonly string[];
|
|
134
|
+
/** The pattern or traversal root an `expansion` came from, as folded. */
|
|
135
|
+
readonly origin?: string;
|
|
136
|
+
/** Why the candidate could not be established. */
|
|
137
|
+
readonly cause?: string;
|
|
138
|
+
}
|
|
139
|
+
/** What the judge found about ONE candidate. `kind` is one of the four candidate-level findings
|
|
140
|
+
* (`declined` is a segment fact, `identity_changed` a re-establishment fact — neither is a candidate's). */
|
|
141
|
+
export interface ReadFinding {
|
|
142
|
+
readonly read: EstablishedRead;
|
|
143
|
+
readonly kind: Extract<ShellReadFinding, "deny_hit" | "outside_roots" | "unestablishable" | "none">;
|
|
144
|
+
/** The deny row that matched and the view it matched on. */
|
|
145
|
+
readonly deny?: {
|
|
146
|
+
readonly pattern: string;
|
|
147
|
+
readonly matchedView: string;
|
|
148
|
+
};
|
|
149
|
+
readonly containment: "inside" | "outside" | "open" | "unknown";
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Judge ONE established candidate. Deny first, over every view (a hit on any view decides — a symlink whose
|
|
153
|
+
* SPELLING names a guarded path is refused even when its target is elsewhere, and a substitution's folded
|
|
154
|
+
* spelling is judged although the candidate could not be established); containment over the canonical
|
|
155
|
+
* target alone, and only where the boundary's face closes it.
|
|
156
|
+
*
|
|
157
|
+
* @contract shell.read_boundary.friction_floor — what this judge answers is a question about TEXT and the
|
|
158
|
+
* FILESYSTEM the command will run in: which files the words of a command may name, resolved against the
|
|
159
|
+
* declared roots, the declared home and the observed working directory, and — where the environment can
|
|
160
|
+
* answer — where those spellings really land (symlinks resolved, patterns expanded, traversal roots
|
|
161
|
+
* enumerated, each within a bound). It is not a question about what the process will open. A deployment's
|
|
162
|
+
* containment for what a shell may reach is the `ExecutionEnv` the command runs in (a sandbox, a
|
|
163
|
+
* container, a remote adapter); everything here is the friction floor under that containment. Under an
|
|
164
|
+
* isolated sandbox the floor is lexical (the sandbox is the boundary and its filesystem is not this
|
|
165
|
+
* process's to resolve). A deny-listed read is REFUSED on every seat; an out-of-root read asks where a
|
|
166
|
+
* person can be asked and is refused where none can; a read the floor cannot establish (an expansion it
|
|
167
|
+
* cannot finish, a spelling the shell rewrites, a working directory it cannot place) is asked or refused
|
|
168
|
+
* the same way — never vouched. Containment judges what a READ program opens: a program path is executed,
|
|
169
|
+
* not disclosed, and a leading environment word's value reaches the program through its environment, so
|
|
170
|
+
* neither is a containment candidate — both still meet the deny judge, on every spelling.
|
|
171
|
+
*/
|
|
172
|
+
export declare function judgeRead(read: EstablishedRead, boundary: ShellReadBoundary): ReadFinding;
|
|
173
|
+
/** One segment's judged reads and declines, with its nested plans judged the same way. */
|
|
174
|
+
export interface JudgedSegment {
|
|
175
|
+
readonly segment: ShellReadSegment;
|
|
176
|
+
readonly findings: readonly ReadFinding[];
|
|
177
|
+
readonly nested: readonly JudgedPlan[];
|
|
178
|
+
}
|
|
179
|
+
export interface JudgedPlan {
|
|
180
|
+
readonly plan: ShellReadPlan;
|
|
181
|
+
readonly segments: readonly JudgedSegment[];
|
|
182
|
+
}
|
|
183
|
+
/** The command-level fold and the finding that produced it. `witness` is absent for `none`; for `declined`
|
|
184
|
+
* the segment that declined stands in for a candidate. */
|
|
185
|
+
export interface ShellReadFold {
|
|
186
|
+
readonly finding: ShellReadFinding;
|
|
187
|
+
readonly witness?: ReadFinding;
|
|
188
|
+
readonly declined?: ShellReadSegment;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Fold a judged plan to ONE finding by {@link SHELL_READ_FINDING_RANK}, over every segment and every nested
|
|
192
|
+
* plan. Two structural rules ride the fold:
|
|
193
|
+
* · a plan the lexer refused (`unreadable`) is `unestablishable`, unconditionally — a quote-blind re-read
|
|
194
|
+
* yields more words, never zero, so an unreadable text is never "no candidates";
|
|
195
|
+
* · a SHELL re-entry segment (`sh -c S`, `eval S`, `env -S 'S'`, a launcher handing text to a shell) is at
|
|
196
|
+
* least `unestablishable` — what runs is text the shell parses, not this plan — and its nested plan may
|
|
197
|
+
* only RAISE that to `deny_hit`: the nested text's deny hits count (a refusal is a refusal whatever runs
|
|
198
|
+
* it), its containment and its silence do not (an inner `cat inside.txt` vouches nothing about the
|
|
199
|
+
* outer text). A substitution body (`$(…)`, a backtick, `<(…)`) is nested WITHOUT re-entry and folds
|
|
200
|
+
* in full.
|
|
201
|
+
*/
|
|
202
|
+
export declare function foldShellReadFindings(judged: JudgedPlan): ShellReadFold;
|
|
203
|
+
/** Judge every established read of a plan, in place. `establish` runs per segment so a caller can bound it. */
|
|
204
|
+
export declare function judgeEstablishedPlan(plan: ShellReadPlan, established: ReadonlyMap<ShellReadSegment, readonly EstablishedRead[]>, boundary: ShellReadBoundary, nestedOf: (plan: ShellReadPlan) => JudgedPlan): JudgedPlan;
|
|
205
|
+
/** Every finding of a judged plan, flattened (segments and nested plans alike). */
|
|
206
|
+
export declare function allFindings(judged: JudgedPlan): ReadFinding[];
|
|
207
|
+
/**
|
|
208
|
+
* The deepest directory containing both, or `undefined` when they share none (two path families).
|
|
209
|
+
*
|
|
210
|
+
* LINEAR, and that is the point: both paths are already folded, so the answer is their common prefix cut
|
|
211
|
+
* at a separator. Climbing one directory at a time re-folds the whole remaining path each step — the same
|
|
212
|
+
* answer at quadratic cost, measured at 4.9 s of synchronous work for one 32 KB command naming a
|
|
213
|
+
* 16 000-segment operand. The family-aware comparator keeps the last word: a prefix it cannot confirm
|
|
214
|
+
* contains both paths yields no candidate.
|
|
215
|
+
*/
|
|
216
|
+
export declare function deepestCommonDir(a: string, b: string): string | undefined;
|
|
217
|
+
/**
|
|
218
|
+
* The ONE directory whose addition to the declared roots would answer every `outside_roots` finding of a
|
|
219
|
+
* command — offered only when re-judging the same established reads with that root added leaves no
|
|
220
|
+
* finding at all (MEASURED, not argued: a deny row or an unestablishable candidate beside the out-of-root
|
|
221
|
+
* one leaves the ask standing whatever root is granted, and then nothing is offered). `exact` ⇒ the value
|
|
222
|
+
* is the one path the command NAMED, offered verbatim, because the containing directory would hand out the
|
|
223
|
+
* home or a root. A `..` spelling gets no offer: the folded and the kernel reading of it can disagree about
|
|
224
|
+
* whether the target is back inside, which no replay of the text settles.
|
|
225
|
+
*/
|
|
226
|
+
export declare function readRootGrantCandidateOf(judged: JudgedPlan, boundary: ShellReadBoundary): {
|
|
227
|
+
readonly dir: string;
|
|
228
|
+
readonly covers?: "exact";
|
|
229
|
+
} | undefined;
|
|
230
|
+
/** The same judged plan under another boundary — the established reads are kept, only the judgment reruns. */
|
|
231
|
+
export declare function rejudge(judged: JudgedPlan, boundary: ShellReadBoundary): JudgedPlan;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { hasWin32AliasedComponent, normalizeAbsPathLexically, withinAnyRoot } from "./safety.js";
|
|
2
|
+
export const SHELL_READ_FINDINGS = ["deny_hit", "outside_roots", "unestablishable", "declined", "identity_changed", "none"];
|
|
3
|
+
export const SHELL_READ_SEATS = ["bash_off", "bash_classify", "readonly", "rejudge"];
|
|
4
|
+
export const SHELL_READ_FINDING_RANK = {
|
|
5
|
+
identity_changed: 6,
|
|
6
|
+
deny_hit: 5,
|
|
7
|
+
outside_roots: 4,
|
|
8
|
+
unestablishable: 3,
|
|
9
|
+
declined: 2,
|
|
10
|
+
none: 0,
|
|
11
|
+
};
|
|
12
|
+
export const SHELL_READ_DECISION = {
|
|
13
|
+
deny_hit: { bash_off: "refuse", bash_classify: "refuse", readonly: "refuse", rejudge: "refuse" },
|
|
14
|
+
outside_roots: { bash_off: "mandated", bash_classify: "mandated", readonly: "refuse", rejudge: "refuse" },
|
|
15
|
+
unestablishable: { bash_off: "mandated", bash_classify: "mandated", readonly: "refuse", rejudge: "refuse" },
|
|
16
|
+
declined: { bash_off: "mandated", bash_classify: "ordinary", readonly: "refuse", rejudge: "refuse" },
|
|
17
|
+
identity_changed: { bash_off: "refuse", bash_classify: "refuse", readonly: "refuse", rejudge: "refuse" },
|
|
18
|
+
none: { bash_off: "vouch", bash_classify: "vouch", readonly: "vouch", rejudge: "vouch" },
|
|
19
|
+
};
|
|
20
|
+
export function decideShellRead(finding, seat) {
|
|
21
|
+
return SHELL_READ_DECISION[finding][seat];
|
|
22
|
+
}
|
|
23
|
+
export function judgeRead(read, boundary) {
|
|
24
|
+
const deny = boundary.denyMatch;
|
|
25
|
+
if (deny !== undefined) {
|
|
26
|
+
const views = [...(read.target !== undefined ? [read.target] : []), ...read.spellings];
|
|
27
|
+
for (const view of views) {
|
|
28
|
+
const folded = normalizeAbsPathLexically(view);
|
|
29
|
+
const pattern = deny(folded);
|
|
30
|
+
if (pattern !== null)
|
|
31
|
+
return { read, kind: "deny_hit", deny: { pattern, matchedView: folded }, containment: containmentOf(read, boundary) };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const containment = containmentOf(read, boundary);
|
|
35
|
+
const provenance = read.candidate.provenance;
|
|
36
|
+
if (provenance === "env_assignment" || (provenance === "program_path" && read.candidate.expands !== "unresolvable"))
|
|
37
|
+
return { read, kind: "none", containment };
|
|
38
|
+
if (read.established === "unestablishable")
|
|
39
|
+
return { read, kind: "unestablishable", containment };
|
|
40
|
+
return { read, kind: containment === "outside" ? "outside_roots" : "none", containment };
|
|
41
|
+
}
|
|
42
|
+
function containmentOf(read, boundary) {
|
|
43
|
+
if (boundary.face === "open")
|
|
44
|
+
return "open";
|
|
45
|
+
if (read.target === undefined)
|
|
46
|
+
return "unknown";
|
|
47
|
+
return withinAnyRoot(boundary.roots, read.target) ? "inside" : "outside";
|
|
48
|
+
}
|
|
49
|
+
export function foldShellReadFindings(judged) {
|
|
50
|
+
let best = { finding: "none" };
|
|
51
|
+
const raise = (next) => {
|
|
52
|
+
if (SHELL_READ_FINDING_RANK[next.finding] > SHELL_READ_FINDING_RANK[best.finding])
|
|
53
|
+
best = next;
|
|
54
|
+
};
|
|
55
|
+
const walk = (j, denyOnly) => {
|
|
56
|
+
if (j.plan.unreadable !== undefined && !denyOnly)
|
|
57
|
+
raise({ finding: "unestablishable" });
|
|
58
|
+
for (const s of j.segments) {
|
|
59
|
+
for (const f of s.findings) {
|
|
60
|
+
if (denyOnly && f.kind !== "deny_hit")
|
|
61
|
+
continue;
|
|
62
|
+
if (f.kind !== "none")
|
|
63
|
+
raise({ finding: f.kind, witness: f });
|
|
64
|
+
}
|
|
65
|
+
if (!denyOnly) {
|
|
66
|
+
if (s.segment.declines.length > 0)
|
|
67
|
+
raise({ finding: "declined", declined: s.segment });
|
|
68
|
+
if (s.segment.reentry === "shell")
|
|
69
|
+
raise({ finding: "unestablishable", declined: s.segment });
|
|
70
|
+
if (s.segment.unreadProgramText !== undefined)
|
|
71
|
+
raise({ finding: "unestablishable", declined: s.segment });
|
|
72
|
+
const move = s.segment.baseMove;
|
|
73
|
+
if (move !== undefined && (typeof move.target === "string" || move.target.expands !== false))
|
|
74
|
+
raise({ finding: "unestablishable", declined: s.segment });
|
|
75
|
+
}
|
|
76
|
+
const nestedDenyOnly = denyOnly || s.segment.reentry === "shell";
|
|
77
|
+
for (const n of s.nested)
|
|
78
|
+
walk(n, nestedDenyOnly);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
walk(judged, false);
|
|
82
|
+
return best;
|
|
83
|
+
}
|
|
84
|
+
export function judgeEstablishedPlan(plan, established, boundary, nestedOf) {
|
|
85
|
+
return {
|
|
86
|
+
plan,
|
|
87
|
+
segments: plan.segments.map((segment) => ({
|
|
88
|
+
segment,
|
|
89
|
+
findings: (established.get(segment) ?? []).map((r) => judgeRead(r, boundary)),
|
|
90
|
+
nested: segment.nested.map(nestedOf),
|
|
91
|
+
})),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
export function allFindings(judged) {
|
|
95
|
+
const out = [];
|
|
96
|
+
const walk = (j) => {
|
|
97
|
+
for (const s of j.segments) {
|
|
98
|
+
out.push(...s.findings);
|
|
99
|
+
for (const n of s.nested)
|
|
100
|
+
walk(n);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
walk(judged);
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
function parentDirLexically(p) {
|
|
107
|
+
return normalizeAbsPathLexically(`${p}/..`);
|
|
108
|
+
}
|
|
109
|
+
export function deepestCommonDir(a, b) {
|
|
110
|
+
if (a === b)
|
|
111
|
+
return a;
|
|
112
|
+
const shorter = Math.min(a.length, b.length);
|
|
113
|
+
let end = 0;
|
|
114
|
+
let i = 0;
|
|
115
|
+
while (i < shorter && a[i] === b[i]) {
|
|
116
|
+
if (a[i] === "/" || a[i] === "\\")
|
|
117
|
+
end = i;
|
|
118
|
+
i++;
|
|
119
|
+
}
|
|
120
|
+
if (i === shorter && (a[shorter] === "/" || a[shorter] === "\\" || b[shorter] === "/" || b[shorter] === "\\"))
|
|
121
|
+
end = shorter;
|
|
122
|
+
const cut = a.slice(0, end);
|
|
123
|
+
const candidate = cut.includes("/") || cut.includes("\\") ? cut : a.slice(0, end + 1);
|
|
124
|
+
if (candidate.length === 0 || !withinAnyRoot([candidate], a) || !withinAnyRoot([candidate], b))
|
|
125
|
+
return undefined;
|
|
126
|
+
return candidate;
|
|
127
|
+
}
|
|
128
|
+
export function readRootGrantCandidateOf(judged, boundary) {
|
|
129
|
+
const outside = allFindings(judged).filter((f) => f.kind === "outside_roots");
|
|
130
|
+
if (outside.length === 0)
|
|
131
|
+
return undefined;
|
|
132
|
+
const targets = [...new Set(outside.map((f) => f.read.target))];
|
|
133
|
+
const paths = targets.filter((p) => !targets.some((q) => q !== p && withinAnyRoot([p], q)));
|
|
134
|
+
if (outside.some((f) => f.read.candidate.spelled.split(/[\\/]/).includes("..")))
|
|
135
|
+
return undefined;
|
|
136
|
+
if (paths.some(hasWin32AliasedComponent))
|
|
137
|
+
return undefined;
|
|
138
|
+
const readsDirectory = new Set(outside.filter((f) => f.read.candidate.provenance === "traversal_root" || f.read.candidate.provenance === "cd_target").map((f) => f.read.target));
|
|
139
|
+
const dirOf = (path) => (readsDirectory.has(path) ? path : parentDirLexically(path));
|
|
140
|
+
let candidate = dirOf(paths[0]);
|
|
141
|
+
for (const path of paths.slice(1)) {
|
|
142
|
+
if (candidate === undefined)
|
|
143
|
+
return undefined;
|
|
144
|
+
const d = dirOf(path);
|
|
145
|
+
if (d === undefined)
|
|
146
|
+
return undefined;
|
|
147
|
+
candidate = deepestCommonDir(candidate, d);
|
|
148
|
+
}
|
|
149
|
+
if (candidate === undefined)
|
|
150
|
+
return undefined;
|
|
151
|
+
const tooWide = parentDirLexically(candidate) === candidate ||
|
|
152
|
+
(boundary.homeDir !== undefined && withinAnyRoot([candidate], boundary.homeDir)) ||
|
|
153
|
+
boundary.roots.some((root) => withinAnyRoot([candidate], root));
|
|
154
|
+
const proposed = tooWide ? (paths.length === 1 ? { dir: paths[0], covers: "exact" } : undefined) : { dir: candidate };
|
|
155
|
+
if (proposed === undefined)
|
|
156
|
+
return undefined;
|
|
157
|
+
const widened = { ...boundary, roots: [...boundary.roots, proposed.dir] };
|
|
158
|
+
const replay = rejudge(judged, widened);
|
|
159
|
+
return foldShellReadFindings(replay).finding === "none" ? proposed : undefined;
|
|
160
|
+
}
|
|
161
|
+
export function rejudge(judged, boundary) {
|
|
162
|
+
return {
|
|
163
|
+
plan: judged.plan,
|
|
164
|
+
segments: judged.segments.map((s) => ({
|
|
165
|
+
segment: s.segment,
|
|
166
|
+
findings: s.findings.map((f) => judgeRead(f.read, boundary)),
|
|
167
|
+
nested: s.nested.map((n) => rejudge(n, boundary)),
|
|
168
|
+
})),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
@@ -85,6 +85,9 @@ export interface ShellReadSegment {
|
|
|
85
85
|
/** `shell` = the program runs its argument as shell text; `argv` = its operands arrive on a stream;
|
|
86
86
|
* `interpreter` = it runs an inline program in a language this parser does not read (a shape fact only). */
|
|
87
87
|
readonly reentry?: "shell" | "argv" | "interpreter";
|
|
88
|
+
/** Program text the station READS as a rule (a `sed` script names the files it opens) but could not read here —
|
|
89
|
+
* supplied from a file, or not parseable to its end. What such a segment reads is then not established. */
|
|
90
|
+
readonly unreadProgramText?: string;
|
|
88
91
|
readonly declines: readonly ShellScanDecline[];
|
|
89
92
|
readonly reads: readonly ReadCandidate[];
|
|
90
93
|
readonly redirects: readonly ShellReadRedirect[];
|