@sema-agent/core 5.31.0 → 5.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +96 -0
- package/dist/agents/cascade.d.ts +49 -1
- package/dist/agents/cascade.js +2 -2
- package/dist/agents/verify.d.ts +70 -4
- package/dist/agents/verify.js +62 -16
- package/dist/core/checkpoint-store.d.ts +95 -0
- package/dist/core/checkpoint-store.js +40 -0
- package/dist/core/hooks.d.ts +14 -6
- package/dist/core/hooks.js +14 -3
- package/dist/core/memory-engine/file-backend.d.ts +172 -22
- package/dist/core/memory-engine/file-backend.js +877 -79
- package/dist/core/memory-engine/memory-backend-contract.js +33 -0
- package/dist/core/runner/assemble-result.d.ts +7 -0
- package/dist/core/runner/assemble-result.js +1 -1
- package/dist/core/runner/prepare-acquire-reconcile.d.ts +72 -0
- package/dist/core/runner/prepare-acquire-reconcile.js +126 -0
- package/dist/core/runner/prepare-config-doors.d.ts +140 -0
- package/dist/core/runner/prepare-config-doors.js +250 -0
- package/dist/core/runner/prepare-safety-scan.d.ts +53 -0
- package/dist/core/runner/prepare-safety-scan.js +80 -0
- package/dist/core/runner/prepare-task.d.ts +28 -80
- package/dist/core/runner/prepare-task.js +102 -586
- package/dist/core/runner/prepare-workspace-restore.d.ts +102 -0
- package/dist/core/runner/prepare-workspace-restore.js +144 -0
- package/dist/core/runner/runtask.js +8 -2
- package/dist/core/tool-policy.d.ts +25 -0
- package/dist/core/types.d.ts +149 -13
- package/dist/index.d.ts +5 -4
- package/dist/index.js +2 -2
- package/dist/orchestration/workflow-governance.d.ts +6 -4
- package/dist/tools/fs/bash-readonly-classifier.d.ts +9 -3
- package/dist/tools/fs/bash-readonly-classifier.js +4 -1
- package/dist/tools/fs/fs-bash.d.ts +19 -3
- package/dist/tools/fs/fs-bash.js +26 -1
- package/dist/tools/fs/index.d.ts +27 -7
- package/dist/tools/fs/index.js +7 -2
- package/dist/tools/fs/read-deny.d.ts +66 -8
- package/dist/tools/fs/read-deny.js +75 -39
- package/dist/tools/fs/read-face.d.ts +24 -2
- package/dist/tools/fs/read-face.js +9 -0
- package/dist/tools/fs/search.js +2 -0
- package/package.json +1 -1
|
@@ -95,9 +95,15 @@ export interface BashReadonlyRootBoundary {
|
|
|
95
95
|
* demotes the command (ask, never auto-allow), independently of the roots — in-root operands are
|
|
96
96
|
* judged too. Returns the matched pattern, or null. TWO named residuals, both inherited from this
|
|
97
97
|
* classifier's declared purity (synchronous, zero I/O — RB-448/RB-451 state the same scope for the
|
|
98
|
-
* containment half): ① operand TARGET matching only — no ancestor intersection
|
|
99
|
-
*
|
|
100
|
-
*
|
|
98
|
+
* containment half): ① operand TARGET matching only — no ancestor intersection: the judge sees
|
|
99
|
+
* the operand's own resolved spelling, never its subtree. The RECURSIVE-reach half of that
|
|
100
|
+
* residual has since been narrowed (backlog #222): a listed recursive/expanding verb's path
|
|
101
|
+
* operand under this wired seat rides {@link CompoundReadonlyVerdict.recursiveReadPaths}
|
|
102
|
+
* (⊂ undecidedPaths), so `grep -r x ~/` no longer auto-allows — it demotes to ask through the
|
|
103
|
+
* undecided contract. What REMAINS of ① is the form table's stated open set (see the KNOWN OPEN
|
|
104
|
+
* SET note at {@link RECURSIVE_READ_FORMS}: a traversal verb absent there that a deployment
|
|
105
|
+
* allowlists classifies by its ordinary form and can auto-run a traversal unjudged);
|
|
106
|
+
* ② LEXICAL only — an in-root symlink whose target is a
|
|
101
107
|
* guarded path reads as its innocent spelling here, exactly as it does for the containment half
|
|
102
108
|
* (the enforcing/canonicalizing recheck is the bash_readonly leg's job via checkedPaths; the
|
|
103
109
|
* classify auto-allow lane has no I/O seat by contract). The structured read faces judge BOTH
|
|
@@ -109,6 +109,7 @@ function foldQuoteRemovalToken(tok) {
|
|
|
109
109
|
}
|
|
110
110
|
return out;
|
|
111
111
|
}
|
|
112
|
+
const POLL_LOOP_CAUSE_MAX_PATHS = 3;
|
|
112
113
|
export function formatOutOfRootReadApprovalOption(directory) {
|
|
113
114
|
const trimmed = directory.replace(/[/\\]+$/, "");
|
|
114
115
|
const cut = Math.max(trimmed.lastIndexOf("/"), trimmed.lastIndexOf("\\"));
|
|
@@ -843,7 +844,9 @@ export function classifyBoundedReadonlyPollLoop(command, allow, boundary) {
|
|
|
843
844
|
if (verdict.reason !== undefined)
|
|
844
845
|
return verdict.reason;
|
|
845
846
|
if (verdict.recursiveReadPaths !== undefined) {
|
|
846
|
-
|
|
847
|
+
const roots = verdict.recursiveReadPaths;
|
|
848
|
+
const shown = roots.slice(0, POLL_LOOP_CAUSE_MAX_PATHS);
|
|
849
|
+
return `this lexical check cannot bound what a recursive read form in the loop body traverses — ${roots.length} path(s) taken as traversal roots: ${shown.join(", ")}${roots.length > shown.length ? `, and ${roots.length - shown.length} more` : ""}`;
|
|
847
850
|
}
|
|
848
851
|
if (verdict.undecidedPaths !== undefined) {
|
|
849
852
|
return `the loop body carries an unexpanded glob (${verdict.undecidedPaths.join(", ")}) — what a REPEATED read touches is decided at run time, so it is not auto-allowed`;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AgentTool, ExecutionEnv } from "../../internal/harness-types.js";
|
|
2
2
|
import { type TaskRegistry } from "../../core/task-registry.js";
|
|
3
3
|
import { type CwdRef } from "./fs-shared.js";
|
|
4
|
+
import type { ReversibilityVerdict } from "../../core/types.js";
|
|
4
5
|
import { type BashReadonlyRootBoundary } from "./bash-readonly-classifier.js";
|
|
5
6
|
/**
|
|
6
7
|
* design/80 D-2 (part-1): a parsed-command classifier for the `bash` tool, exposed as a
|
|
@@ -36,10 +37,25 @@ import { type BashReadonlyRootBoundary } from "./bash-readonly-classifier.js";
|
|
|
36
37
|
* allowlisted readers + literal `sleep`, the same read boundary, everything else banned; the full
|
|
37
38
|
* safety argument lives on that function). Additive by construction: the plain verdict is tried
|
|
38
39
|
* first and is never overridden.
|
|
40
|
+
*
|
|
41
|
+
* backlog #239 — the RECURSIVE-READ demotion returns a STRUCTURED cause alongside the verdict. That
|
|
42
|
+
* demotion is the one arm whose reason is genuinely unrecoverable downstream: a recursive read is
|
|
43
|
+
* refused for what the traversal REACHES, and the reached set is by definition not in the command text
|
|
44
|
+
* an approval card shows. The cause is minted from the SAME classification pass that produced the
|
|
45
|
+
* verdict, so nothing re-parses the command to recover it.
|
|
46
|
+
*
|
|
47
|
+
* It is a `cause` (code + operand arrays), not a `reason` sentence, and that is a correction rather than
|
|
48
|
+
* a preference: every sentence tried here was true of one call and false of another — "a recursive read
|
|
49
|
+
* of X" is false for the zero-operand over-fire, "operand the check treated as a root" is false when the
|
|
50
|
+
* check SYNTHESIZES the root, and any of them, flattened into one length-capped string, silently dropped
|
|
51
|
+
* whichever family sorted last. The consumer now writes the sentence, from a code that cannot go stale
|
|
52
|
+
* and arrays that cannot be truncated into a lie.
|
|
53
|
+
*
|
|
54
|
+
* The other non-reversible arms stay bare on purpose — their cause is legible in the command itself (a
|
|
55
|
+
* write verb, a pipe, an unlisted name), and manufacturing an account for them would put text on a card
|
|
56
|
+
* that adds nothing the reader cannot already see.
|
|
39
57
|
*/
|
|
40
|
-
export declare function bashReversibilityProbe(allow?: readonly string[], boundary?: BashReadonlyRootBoundary | (() => BashReadonlyRootBoundary | undefined)): (args: unknown) =>
|
|
41
|
-
reversible: boolean;
|
|
42
|
-
};
|
|
58
|
+
export declare function bashReversibilityProbe(allow?: readonly string[], boundary?: BashReadonlyRootBoundary | (() => BashReadonlyRootBoundary | undefined)): (args: unknown) => ReversibilityVerdict;
|
|
43
59
|
/**
|
|
44
60
|
* design/199 D-6 — the FULL shell's contract id, single-sourced: both shell faces share the wire
|
|
45
61
|
* name "Bash", so this id is the ONE structural discriminator between the write-capable shell and
|
package/dist/tools/fs/fs-bash.js
CHANGED
|
@@ -10,7 +10,18 @@ import { imageMagicMatches, withinAnyRoot } from "./safety.js";
|
|
|
10
10
|
import { isRemoteExecutionEnv, hasDestroy, isIsolated } from "../../core/remote-env.js";
|
|
11
11
|
import { ghRateLimitHint } from "./gh-rate-limit.js";
|
|
12
12
|
import { resolveBashTimeoutCaps, bashTimeoutCapsSec, bashTimeoutArgRefusal, bashTimeoutParamDescription, envErrorDetail, bashMaxOutputChars, clipShellOutput, writeShellOverflowFile, createShellOverflowSpoolFence, shellRecoveryHint, CWD_SENTINEL, BASH_READONLY_CONFINEMENT_NOTE, } from "./fs-shared.js";
|
|
13
|
+
import { PROBE_CAUSE_PATH_MAX } from "../../core/checkpoint-store.js";
|
|
14
|
+
import { inlineUntrusted } from "../../core/untrusted-text.js";
|
|
13
15
|
import { BASH_READONLY_DEFAULT_ALLOW, coarseReadonlyCheck, classifyBoundedReadonlyPollLoop, classifyCompoundReadonlyDetailed, classifySimpleCommandReadBoundary, NOT_AUTO_ALLOWED, } from "./bash-readonly-classifier.js";
|
|
16
|
+
const RECURSIVE_CAUSE_MAX_PATHS = 3;
|
|
17
|
+
const RECURSIVE_READ_CAUSE_CODE = "shell.recursive_read_unbounded";
|
|
18
|
+
function operandFamily(paths) {
|
|
19
|
+
const enc = (s) => s.replace(/%/g, "%25").replace(/;/g, "%3B").replace(/:/g, "%3A");
|
|
20
|
+
return {
|
|
21
|
+
shown: paths.slice(0, RECURSIVE_CAUSE_MAX_PATHS).map((p) => enc(inlineUntrusted(p, PROBE_CAUSE_PATH_MAX))),
|
|
22
|
+
total: paths.length,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
14
25
|
export function bashReversibilityProbe(allow, boundary) {
|
|
15
26
|
const allowSet = new Set(allow ?? BASH_READONLY_DEFAULT_ALLOW);
|
|
16
27
|
return (args) => {
|
|
@@ -23,8 +34,22 @@ export function bashReversibilityProbe(allow, boundary) {
|
|
|
23
34
|
const resolved = typeof boundary === "function" ? boundary() : boundary;
|
|
24
35
|
const detailed = classifyCompoundReadonlyDetailed(command, allowSet, resolved);
|
|
25
36
|
if (detailed.reason === undefined) {
|
|
26
|
-
if (detailed.undecidedPaths !== undefined && detailed.undecidedPaths.length > 0)
|
|
37
|
+
if (detailed.undecidedPaths !== undefined && detailed.undecidedPaths.length > 0) {
|
|
38
|
+
const recursive = detailed.recursiveReadPaths;
|
|
39
|
+
if (recursive !== undefined && recursive.length > 0) {
|
|
40
|
+
const recursiveSet = new Set(recursive);
|
|
41
|
+
const others = detailed.undecidedPaths.filter((p) => !recursiveSet.has(p));
|
|
42
|
+
return {
|
|
43
|
+
reversible: false,
|
|
44
|
+
cause: {
|
|
45
|
+
code: RECURSIVE_READ_CAUSE_CODE,
|
|
46
|
+
roots: operandFamily(recursive),
|
|
47
|
+
...(others.length > 0 ? { further: operandFamily(others) } : {}),
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
27
51
|
return { reversible: false };
|
|
52
|
+
}
|
|
28
53
|
return { reversible: true };
|
|
29
54
|
}
|
|
30
55
|
return { reversible: classifyBoundedReadonlyPollLoop(command, allowSet, resolved) === undefined };
|
package/dist/tools/fs/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AgentTool, ExecutionEnv } from "../../internal/harness-types.js";
|
|
2
|
-
import type
|
|
2
|
+
import { type BeforeWriteHook } from "../../core/types.js";
|
|
3
3
|
import { type TaskRegistry } from "../../core/task-registry.js";
|
|
4
4
|
import { type ReadFileState } from "./safety.js";
|
|
5
5
|
import type { PdfModelCapabilities } from "./pdf.js";
|
|
@@ -120,18 +120,38 @@ export interface HandsToolkitOptions {
|
|
|
120
120
|
monitorToolActive?: boolean;
|
|
121
121
|
/** design/199 件B — ADDITIONS to the built-in sensitive-path READ deny set
|
|
122
122
|
* ({@link import("./read-deny.js").READ_FACE_DEFAULT_DENY_ENTRIES}), judged by the structured read
|
|
123
|
-
* faces (Read/Grep/Glob/RepoMap and their traversals) in BOTH containment modes. Add-only
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
* (
|
|
123
|
+
* faces (Read/Grep/Glob/RepoMap and their traversals) in BOTH containment modes. Add-only:
|
|
124
|
+
* additions union with the active built-ins, `[]` ≡ absent (union identity), and additions can
|
|
125
|
+
* never remove a built-in — the one removal channel is the deployment's built-in configuration
|
|
126
|
+
* ({@link readDenyBuiltinTiers} / {@link readDenyBuiltinExclude}, #245 revision of D-4). Bad entry
|
|
127
|
+
* shapes throw at wiring time (#123). The write faces are untouched (their guard is
|
|
128
|
+
* createSensitivePathPolicy). */
|
|
127
129
|
readDenyPatterns?: readonly ReadDenyEntry[];
|
|
130
|
+
/** #245 — the deployment's built-in-table TIER selection ({@link import("./read-deny.js").ReadDenyBuiltinConfig}
|
|
131
|
+
* `tiers` key): EXACTLY the listed tiers of {@link import("./read-deny.js").READ_FACE_BUILTIN_DENY_TABLE}
|
|
132
|
+
* are active (`[]` = none — explicit and legal); absent = the default selection
|
|
133
|
+
* ({@link import("./read-deny.js").READ_DENY_DEFAULT_TIERS} — every tier except `shell-history`).
|
|
134
|
+
* Composes with {@link readDenyBuiltinExclude} (tier selection first, then row removal) and never
|
|
135
|
+
* touches the additions seats. Unknown tier names throw at wiring time (#123). Deployment seat
|
|
136
|
+
* ONLY — deliberately not a TaskSpec key, so no task can widen below its deployment. */
|
|
137
|
+
readDenyBuiltinTiers?: readonly string[];
|
|
138
|
+
/** #245 — per-row removal from the built-in table by STABLE NAME (= the row's canonical pattern
|
|
139
|
+
* text, e.g. `".ssh"`, `".config/gcloud"`), applied after {@link readDenyBuiltinTiers} selection.
|
|
140
|
+
* Naming a row of an inactive tier is a satisfied intent (no-op); a name matching NO table row
|
|
141
|
+
* throws at wiring time (#123 — never silently ignored). Deployment seat ONLY, same clamp
|
|
142
|
+
* argument as the tiers key. */
|
|
143
|
+
readDenyBuiltinExclude?: readonly string[];
|
|
128
144
|
/** design/199 件A — the READ-face containment state for the structured read faces
|
|
129
145
|
* ({@link import("./read-face.js").ReadFace}; resolved through the SAME
|
|
130
146
|
* {@link import("./read-face.js").resolveReadFace} order prepare-task uses). Absent ⇒ the
|
|
131
147
|
* resolution order's default = "roots" (D-1b: the engine never opens implicitly). "open" skips
|
|
132
148
|
* ONLY the roots containment judgment — the deny set, the UNC out-of-set refusal and the
|
|
133
|
-
* special-file type gates run in both faces (§2.0).
|
|
134
|
-
* (
|
|
149
|
+
* special-file type gates run in both faces (§2.0). Beside `readOnly: true` the mount wins:
|
|
150
|
+
* this seat is deps-shaped (a deployment default, not a per-call assertion), so its "open"
|
|
151
|
+
* CLAMPS to "roots" and the clamp is ANNOUNCED (`config.read_face_deployment_clamped`, through
|
|
152
|
+
* `onNotice` / `console.warn`) — the loud refusal belongs to the TASK seat (`TaskSpec.readFace`),
|
|
153
|
+
* which is the one genuine per-call contradiction (#237; stale "refused loudly" wording here
|
|
154
|
+
* predated the 96ef89d seat distinction). Never affects the write faces. */
|
|
135
155
|
readFace?: "open" | "roots";
|
|
136
156
|
}
|
|
137
157
|
/**
|
package/dist/tools/fs/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { deliverEngineNotice } from "../../core/types.js";
|
|
1
2
|
import { createTaskOutputTool, createTaskStopTool } from "../../core/task-registry.js";
|
|
2
3
|
import { hasBackgroundShell } from "../../core/background-shell.js";
|
|
3
4
|
import {} from "./safety.js";
|
|
@@ -12,7 +13,7 @@ export * from "./fs-bash.js";
|
|
|
12
13
|
export * from "./read-deny.js";
|
|
13
14
|
import { BASH_READONLY_DEFAULT_ALLOW, } from "./bash-readonly-classifier.js";
|
|
14
15
|
import { compileReadDeny } from "./read-deny.js";
|
|
15
|
-
import { resolveReadFace } from "./read-face.js";
|
|
16
|
+
import { deploymentReadFaceClampNotice, resolveReadFace } from "./read-face.js";
|
|
16
17
|
export * from "./read-face.js";
|
|
17
18
|
import {} from "./fs-shared.js";
|
|
18
19
|
import { createReadFileTool } from "./fs-read.js";
|
|
@@ -33,12 +34,16 @@ export function createHandsToolkit(env, readFileState, rootCanonical, opts = {})
|
|
|
33
34
|
scope: c.principal ?? opts.taskScope,
|
|
34
35
|
...(opts.sessionId !== undefined ? { sessionId: opts.sessionId } : {}),
|
|
35
36
|
}, env);
|
|
36
|
-
const readDeny = compileReadDeny(opts.readDenyPatterns ?? [], "HandsToolkitOptions.readDenyPatterns"
|
|
37
|
+
const readDeny = compileReadDeny(opts.readDenyPatterns ?? [], "HandsToolkitOptions.readDenyPatterns", {
|
|
38
|
+
...(opts.readDenyBuiltinTiers !== undefined ? { tiers: opts.readDenyBuiltinTiers } : {}),
|
|
39
|
+
...(opts.readDenyBuiltinExclude !== undefined ? { exclude: opts.readDenyBuiltinExclude } : {}),
|
|
40
|
+
});
|
|
37
41
|
const readFace = resolveReadFace({
|
|
38
42
|
depsReadFace: opts.readFace,
|
|
39
43
|
readOnlyMount: readOnly,
|
|
40
44
|
orgGoverned: false,
|
|
41
45
|
fullShellReachable: includeShell && !readOnly,
|
|
46
|
+
onDeploymentClamp: () => deliverEngineNotice(opts.onNotice, deploymentReadFaceClampNotice()),
|
|
42
47
|
});
|
|
43
48
|
const tools = [
|
|
44
49
|
createReadFileTool(env, readFileState, rootCanonical, readOnly ? undefined : cwdRef, readFaceRoots, opts.readImageDownsampler, opts.pdfModelCapabilities, bgOutputReadExemption, opts.readCyberReminder, readDeny, readFace),
|
|
@@ -52,9 +52,30 @@ export interface ReadDenyMatcher {
|
|
|
52
52
|
readonly rgProbeGlobs: readonly ReadDenyRgGlob[];
|
|
53
53
|
}
|
|
54
54
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
55
|
+
* The closed set of built-in tiers (#245). A tier is the deployment's selection unit
|
|
56
|
+
* ({@link ReadDenyBuiltinConfig.tiers}); every {@link READ_FACE_BUILTIN_DENY_TABLE} row belongs to
|
|
57
|
+
* exactly one. Unknown tier names refuse loudly (#123) — this array is the validation source and the
|
|
58
|
+
* admin-face vocabulary, never re-spelled downstream.
|
|
59
|
+
*/
|
|
60
|
+
export declare const READ_DENY_BUILTIN_TIERS: readonly ["credentials", "shell-history", "browser", "wallet"];
|
|
61
|
+
/** One member of {@link READ_DENY_BUILTIN_TIERS}. */
|
|
62
|
+
export type ReadDenyBuiltinTier = (typeof READ_DENY_BUILTIN_TIERS)[number];
|
|
63
|
+
/**
|
|
64
|
+
* One row of the built-in table: the canonical deny pattern (also the row's STABLE NAME — the
|
|
65
|
+
* `readDenyBuiltinExclude` key; renaming a pattern is a semantic change and deliberately invalidates
|
|
66
|
+
* configs naming the old spelling, loudly) and the tier it belongs to.
|
|
67
|
+
*/
|
|
68
|
+
export interface ReadDenyBuiltinRow {
|
|
69
|
+
readonly pattern: string;
|
|
70
|
+
readonly tier: ReadDenyBuiltinTier;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* READ_FACE_BUILTIN_DENY_TABLE — the FULL built-in table with tier metadata (#245: the material
|
|
74
|
+
* basis of the deployment admin face — what an org administrator "sees" before selecting tiers /
|
|
75
|
+
* excluding rows). Activation is a separate fact: the default face is the table under
|
|
76
|
+
* {@link READ_DENY_DEFAULT_TIERS} (shell-history rows exist here but are OFF by default).
|
|
77
|
+
* The dotenv family is deliberately NOT here — workspace `.env` files are working material for the
|
|
78
|
+
* tasks this engine runs (the design's `outside_workspace` tier was also ruled OUT).
|
|
58
79
|
*
|
|
59
80
|
* Each row states its tradeoff. Rows are matched as segment runs anywhere in the path, so a repo
|
|
60
81
|
* fixture spelled `fixtures/.ssh/id_rsa` is refused too — deliberate: the refusal names the pattern,
|
|
@@ -67,6 +88,39 @@ export interface ReadDenyMatcher {
|
|
|
67
88
|
* `.mcp.json` (agent config is routinely read for debugging); the engine's own data root
|
|
68
89
|
* (transcript integrity policy owns it with an ask on the write face).
|
|
69
90
|
*/
|
|
91
|
+
export declare const READ_FACE_BUILTIN_DENY_TABLE: readonly ReadDenyBuiltinRow[];
|
|
92
|
+
/**
|
|
93
|
+
* The DEFAULT tier selection (#245 ruling ②): every tier EXCEPT `shell-history` — history files are
|
|
94
|
+
* unrestricted unless a deployment opts the tier in. Derived, not re-listed: a future tier added to
|
|
95
|
+
* the closed set is default-ON unless this expression is deliberately revisited.
|
|
96
|
+
*/
|
|
97
|
+
export declare const READ_DENY_DEFAULT_TIERS: readonly ReadDenyBuiltinTier[];
|
|
98
|
+
/**
|
|
99
|
+
* The deployment's built-in-table configuration (#245 — deployment seat ONLY: RunnerDeps /
|
|
100
|
+
* HandsToolkitOptions; deliberately NOT a TaskSpec key and NOT in the governed workflow whitelist,
|
|
101
|
+
* so no task or governed script can widen below its deployment).
|
|
102
|
+
* - `tiers` — EXACTLY-these tier selection: listed tiers are active, unlisted ones are not
|
|
103
|
+
* (`[]` = no built-in tiers at all — an explicit, legal posture; absent = {@link READ_DENY_DEFAULT_TIERS}).
|
|
104
|
+
* - `exclude` — per-row removal by canonical pattern name, applied AFTER tier selection.
|
|
105
|
+
* Unknown tier names / unknown row names refuse loudly (#123) — never silently ignored.
|
|
106
|
+
*/
|
|
107
|
+
export interface ReadDenyBuiltinConfig {
|
|
108
|
+
tiers?: readonly string[];
|
|
109
|
+
exclude?: readonly string[];
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Resolve the ACTIVE built-in rows under a deployment configuration (validated, loud — #123's three
|
|
113
|
+
* bad-value states all throw: garbage shapes, unknown tier names, unknown row names). The one
|
|
114
|
+
* resolution used by {@link compileReadDeny}; exported so an admin face can preview the effective
|
|
115
|
+
* table under a candidate configuration with the engine's own rules.
|
|
116
|
+
*/
|
|
117
|
+
export declare function resolveReadDenyBuiltins(config?: ReadDenyBuiltinConfig): readonly ReadDenyBuiltinRow[];
|
|
118
|
+
/**
|
|
119
|
+
* READ_FACE_DEFAULT_DENY_ENTRIES — the DEFAULT-ACTIVE built-in set: {@link READ_FACE_BUILTIN_DENY_TABLE}
|
|
120
|
+
* under {@link READ_DENY_DEFAULT_TIERS} (#245: shell-history rows are in the table, not in this face).
|
|
121
|
+
* Kept as the historical name for consumers rendering "what does an unconfigured deployment refuse";
|
|
122
|
+
* the tier-annotated table is the admin-face export.
|
|
123
|
+
*/
|
|
70
124
|
export declare const READ_FACE_DEFAULT_DENY_ENTRIES: readonly ReadDenyEntry[];
|
|
71
125
|
/** A compiled segment-run pattern (shared engine — the write guard delegates here too). */
|
|
72
126
|
export interface CompiledSegmentPattern {
|
|
@@ -102,9 +156,13 @@ export declare function matchSegmentPatterns(path: string, compiled: readonly Co
|
|
|
102
156
|
*/
|
|
103
157
|
export declare function persistedReadDenyEntryProblem(e: unknown): string | null;
|
|
104
158
|
/**
|
|
105
|
-
* Compile the read-face deny judge: built-in
|
|
106
|
-
*
|
|
107
|
-
* cannot observe a "replace" intent, and union-with-empty is the identity, not a silent fallback)
|
|
108
|
-
*
|
|
159
|
+
* Compile the read-face deny judge: ACTIVE built-in rows ∪ additions. Additions are add-only at every
|
|
160
|
+
* task layer (`[]` additions ≡ absent, deliberately a no-op rather than a refusal: an array API
|
|
161
|
+
* cannot observe a "replace" intent, and union-with-empty is the identity, not a silent fallback);
|
|
162
|
+
* the built-in half is selected by the DEPLOYMENT's `builtin` config (#245 — absent = the default
|
|
163
|
+
* tier selection, see {@link resolveReadDenyBuiltins}). A deployment that turned a tier off can still
|
|
164
|
+
* re-add single patterns through the additions seats — they then ride as ordinary additions.
|
|
165
|
+
* Bad entry shapes / zero-segment patterns / unknown tier or row names throw loudly, naming the
|
|
166
|
+
* layer or knob (#123).
|
|
109
167
|
*/
|
|
110
|
-
export declare function compileReadDeny(additions?: readonly ReadDenyEntry[], layer?: string): ReadDenyMatcher;
|
|
168
|
+
export declare function compileReadDeny(additions?: readonly ReadDenyEntry[], layer?: string, builtin?: ReadDenyBuiltinConfig): ReadDenyMatcher;
|
|
@@ -1,40 +1,76 @@
|
|
|
1
|
-
export const
|
|
2
|
-
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
".
|
|
8
|
-
".
|
|
9
|
-
".
|
|
10
|
-
".
|
|
11
|
-
".
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
".
|
|
15
|
-
".config
|
|
16
|
-
".
|
|
17
|
-
".
|
|
18
|
-
".
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
".
|
|
22
|
-
"
|
|
23
|
-
"Library/Application Support/
|
|
24
|
-
"Library/
|
|
25
|
-
"
|
|
26
|
-
".config/
|
|
27
|
-
".
|
|
28
|
-
"
|
|
29
|
-
"AppData/Local/
|
|
30
|
-
"AppData/
|
|
31
|
-
"
|
|
32
|
-
".
|
|
33
|
-
".
|
|
34
|
-
"
|
|
35
|
-
"Library/Application Support/
|
|
36
|
-
"wallet
|
|
1
|
+
export const READ_DENY_BUILTIN_TIERS = ["credentials", "shell-history", "browser", "wallet"];
|
|
2
|
+
export const READ_FACE_BUILTIN_DENY_TABLE = [
|
|
3
|
+
{ pattern: ".ssh", tier: "credentials" },
|
|
4
|
+
{ pattern: "id_rsa*", tier: "credentials" },
|
|
5
|
+
{ pattern: "id_ed25519*", tier: "credentials" },
|
|
6
|
+
{ pattern: "id_ecdsa*", tier: "credentials" },
|
|
7
|
+
{ pattern: ".gnupg", tier: "credentials" },
|
|
8
|
+
{ pattern: ".aws", tier: "credentials" },
|
|
9
|
+
{ pattern: ".config/gcloud", tier: "credentials" },
|
|
10
|
+
{ pattern: ".azure", tier: "credentials" },
|
|
11
|
+
{ pattern: ".kube", tier: "credentials" },
|
|
12
|
+
{ pattern: ".netrc", tier: "credentials" },
|
|
13
|
+
{ pattern: "_netrc", tier: "credentials" },
|
|
14
|
+
{ pattern: ".git-credentials", tier: "credentials" },
|
|
15
|
+
{ pattern: ".docker/config.json", tier: "credentials" },
|
|
16
|
+
{ pattern: ".config/gh", tier: "credentials" },
|
|
17
|
+
{ pattern: ".npmrc", tier: "credentials" },
|
|
18
|
+
{ pattern: ".pypirc", tier: "credentials" },
|
|
19
|
+
{ pattern: ".local/share/keyrings", tier: "credentials" },
|
|
20
|
+
{ pattern: "Library/Keychains", tier: "credentials" },
|
|
21
|
+
{ pattern: ".bash_history", tier: "shell-history" },
|
|
22
|
+
{ pattern: ".zsh_history", tier: "shell-history" },
|
|
23
|
+
{ pattern: "Library/Application Support/Google/Chrome", tier: "browser" },
|
|
24
|
+
{ pattern: "Library/Application Support/Firefox", tier: "browser" },
|
|
25
|
+
{ pattern: "Library/Safari", tier: "browser" },
|
|
26
|
+
{ pattern: ".config/google-chrome", tier: "browser" },
|
|
27
|
+
{ pattern: ".config/chromium", tier: "browser" },
|
|
28
|
+
{ pattern: ".mozilla/firefox", tier: "browser" },
|
|
29
|
+
{ pattern: "AppData/Local/Google/Chrome/User Data", tier: "browser" },
|
|
30
|
+
{ pattern: "AppData/Local/Microsoft/Edge/User Data", tier: "browser" },
|
|
31
|
+
{ pattern: "AppData/Roaming/Mozilla/Firefox", tier: "browser" },
|
|
32
|
+
{ pattern: ".bitcoin", tier: "wallet" },
|
|
33
|
+
{ pattern: ".ethereum", tier: "wallet" },
|
|
34
|
+
{ pattern: ".electrum", tier: "wallet" },
|
|
35
|
+
{ pattern: "Library/Application Support/Exodus", tier: "wallet" },
|
|
36
|
+
{ pattern: "Library/Application Support/Ledger Live", tier: "wallet" },
|
|
37
|
+
{ pattern: "wallet.dat", tier: "wallet" },
|
|
37
38
|
];
|
|
39
|
+
export const READ_DENY_DEFAULT_TIERS = READ_DENY_BUILTIN_TIERS.filter((t) => t !== "shell-history");
|
|
40
|
+
export function resolveReadDenyBuiltins(config) {
|
|
41
|
+
let activeTiers;
|
|
42
|
+
if (config?.tiers === undefined) {
|
|
43
|
+
activeTiers = new Set(READ_DENY_DEFAULT_TIERS);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
if (!Array.isArray(config.tiers)) {
|
|
47
|
+
throw new Error(`readDenyBuiltinTiers: expected an array of tier names, got ${JSON.stringify(config.tiers)}.`);
|
|
48
|
+
}
|
|
49
|
+
for (const t of config.tiers) {
|
|
50
|
+
if (typeof t !== "string" || !READ_DENY_BUILTIN_TIERS.includes(t)) {
|
|
51
|
+
throw new Error(`readDenyBuiltinTiers: unknown tier ${JSON.stringify(t)} — known tiers: ${READ_DENY_BUILTIN_TIERS.join(", ")}.`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
activeTiers = new Set(config.tiers);
|
|
55
|
+
}
|
|
56
|
+
let excluded;
|
|
57
|
+
if (config?.exclude === undefined) {
|
|
58
|
+
excluded = new Set();
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
if (!Array.isArray(config.exclude)) {
|
|
62
|
+
throw new Error(`readDenyBuiltinExclude: expected an array of built-in row names (canonical pattern texts), got ${JSON.stringify(config.exclude)}.`);
|
|
63
|
+
}
|
|
64
|
+
for (const name of config.exclude) {
|
|
65
|
+
if (typeof name !== "string" || !READ_FACE_BUILTIN_DENY_TABLE.some((r) => r.pattern === name)) {
|
|
66
|
+
throw new Error(`readDenyBuiltinExclude: ${JSON.stringify(name)} names no built-in row — row names are the canonical pattern texts of READ_FACE_BUILTIN_DENY_TABLE (e.g. ".ssh", ".config/gcloud").`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
excluded = new Set(config.exclude);
|
|
70
|
+
}
|
|
71
|
+
return READ_FACE_BUILTIN_DENY_TABLE.filter((r) => activeTiers.has(r.tier) && !excluded.has(r.pattern));
|
|
72
|
+
}
|
|
73
|
+
export const READ_FACE_DEFAULT_DENY_ENTRIES = resolveReadDenyBuiltins().map((r) => r.pattern);
|
|
38
74
|
function asciiLower(s) {
|
|
39
75
|
let out = "";
|
|
40
76
|
for (let i = 0; i < s.length; i++) {
|
|
@@ -107,7 +143,7 @@ export function persistedReadDenyEntryProblem(e) {
|
|
|
107
143
|
return "pattern is not in canonical a/b form";
|
|
108
144
|
return null;
|
|
109
145
|
}
|
|
110
|
-
export function compileReadDeny(additions = [], layer = "additions") {
|
|
146
|
+
export function compileReadDeny(additions = [], layer = "additions", builtin) {
|
|
111
147
|
const normalized = [];
|
|
112
148
|
const seen = new Set();
|
|
113
149
|
const push = (e) => {
|
|
@@ -117,8 +153,8 @@ export function compileReadDeny(additions = [], layer = "additions") {
|
|
|
117
153
|
seen.add(key);
|
|
118
154
|
normalized.push(e);
|
|
119
155
|
};
|
|
120
|
-
for (const
|
|
121
|
-
push(normalizeEntry(
|
|
156
|
+
for (const row of resolveReadDenyBuiltins(builtin))
|
|
157
|
+
push(normalizeEntry(row.pattern, "built-in"));
|
|
122
158
|
for (const entry of additions)
|
|
123
159
|
push(normalizeEntry(entry, layer));
|
|
124
160
|
const compiled = normalized.map((e) => {
|
|
@@ -5,8 +5,9 @@ export type ReadFace = "open" | "roots";
|
|
|
5
5
|
/** Loud value gate (#123): a `readFace` seat carries exactly "open" | "roots" — anything else
|
|
6
6
|
* (case variants, truthy garbage) refuses at wiring time, never folds to a default. Exported
|
|
7
7
|
* module-internally (not on the package surface) so prepare's unconditional config-guard leg can
|
|
8
|
-
* screen the seats on runs that never mount hands
|
|
9
|
-
*
|
|
8
|
+
* screen the seats on runs that never mount hands early and unconditionally (since #242 the
|
|
9
|
+
* resolver itself ALSO runs on every hands-less leg; this screen still fires first, so a garbage
|
|
10
|
+
* value refuses before any leg-shape branching). */
|
|
10
11
|
export declare function assertReadFaceValue(v: unknown, seat: string): ReadFace | undefined;
|
|
11
12
|
/** Inputs to {@link resolveReadFace} — all structural/declaration facts, never permission modes
|
|
12
13
|
* (a deliberate axis separation: this shape carries facts, not verdicts). */
|
|
@@ -29,6 +30,13 @@ export interface ReadFaceInputs {
|
|
|
29
30
|
* "open"… no — see the resolution order: an EXPLICIT open still wins (row 4 is a default, not a
|
|
30
31
|
* clamp); what it changes is that NOTHING implicit opens a bash-less mount. */
|
|
31
32
|
fullShellReachable: boolean;
|
|
33
|
+
/** #237 — observation hook for the ONE silent branch of the order: row 1's deployment-seat clamp
|
|
34
|
+
* (readOnly mount + deps "open" + NO task seat). Fired exactly when the deps declaration was the
|
|
35
|
+
* deciding seat and the mount overrode it — not when a task-level "roots" pinned the same value
|
|
36
|
+
* anyway (the mount changed nothing there, and announcing it would claim a false cause), and
|
|
37
|
+
* never on the throw rows (those are already loud). The resolver stays the single owner of the
|
|
38
|
+
* clamp predicate; stations own delivery/dedup ({@link deploymentReadFaceClampNotice}). */
|
|
39
|
+
onDeploymentClamp?: () => void;
|
|
32
40
|
}
|
|
33
41
|
/**
|
|
34
42
|
* The ONE resolution order (§2.2), first hit wins. prepare-task AND createHandsToolkit both call
|
|
@@ -47,3 +55,17 @@ export interface ReadFaceInputs {
|
|
|
47
55
|
* 5. default: ROOTS (D-1b — the engine never opens implicitly).
|
|
48
56
|
*/
|
|
49
57
|
export declare function resolveReadFace(i: ReadFaceInputs): ReadFace;
|
|
58
|
+
/**
|
|
59
|
+
* #237 — the ONE text of the deployment-clamp announcement (`config.read_face_deployment_clamped`).
|
|
60
|
+
* Both stations deliver THIS object through their own `deliverEngineNotice` call (prepare-task with
|
|
61
|
+
* the `RunnerDeps.onNotice` seat, once per notice sink — the sink, not the deps object, because the
|
|
62
|
+
* Runner rebuilds its deps; `createHandsToolkit` with its band-local `HandsToolkitOptions.onNotice`
|
|
63
|
+
* seat — toolkit creation is itself the one-per-mount boot moment, and an absent seat prints the
|
|
64
|
+
* same line via `console.warn`). Built here, import-free, so the message can never fork between
|
|
65
|
+
* the stations.
|
|
66
|
+
*/
|
|
67
|
+
export declare function deploymentReadFaceClampNotice(): {
|
|
68
|
+
code: string;
|
|
69
|
+
message: string;
|
|
70
|
+
detail: Record<string, unknown>;
|
|
71
|
+
};
|
|
@@ -16,6 +16,8 @@ export function resolveReadFace(i) {
|
|
|
16
16
|
e.code = "config.read_face_readonly_conflict";
|
|
17
17
|
throw e;
|
|
18
18
|
}
|
|
19
|
+
if (deps === "open" && spec === undefined)
|
|
20
|
+
i.onDeploymentClamp?.();
|
|
19
21
|
return "roots";
|
|
20
22
|
}
|
|
21
23
|
if (i.orgGoverned) {
|
|
@@ -36,3 +38,10 @@ export function resolveReadFace(i) {
|
|
|
36
38
|
return "roots";
|
|
37
39
|
return "roots";
|
|
38
40
|
}
|
|
41
|
+
export function deploymentReadFaceClampNotice() {
|
|
42
|
+
return {
|
|
43
|
+
code: "config.read_face_deployment_clamped",
|
|
44
|
+
message: `readFace (deployment seat): the deployment-wide "open" declaration is not in force on this read-only (verifier) mount — clamped to "roots" (stricter-wins: the mount's containment is load-bearing and never openable). Other mounts still honor "open".`,
|
|
45
|
+
detail: { seat: "readFace (deployment seat)", declared: "open", inForce: "roots", cause: "read_only_mount" },
|
|
46
|
+
};
|
|
47
|
+
}
|
package/dist/tools/fs/search.js
CHANGED
|
@@ -1147,6 +1147,8 @@ async function rgDenyExistenceProbe(env, root, deny, target, signal) {
|
|
|
1147
1147
|
return { kind: "probe_failed" };
|
|
1148
1148
|
const first = res.value.stdout.split("\n").find((l) => l.trim().length > 0);
|
|
1149
1149
|
if (first !== undefined) {
|
|
1150
|
+
if (/^__RG_PROBE_EXIT_\d+$/.test(first.trim()))
|
|
1151
|
+
return { kind: "probe_failed" };
|
|
1150
1152
|
const hit = deny.matchPath(first.trim());
|
|
1151
1153
|
return { kind: "existence", ...(hit !== null ? { patterns: [hit.pattern] } : {}) };
|
|
1152
1154
|
}
|