@sema-agent/core 7.33.8 → 7.35.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 +462 -3
- 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/errors.d.ts +23 -1
- package/dist/brain/errors.js +19 -2
- package/dist/brain/open-responses.js +1 -0
- package/dist/brain/openai.js +1 -0
- package/dist/brain/status-sink.d.ts +7 -1
- package/dist/brain/stream-engine.d.ts +3 -0
- package/dist/brain/stream-engine.js +2 -1
- package/dist/brain/stream-shared.d.ts +3 -1
- package/dist/brain/stream-shared.js +16 -4
- package/dist/core/a2a.d.ts +3 -1
- package/dist/core/a2a.js +1 -1
- package/dist/core/ask-origin.d.ts +379 -154
- package/dist/core/ask-origin.js +224 -45
- package/dist/core/ask-question.js +2 -1
- 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 +54 -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 +19 -3
- package/dist/core/gate-lanes.js +116 -50
- package/dist/core/gate-outcome.d.ts +40 -7
- package/dist/core/gate-outcome.js +26 -4
- package/dist/core/governance-codes.d.ts +18 -2
- package/dist/core/governance-codes.js +23 -1
- package/dist/core/hooks.d.ts +53 -29
- package/dist/core/hooks.js +1 -3
- package/dist/core/mcp.js +1 -1
- package/dist/core/message-utils.d.ts +2 -1
- package/dist/core/message-utils.js +2 -1
- package/dist/core/mode-policy.d.ts +185 -0
- package/dist/core/mode-policy.js +184 -0
- package/dist/core/oracle-isolation.d.ts +3 -3
- package/dist/core/permission-rule-model.d.ts +20 -3
- package/dist/core/permission-rule-model.js +40 -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 +56 -9
- package/dist/core/persisted-rule-arms.d.ts +14 -10
- package/dist/core/persisted-rule-arms.js +13 -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/active-skill-scope.js +5 -4
- package/dist/core/runner/assemble-result.js +2 -2
- 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 +62 -38
- package/dist/core/runner/denial-limit-arms.d.ts +35 -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/file-history-request.d.ts +26 -0
- package/dist/core/runner/file-history-request.js +90 -0
- 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 +11 -6
- package/dist/core/runner/permission-rule-lanes.js +55 -48
- 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 +6 -4
- package/dist/core/runner/prepare-ask-lane.js +3 -3
- 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-file-history.d.ts +9 -3
- package/dist/core/runner/prepare-file-history.js +4 -90
- package/dist/core/runner/prepare-gate-stations.d.ts +37 -9
- package/dist/core/runner/prepare-gate-stations.js +46 -29
- 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 +30 -23
- package/dist/core/runner/prepare-inherited-gate.js +45 -25
- package/dist/core/runner/prepare-park-ask.d.ts +6 -4
- package/dist/core/runner/prepare-park-ask.js +7 -5
- package/dist/core/runner/prepare-policy-chain.d.ts +25 -4
- package/dist/core/runner/prepare-policy-chain.js +203 -180
- 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 +56 -24
- 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-brain-sinks.js +3 -1
- package/dist/core/runner/run-identity-wiring.js +1 -1
- package/dist/core/runner/run-leg.js +29 -18
- package/dist/core/runner/run-notification-lane.d.ts +3 -0
- package/dist/core/runner/run-notification-lane.js +3 -1
- 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.d.ts +1 -0
- package/dist/core/runner/runtask.js +8 -11
- package/dist/core/runner/session-mode-seats.d.ts +48 -0
- package/dist/core/runner/session-mode-seats.js +73 -0
- package/dist/core/runner/session-rule-policy.js +3 -2
- 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 +30 -58
- package/dist/core/secret-env.js +149 -586
- package/dist/core/sensitive-path-policy.d.ts +4 -2
- package/dist/core/sensitive-path-policy.js +41 -31
- package/dist/core/store-contracts/checkpoint-store-contract.js +2 -1
- 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 +84 -30
- package/dist/core/tool-policy.js +76 -16
- package/dist/core/tool-registry.d.ts +38 -0
- package/dist/core/tool-registry.js +25 -1
- package/dist/core/tool-roster.d.ts +17 -2
- package/dist/core/tool-roster.js +11 -1
- package/dist/core/tool-spec.d.ts +8 -9
- package/dist/core/trace.d.ts +18 -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/node-execution-env.d.ts +6 -1
- package/dist/engine/execution-env/node-execution-env.js +16 -3
- package/dist/engine/harness/agent-harness.js +2 -1
- 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/engine/lsp/node-lsp-manager.js +1 -1
- package/dist/index.d.ts +18 -8
- package/dist/index.js +15 -6
- 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/index.d.ts +5 -3
- package/dist/stores/file/session-store.d.ts +8 -2
- package/dist/stores/file/session-store.js +5 -5
- 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 +50 -0
- package/dist/tools/fs/file-read-check.js +79 -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 +52 -13
- package/dist/tools/fs/shell-read-check.js +199 -20
- 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 +1 -1
- package/test/export-surface.snapshot.json +117 -11
- package/wire-changes/core@7.34.0.json +2621 -0
- package/wire-changes/core@7.35.0.json +584 -0
|
@@ -22,19 +22,23 @@ 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
|
-
|
|
26
|
-
|
|
25
|
+
import { type ToolCallLike } from "../../core/tool-registry.js";
|
|
26
|
+
/** The ONE mint of the containment card — the targets a finding established OUTSIDE the roots (an out-of-root read, a
|
|
27
|
+
* target that resolves elsewhere by the spawn). */
|
|
27
28
|
declare function containmentDetails(paths: readonly string[]): {
|
|
28
29
|
readonly type: "readonly_out_of_root";
|
|
29
30
|
readonly code: "readonly_out_of_root";
|
|
30
31
|
readonly paths: readonly string[];
|
|
31
32
|
};
|
|
32
|
-
/** The
|
|
33
|
-
*
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
code: "
|
|
37
|
-
}
|
|
33
|
+
/** The ONE mint of the card of a finding that established nothing: where the command reads could not be established
|
|
34
|
+
* (an unreadable or re-entered program text, a program named from its input, a station that could not answer). */
|
|
35
|
+
declare function unestablishedDetails(): {
|
|
36
|
+
readonly type: "read_unestablished";
|
|
37
|
+
readonly code: "read_unestablished";
|
|
38
|
+
};
|
|
39
|
+
/** The structured details a refusal or a mandated card carries: the deny card is the read faces' own (`violationDetails`,
|
|
40
|
+
* safety.ts), the containment and the unestablished cards are minted above. */
|
|
41
|
+
export type ShellReadRefusalDetails = NonNullable<ReturnType<typeof violationDetails>> | ReturnType<typeof containmentDetails> | ReturnType<typeof unestablishedDetails>;
|
|
38
42
|
/** What the station answers for ONE call. */
|
|
39
43
|
export interface ShellReadVerdict {
|
|
40
44
|
readonly disposition: ShellReadDisposition;
|
|
@@ -43,12 +47,40 @@ export interface ShellReadVerdict {
|
|
|
43
47
|
* sentence). `mandated` / `ordinary`: the ACCOUNT of the finding, for the approval card's disclosure seat
|
|
44
48
|
* (never the ask's own message — that is the auto-mode classifier's input). Absent for `vouch`. */
|
|
45
49
|
readonly sentence?: string;
|
|
46
|
-
/** `refuse`: the refusal's structured details. `mandated` on an `outside_roots` / `unestablishable` finding
|
|
47
|
-
*
|
|
48
|
-
* its `tool_end`
|
|
50
|
+
/** `refuse`: the refusal's structured details. `mandated` on an `outside_roots` / `unestablishable` finding (and
|
|
51
|
+
* `ordinary` on the file tools' out-of-root read): the finding's card, so an ask a person refuses or nobody can answer
|
|
52
|
+
* still carries WHAT was found on its `tool_end` — the targets outside the roots (`readonly_out_of_root`, or the file
|
|
53
|
+
* tools' `path_not_in_root`), or that nothing could be established (`read_unestablished`). */
|
|
49
54
|
readonly details?: ShellReadRefusalDetails;
|
|
50
55
|
/** `mandated` on an `outside_roots` finding: the directory whose grant would clear this ask. */
|
|
51
56
|
readonly readRootCandidate?: ReadRootGrantCandidate;
|
|
57
|
+
/** A FACT beside the disposition, for the mode that answers a file command itself: every write target this command
|
|
58
|
+
* names was established inside the boundary's WRITE roots at every seat the program writes it (the entry, where it
|
|
59
|
+
* lands, or both — `WriteSeat`, shell-read-write-targets.ts), off the write-protection table and off every deployment
|
|
60
|
+
* write guard (`ShellReadBoundary.writeProtectionJudge` / `writeTargetGuard`), no removal (`rm` / `rmdir`) targets a
|
|
61
|
+
* write root or a directory containing one, every program the command runs is in its text (no `sed -f` file, no
|
|
62
|
+
* re-entered shell, no unreadable text), and the line's order keeps each segment's establishment true
|
|
63
|
+
* ({@link lineOrderPlaced}). Absent ⇒ not established — the mode does not answer; the station's own table is unchanged. */
|
|
64
|
+
readonly writesPlaced?: true;
|
|
65
|
+
/** A FACT beside the disposition: a write target this command names lands on the write-protection table — judged by
|
|
66
|
+
* the table's own judge, as a write tool's target is — so the command's ask carries that table's class, whichever
|
|
67
|
+
* mode reads it. Absent ⇒ no target the station established is covered (or no judge is mounted). */
|
|
68
|
+
readonly writeProtected?: true;
|
|
69
|
+
/** A FACT beside `writesPlaced`'s absence: the command is one of the mode's file commands whose writes the station would
|
|
70
|
+
* have placed but for an OPTION WORD on a `cp` / `mv` segment ({@link UNPLACED_OPTIONS}: any word starting with `-` — the
|
|
71
|
+
* reference implementation's rule, "with flags requires manual approval to ensure path safety"). Names the first such
|
|
72
|
+
* program, so the approval card and the headless refusal can say WHY the mode did not answer. Absent ⇒ not that reason
|
|
73
|
+
* (or the writes were placed). */
|
|
74
|
+
readonly unplacedOptions?: {
|
|
75
|
+
readonly program: "cp" | "mv";
|
|
76
|
+
};
|
|
77
|
+
/** A FACT beside the disposition: some read of the command could not be established (an `unestablishable` or `declined`
|
|
78
|
+
* finding anywhere in it, whichever finding won). A mode that releases the call releases it past the read-deny table. */
|
|
79
|
+
readonly unestablished?: true;
|
|
80
|
+
/** The station's admission of this call's judged target, activated only by the caller that APPLIES this verdict (the
|
|
81
|
+
* gate's fold, the resumed leg's belt): a verdict nobody received — a wait that elapsed, a judgment that threw —
|
|
82
|
+
* admits nothing, whenever it completes. */
|
|
83
|
+
readonly admit?: () => void;
|
|
52
84
|
}
|
|
53
85
|
/** The station seat one tool call is judged on (the `rejudge` seat is asked for by the caller). */
|
|
54
86
|
export type ShellReadStationSeat = Exclude<ShellReadSeat, "rejudge">;
|
|
@@ -58,14 +90,21 @@ export type ShellReadStationSeat = Exclude<ShellReadSeat, "rejudge">;
|
|
|
58
90
|
* is load-bearing for the gate: a call the station does not cover must not cost the fold a suspension
|
|
59
91
|
* point. `unanswered` is the verdict the fold substitutes when the wait elapsed or threw — fail-closed as
|
|
60
92
|
* `unestablishable` on the call's own seat.
|
|
93
|
+
*
|
|
94
|
+
* Both take the CALL (`{ toolName, face }`), not a name: a station is a tighten keyed on the engine's own tool names,
|
|
95
|
+
* so it holds a call made under ANY of the call's names (`ruleNamesOf` — a tool that answers to `Bash` under another
|
|
96
|
+
* wire name is judged as `Bash`). The first of the names the station's membership holds keys its seat, its boundary
|
|
97
|
+
* and the admission the engine's tool reads before it spawns; the sentences name the call's wire name.
|
|
61
98
|
*/
|
|
62
99
|
export interface ShellReadCheck {
|
|
63
|
-
judge(
|
|
100
|
+
judge(call: ReadStationCall, args: unknown, cwd: string | undefined, signal: AbortSignal | undefined, opts: {
|
|
64
101
|
readonly toolCallId?: string;
|
|
65
102
|
readonly seat?: "rejudge";
|
|
66
103
|
}): Promise<ShellReadVerdict> | null;
|
|
67
|
-
unanswered(
|
|
104
|
+
unanswered(call: ReadStationCall, cause: string): ShellReadVerdict;
|
|
68
105
|
}
|
|
106
|
+
/** The call a read station judges: its wire name and the face the gate stamped (whose `names` the station reads). */
|
|
107
|
+
export type ReadStationCall = Pick<ToolCallLike, "toolName" | "face">;
|
|
69
108
|
/** The pre-spawn admission the shell tools call: re-establishes what the station established for this
|
|
70
109
|
* call and refuses when any canonical target moved. `undefined` ⇒ spawn. A call the station never judged
|
|
71
110
|
* (no gate ran) has nothing to re-establish and spawns. */
|
|
@@ -1,17 +1,183 @@
|
|
|
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";
|
|
11
|
+
import { ruleNamesOf } from "../../core/tool-registry.js";
|
|
10
12
|
import { normalizeReadRootCandidate } from "../../core/read-root-candidate.js";
|
|
11
13
|
import { argvOf, lexShellText } from "../../shell/lex.js";
|
|
12
14
|
function containmentDetails(paths) {
|
|
13
15
|
return { type: "readonly_out_of_root", code: "readonly_out_of_root", paths };
|
|
14
16
|
}
|
|
17
|
+
function unestablishedDetails() {
|
|
18
|
+
return { type: "read_unestablished", code: "read_unestablished" };
|
|
19
|
+
}
|
|
20
|
+
function findingDetails(finding, paths) {
|
|
21
|
+
return finding === "unestablishable" || finding === "declined" ? unestablishedDetails() : containmentDetails(paths);
|
|
22
|
+
}
|
|
23
|
+
const REMOVAL_COMMANDS = new Set(["rm", "rmdir"]);
|
|
24
|
+
const WRITES_BENEATH = {
|
|
25
|
+
cp: () => true,
|
|
26
|
+
mv: () => true,
|
|
27
|
+
rm: (s) => s.argv.some((w) => spellsOption(w.folded, "recursive", "[rR]")),
|
|
28
|
+
};
|
|
29
|
+
const isPattern = (f) => f.read.candidate.expands !== false && f.read.candidate.expands !== "unresolvable";
|
|
30
|
+
const RESHAPES_TREE = new Set(["mv"]);
|
|
31
|
+
const anyOptionWord = (words) => words.some((w) => w.startsWith("-"));
|
|
32
|
+
const UNPLACED_OPTIONS = {
|
|
33
|
+
rmdir: (ws) => ws.some((w) => spellsOption(w, "parents", "p")),
|
|
34
|
+
sed: (ws) => ws.some((w) => spellsOption(w, "file", "f")),
|
|
35
|
+
cp: anyOptionWord,
|
|
36
|
+
mv: anyOptionWord,
|
|
37
|
+
};
|
|
38
|
+
function runOrderOf(judged) {
|
|
39
|
+
const runs = [];
|
|
40
|
+
const plans = [];
|
|
41
|
+
let concurrent = false;
|
|
42
|
+
const walk = (j) => {
|
|
43
|
+
plans.push(j);
|
|
44
|
+
for (const s of j.segments) {
|
|
45
|
+
s.nested.forEach(walk);
|
|
46
|
+
runs.push(s);
|
|
47
|
+
if (j.plan.shape.backgrounded || s.segment.connector === "|")
|
|
48
|
+
concurrent = true;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
walk(judged);
|
|
52
|
+
return { runs, plans, concurrent };
|
|
53
|
+
}
|
|
54
|
+
function lineOrderPlaced(runs, concurrent) {
|
|
55
|
+
const reshapes = (s) => RESHAPES_TREE.has(s.segment.verb?.basename ?? "");
|
|
56
|
+
const writes = (s) => s.findings.some((f) => f.read.candidate.face === "write");
|
|
57
|
+
const stepsUp = (s) => s.findings.some((f) => f.read.candidate.spelled.split(/[\\/]/).includes(".."));
|
|
58
|
+
return runs.every((later, j) => runs.every((earlier, i) => i === j || (i > j && !concurrent) || (!reshapes(earlier) && !(writes(earlier) && stepsUp(later)))));
|
|
59
|
+
}
|
|
60
|
+
function writesPlacedOf(judged, boundary) {
|
|
61
|
+
const writeRoots = boundary.writeRoots;
|
|
62
|
+
if (writeRoots === undefined || writeRoots.length === 0)
|
|
63
|
+
return false;
|
|
64
|
+
const { runs, plans, concurrent } = runOrderOf(judged);
|
|
65
|
+
const placed = (s) => {
|
|
66
|
+
const verb = s.segment.verb?.basename ?? "";
|
|
67
|
+
if (s.segment.reentry === "shell" || s.segment.unreadProgramText !== undefined)
|
|
68
|
+
return false;
|
|
69
|
+
if (UNPLACED_OPTIONS[verb]?.(programArgs(s.segment).map((w) => w.spelled)) === true)
|
|
70
|
+
return false;
|
|
71
|
+
const beneath = WRITES_BENEATH[verb]?.(s.segment) === true;
|
|
72
|
+
for (const f of s.findings) {
|
|
73
|
+
if (f.read.candidate.face !== "write")
|
|
74
|
+
continue;
|
|
75
|
+
const target = f.read.target;
|
|
76
|
+
if (f.read.established === "unestablishable" || target === undefined || isPattern(f) || !withinAnyRoot(writeRoots, target))
|
|
77
|
+
return false;
|
|
78
|
+
if (beneath && denyRowAtOrBelow(f.read, boundary, !REMOVAL_COMMANDS.has(verb)))
|
|
79
|
+
return false;
|
|
80
|
+
if (REMOVAL_COMMANDS.has(verb) && writeRoots.some((root) => withinAnyRoot([target], root)))
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
return true;
|
|
84
|
+
};
|
|
85
|
+
return plans.every((p) => p.plan.unreadable === undefined) && runs.every(placed) && lineOrderPlaced(runs, concurrent);
|
|
86
|
+
}
|
|
87
|
+
function unplacedOptionsOf(judged) {
|
|
88
|
+
for (const s of judged.segments) {
|
|
89
|
+
const nested = s.nested.map(unplacedOptionsOf).find((u) => u !== undefined);
|
|
90
|
+
if (nested !== undefined)
|
|
91
|
+
return nested;
|
|
92
|
+
const verb = s.segment.verb?.basename;
|
|
93
|
+
if ((verb === "cp" || verb === "mv") && anyOptionWord(programArgs(s.segment).map((w) => w.spelled)))
|
|
94
|
+
return { program: verb };
|
|
95
|
+
}
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
async function writeGuardsOf(judged, boundary, placed, env, signal) {
|
|
99
|
+
const protection = boundary.writeProtectionJudge;
|
|
100
|
+
const guard = placed ? boundary.writeTargetGuard : undefined;
|
|
101
|
+
if (protection === undefined && guard === undefined)
|
|
102
|
+
return { unplaced: false, writeProtected: false };
|
|
103
|
+
const { runs, concurrent } = runOrderOf(judged);
|
|
104
|
+
const asKey = new Map();
|
|
105
|
+
const asSpelling = new Map();
|
|
106
|
+
const landingKeys = new Set();
|
|
107
|
+
const writtenBy = runs.map(() => []);
|
|
108
|
+
const add = (into, view, child, at) => {
|
|
109
|
+
if (!isAbsoluteForFamily(pathFamilyOf({ root: view }), view))
|
|
110
|
+
return;
|
|
111
|
+
const seen = into.get(view);
|
|
112
|
+
into.set(view, seen === undefined ? { child, at } : { child: seen.child && child, at: Math.max(seen.at, at) });
|
|
113
|
+
};
|
|
114
|
+
runs.forEach((s, at) => {
|
|
115
|
+
for (const f of s.findings) {
|
|
116
|
+
const written = f.read.written;
|
|
117
|
+
if (written === undefined || f.read.established === "unestablishable" || f.read.target === undefined)
|
|
118
|
+
continue;
|
|
119
|
+
const child = written.child === true;
|
|
120
|
+
const key = f.read.established === "canonical" || f.read.established === "expansion";
|
|
121
|
+
add(key ? asKey : asSpelling, f.read.target, child, at);
|
|
122
|
+
if (key && written.seat === "landing")
|
|
123
|
+
landingKeys.add(f.read.target);
|
|
124
|
+
if (written.seat === "landing")
|
|
125
|
+
for (const spelling of f.read.spellings)
|
|
126
|
+
add(asSpelling, spelling, child, at);
|
|
127
|
+
writtenBy[at].push(f.read.target, ...f.read.spellings);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
const probes = new Map();
|
|
131
|
+
const probe = (p) => {
|
|
132
|
+
let r = probes.get(p);
|
|
133
|
+
if (r === undefined) {
|
|
134
|
+
r = env.canonicalPath(p, signal).catch(() => undefined);
|
|
135
|
+
probes.set(p, r);
|
|
136
|
+
}
|
|
137
|
+
return r;
|
|
138
|
+
};
|
|
139
|
+
const parentOf = (view) => normalizeAbsPathLexically(`${view}/..`);
|
|
140
|
+
const writable = async (view, v) => {
|
|
141
|
+
const at = await probe(view);
|
|
142
|
+
if (at === undefined || at.ok)
|
|
143
|
+
return true;
|
|
144
|
+
if (at.error.code === "not_directory")
|
|
145
|
+
return false;
|
|
146
|
+
if (!v.child || at.error.code !== "not_found")
|
|
147
|
+
return true;
|
|
148
|
+
const dest = await probe(parentOf(view));
|
|
149
|
+
return dest === undefined || dest.ok || (dest.error.code !== "not_found" && dest.error.code !== "not_directory");
|
|
150
|
+
};
|
|
151
|
+
const changedBefore = (view, at) => {
|
|
152
|
+
const fold = (p) => p.normalize("NFC").toLowerCase();
|
|
153
|
+
const dir = fold(parentOf(view));
|
|
154
|
+
return writtenBy.some((paths, i) => i !== at && (i < at || concurrent) && paths.some((w) => withinAnyRoot([fold(w)], dir) || withinAnyRoot([dir], fold(w))));
|
|
155
|
+
};
|
|
156
|
+
const answer = async (ask) => {
|
|
157
|
+
try {
|
|
158
|
+
return (await ask()) === true;
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
const asked = [...[...asKey].map(([view, v]) => [view, v, true]), ...[...asSpelling].filter(([view]) => !landingKeys.has(view)).map(([view, v]) => [view, v, false])];
|
|
165
|
+
let unplaced = false;
|
|
166
|
+
let writeProtected = false;
|
|
167
|
+
for (const [view, v, key] of asked) {
|
|
168
|
+
if (!(await writable(view, v)) && !changedBefore(view, v.at))
|
|
169
|
+
continue;
|
|
170
|
+
const covered = protection === undefined ? false : await answer(() => protection(view, { ...(signal !== undefined ? { signal } : {}), ...(key ? { canonical: true } : {}) }));
|
|
171
|
+
const guarded = guard === undefined ? false : await answer(() => guard(view, signal));
|
|
172
|
+
if (covered === true)
|
|
173
|
+
writeProtected = true;
|
|
174
|
+
if (covered !== false || guarded !== false)
|
|
175
|
+
unplaced = true;
|
|
176
|
+
if (writeProtected)
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
return { unplaced, writeProtected };
|
|
180
|
+
}
|
|
15
181
|
const ADMISSION_MAX = 1024;
|
|
16
182
|
function shellCommandOf(args) {
|
|
17
183
|
const command = args?.command;
|
|
@@ -76,14 +242,17 @@ function refusalOf(tool, fold, roots, paths) {
|
|
|
76
242
|
const w = fold.witness;
|
|
77
243
|
if (fold.finding === "deny_hit" && w?.deny !== undefined) {
|
|
78
244
|
const target = w.read.target ?? w.deny.matchedView;
|
|
79
|
-
const
|
|
80
|
-
const
|
|
81
|
-
|
|
245
|
+
const write = w.deny.face === "write";
|
|
246
|
+
const core = readDenySentence({ target, pattern: w.deny.pattern, matchedView: w.deny.matchedView, ...(write ? { face: "write" } : {}) });
|
|
247
|
+
const sentence = `Error (${tool}): ${core} ${write ? WRITE_DENY_REFUSAL_NOTE : DENY_REFUSAL_NOTE}`;
|
|
248
|
+
return { sentence, ...(write ? {} : { details: violationDetails({ code: "read_path_denied", message: sentence, target, pattern: w.deny.pattern }) }) };
|
|
82
249
|
}
|
|
83
250
|
const account = findingAccount(fold, roots);
|
|
84
251
|
const sentence = `Error (${tool}): ${account}. ${BASH_READONLY_CONFINEMENT_NOTE}`;
|
|
85
|
-
return { sentence, details:
|
|
252
|
+
return { sentence, details: findingDetails(fold.finding, paths) };
|
|
86
253
|
}
|
|
254
|
+
const WRITE_DENY_REFUSAL_NOTE = "The boundary judges this command's write targets — its output redirections and the operands of mkdir, touch, rm, " +
|
|
255
|
+
"rmdir, mv, cp, tee and an in-place sed — against the write deny list; a denied write is refused here rather than escalated.";
|
|
87
256
|
const DENY_REFUSAL_NOTE = "The read boundary judges this command's written spelling and, where a filesystem can be consulted, the " +
|
|
88
257
|
"paths it would actually read; a deny-listed read is refused here rather than escalated.";
|
|
89
258
|
function verdictOf(tool, seat, fold, judged, boundary) {
|
|
@@ -91,17 +260,19 @@ function verdictOf(tool, seat, fold, judged, boundary) {
|
|
|
91
260
|
if (disposition === "vouch")
|
|
92
261
|
return { disposition, finding: fold.finding };
|
|
93
262
|
const paths = outsidePathsOf(judged);
|
|
263
|
+
const unestablished = fold.unestablished === true ? { unestablished: true } : {};
|
|
94
264
|
if (disposition === "refuse") {
|
|
95
265
|
const r = refusalOf(tool, fold, boundary.roots, paths);
|
|
96
|
-
return { disposition, finding: fold.finding, sentence: r.sentence, details: r.details };
|
|
266
|
+
return { disposition, finding: fold.finding, sentence: r.sentence, ...(r.details !== undefined ? { details: r.details } : {}) };
|
|
97
267
|
}
|
|
98
268
|
const grant = fold.finding === "outside_roots" ? readRootGrantCandidateOf(judged, boundary) : undefined;
|
|
99
269
|
const containment = fold.finding === "outside_roots" || fold.finding === "unestablishable";
|
|
100
270
|
return {
|
|
101
271
|
disposition,
|
|
102
272
|
finding: fold.finding,
|
|
273
|
+
...unestablished,
|
|
103
274
|
sentence: findingAccount(fold, boundary.roots),
|
|
104
|
-
...(disposition === "mandated" && containment ? { details:
|
|
275
|
+
...(disposition === "mandated" && containment ? { details: findingDetails(fold.finding, paths) } : {}),
|
|
105
276
|
...(grant !== undefined ? { readRootCandidate: { dir: grant.dir, ...(grant.covers !== undefined ? { covers: grant.covers } : {}), clearsThisAsk: true } } : {}),
|
|
106
277
|
};
|
|
107
278
|
}
|
|
@@ -137,17 +308,22 @@ async function applyAlsoInside(judged, boundary) {
|
|
|
137
308
|
});
|
|
138
309
|
return rewrite(judged);
|
|
139
310
|
}
|
|
140
|
-
async function runShellReadStation(tool, command, declared, seat, readers, establisher, signal) {
|
|
311
|
+
async function runShellReadStation(tool, command, declared, seat, readers, establisher, env, signal) {
|
|
141
312
|
const plan = planShellReads(command, declarationsOf(declared, readers));
|
|
142
313
|
const established = await establisher.establish(plan, declared, signal);
|
|
143
314
|
const boundary = { ...declared, roots: established.roots };
|
|
144
315
|
const judged = await applyAlsoInside(judgePlan(plan, established, boundary), boundary);
|
|
145
|
-
const
|
|
316
|
+
const judgedVerdict = verdictOf(tool, seat, foldShellReadFindings(judged), judged, boundary);
|
|
317
|
+
const unplaced = unplacedOptionsOf(judged);
|
|
318
|
+
const placed = writesPlacedOf(judged, boundary);
|
|
319
|
+
const guarded = judgedVerdict.disposition === "refuse" ? { unplaced: false, writeProtected: false } : await writeGuardsOf(judged, boundary, placed, env, signal);
|
|
320
|
+
const facts = guarded.writeProtected ? { ...judgedVerdict, writeProtected: true } : judgedVerdict;
|
|
321
|
+
const verdict = placed && !guarded.unplaced ? { ...facts, writesPlaced: true } : unplaced !== undefined ? { ...facts, unplacedOptions: unplaced } : facts;
|
|
146
322
|
return { plan, established, judged, boundary, verdict };
|
|
147
323
|
}
|
|
148
324
|
export async function judgeShellRead(input) {
|
|
149
325
|
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);
|
|
326
|
+
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
327
|
const offer = normalizeReadRootCandidate(run.verdict.readRootCandidate);
|
|
152
328
|
return {
|
|
153
329
|
plan: run.plan,
|
|
@@ -184,30 +360,33 @@ export function createShellReadStation(opts) {
|
|
|
184
360
|
}
|
|
185
361
|
admissions.set(toolCallId, a);
|
|
186
362
|
};
|
|
363
|
+
const memberOf = (call) => ruleNamesOf(call).find((n) => opts.tools.has(n));
|
|
187
364
|
const check = {
|
|
188
|
-
judge(
|
|
189
|
-
|
|
365
|
+
judge(call, args, cwd, signal, callOpts) {
|
|
366
|
+
const member = memberOf(call);
|
|
367
|
+
if (member === undefined)
|
|
190
368
|
return null;
|
|
191
369
|
const command = shellCommandOf(args);
|
|
192
370
|
if (command === undefined)
|
|
193
371
|
return null;
|
|
194
|
-
const boundary = opts.boundaryOf(
|
|
372
|
+
const boundary = opts.boundaryOf(member, cwd);
|
|
195
373
|
if (boundary === undefined)
|
|
196
374
|
return null;
|
|
197
|
-
const seat = callOpts.seat ?? opts.seatOf(
|
|
375
|
+
const seat = callOpts.seat ?? opts.seatOf(member);
|
|
198
376
|
return (async () => {
|
|
199
|
-
const run = await runShellReadStation(toolName, command, boundary, seat, readers, establisherFor(boundary.roots), signal);
|
|
377
|
+
const run = await runShellReadStation(call.toolName, command, boundary, seat, readers, establisherFor(boundary.roots), opts.env, signal);
|
|
200
378
|
if (run.verdict.disposition !== "refuse" && callOpts.toolCallId !== undefined)
|
|
201
|
-
remember(callOpts.toolCallId, { toolName, boundary: run.boundary, established: run.established, command });
|
|
379
|
+
remember(callOpts.toolCallId, { toolName: member, boundary: run.boundary, established: run.established, command });
|
|
202
380
|
return run.verdict;
|
|
203
381
|
})();
|
|
204
382
|
},
|
|
205
|
-
unanswered(
|
|
206
|
-
const
|
|
383
|
+
unanswered(call, cause) {
|
|
384
|
+
const toolName = call.toolName;
|
|
385
|
+
const seat = opts.seatOf(memberOf(call) ?? toolName);
|
|
207
386
|
const fold = { finding: "unestablishable" };
|
|
208
387
|
const disposition = decideShellRead("unestablishable", seat);
|
|
209
388
|
const account = `${cause}, so where this command reads could not be established ${NOT_AUTO_ALLOWED}`;
|
|
210
|
-
const details =
|
|
389
|
+
const details = unestablishedDetails();
|
|
211
390
|
if (disposition === "refuse")
|
|
212
391
|
return { disposition, finding: "unestablishable", sentence: `Error (${toolName}): ${account}. ${BASH_READONLY_CONFINEMENT_NOTE}`, details };
|
|
213
392
|
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`.
|