@sema-agent/core 7.33.8 → 7.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +339 -0
- package/dist/agents/cross-session-judge.d.ts +16 -7
- package/dist/agents/cross-session-judge.js +18 -15
- package/dist/agents/peer-session-drain.d.ts +4 -0
- package/dist/agents/peer-session-drain.js +5 -2
- package/dist/agents/repair-loop.d.ts +1 -1
- package/dist/agents/subagent.d.ts +3 -0
- package/dist/agents/subagent.js +12 -3
- package/dist/brain/anthropic.js +1 -0
- package/dist/brain/open-responses.js +1 -0
- package/dist/brain/openai.js +1 -0
- package/dist/brain/stream-engine.d.ts +3 -0
- package/dist/brain/stream-engine.js +2 -1
- package/dist/core/a2a.d.ts +3 -1
- package/dist/core/a2a.js +1 -1
- package/dist/core/ask-origin.d.ts +351 -154
- package/dist/core/ask-origin.js +223 -45
- package/dist/core/ask-unresolvable-notice.d.ts +5 -2
- package/dist/core/ask-unresolvable-notice.js +4 -3
- package/dist/core/auto-mode-arming.d.ts +17 -4
- package/dist/core/auto-mode-arming.js +31 -6
- package/dist/core/auto-mode-defaults.d.ts +16 -7
- package/dist/core/auto-mode-defaults.js +2 -1
- package/dist/core/auto-mode-directive.d.ts +18 -30
- package/dist/core/auto-mode-directive.js +9 -18
- package/dist/core/auto-mode-prompt.d.ts +62 -1
- package/dist/core/auto-mode-prompt.js +42 -4
- package/dist/core/auto-mode-rebuild.d.ts +2 -1
- package/dist/core/auto-mode-rebuild.js +1 -0
- package/dist/core/auto-mode.d.ts +32 -3
- package/dist/core/auto-mode.js +80 -41
- package/dist/core/checkpoint-store.d.ts +50 -35
- package/dist/core/checkpoint-store.js +58 -3
- package/dist/core/child-clamp-seats.js +1 -0
- package/dist/core/classifier-unavailable-arm.d.ts +57 -0
- package/dist/core/classifier-unavailable-arm.js +17 -0
- package/dist/core/effective-path-target.d.ts +12 -0
- package/dist/core/effective-path-target.js +18 -7
- package/dist/core/engine-notice.d.ts +53 -2
- package/dist/core/fs-write-gate-policy.d.ts +5 -1
- package/dist/core/fs-write-gate-policy.js +4 -1
- package/dist/core/gate-fold.js +17 -1
- package/dist/core/gate-lanes.js +111 -46
- package/dist/core/gate-outcome.d.ts +40 -7
- package/dist/core/gate-outcome.js +26 -4
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +12 -0
- package/dist/core/hooks.d.ts +50 -29
- package/dist/core/hooks.js +1 -3
- package/dist/core/mcp.js +1 -1
- package/dist/core/mode-policy.d.ts +182 -0
- package/dist/core/mode-policy.js +179 -0
- package/dist/core/oracle-isolation.d.ts +3 -3
- package/dist/core/permission-rule-model.d.ts +16 -3
- package/dist/core/permission-rule-model.js +36 -3
- package/dist/core/permission-rule-provider.d.ts +22 -11
- package/dist/core/permission-rule-provider.js +17 -7
- package/dist/core/permission-rules.d.ts +23 -1
- package/dist/core/permission-rules.js +35 -8
- package/dist/core/persisted-rule-arms.d.ts +10 -9
- package/dist/core/persisted-rule-arms.js +9 -6
- package/dist/core/present-plan-tool.d.ts +1 -1
- package/dist/core/present-plan-tool.js +3 -3
- package/dist/core/read-only-shell.d.ts +19 -19
- package/dist/core/read-only-shell.js +38 -12
- package/dist/core/retired-keys.d.ts +9 -3
- package/dist/core/retired-keys.js +5 -1
- package/dist/core/runner/classifier-ladder.d.ts +43 -1
- package/dist/core/runner/classifier-ladder.js +29 -0
- package/dist/core/runner/contracts.d.ts +37 -35
- package/dist/core/runner/denial-limit-arms.d.ts +30 -7
- package/dist/core/runner/denial-limit-arms.js +15 -9
- package/dist/core/runner/env-capability-census.js +1 -1
- package/dist/core/runner/gate-exit.d.ts +30 -4
- package/dist/core/runner/gate-exit.js +28 -7
- package/dist/core/runner/permission-rule-lanes.d.ts +9 -5
- package/dist/core/runner/permission-rule-lanes.js +44 -40
- package/dist/core/runner/persisted-path-rows.d.ts +64 -0
- package/dist/core/runner/persisted-path-rows.js +89 -0
- package/dist/core/runner/prepare-artifact.d.ts +5 -1
- package/dist/core/runner/prepare-artifact.js +9 -3
- package/dist/core/runner/prepare-ask-lane.d.ts +5 -3
- package/dist/core/runner/prepare-ask-lane.js +1 -1
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +39 -13
- package/dist/core/runner/prepare-caps-and-workflow.js +58 -29
- package/dist/core/runner/prepare-gate-stations.d.ts +33 -6
- package/dist/core/runner/prepare-gate-stations.js +32 -19
- package/dist/core/runner/prepare-hands-readface.d.ts +40 -17
- package/dist/core/runner/prepare-hands-readface.js +64 -53
- package/dist/core/runner/prepare-inherited-gate.d.ts +22 -22
- package/dist/core/runner/prepare-inherited-gate.js +37 -23
- package/dist/core/runner/prepare-park-ask.d.ts +2 -2
- package/dist/core/runner/prepare-park-ask.js +2 -1
- package/dist/core/runner/prepare-policy-chain.d.ts +22 -2
- package/dist/core/runner/prepare-policy-chain.js +39 -12
- package/dist/core/runner/prepare-protocol-tools.d.ts +28 -0
- package/dist/core/runner/prepare-protocol-tools.js +13 -3
- package/dist/core/runner/prepare-run-refs.d.ts +6 -6
- package/dist/core/runner/prepare-run-refs.js +9 -4
- package/dist/core/runner/prepare-safety-scan.d.ts +5 -0
- package/dist/core/runner/prepare-safety-scan.js +19 -3
- package/dist/core/runner/prepare-suspend-saga.d.ts +5 -5
- package/dist/core/runner/prepare-suspend-saga.js +6 -6
- package/dist/core/runner/prepare-task.js +51 -22
- package/dist/core/runner/prepare-wiring-manifest.d.ts +10 -6
- package/dist/core/runner/prepare-wiring-manifest.js +5 -3
- package/dist/core/runner/read-root-grant.d.ts +9 -0
- package/dist/core/runner/read-root-grant.js +12 -0
- package/dist/core/runner/retired-runner-deps.js +1 -0
- package/dist/core/runner/run-identity-wiring.js +1 -1
- package/dist/core/runner/run-leg.js +24 -15
- package/dist/core/runner/run-notification-lane.js +1 -0
- package/dist/core/runner/run-telemetry-and-budget-seats.d.ts +2 -2
- package/dist/core/runner/run-telemetry-and-budget-seats.js +1 -1
- package/dist/core/runner/run-turn-boundary.js +1 -1
- package/dist/core/runner/runtask.js +3 -0
- package/dist/core/runner/stream-mode-verb.d.ts +27 -0
- package/dist/core/runner/stream-mode-verb.js +19 -0
- package/dist/core/runner/turn-attachments.d.ts +3 -3
- package/dist/core/runner/turn-attachments.js +1 -1
- package/dist/core/runner/write-protection-station.d.ts +7 -1
- package/dist/core/runner/write-protection-station.js +6 -3
- package/dist/core/runner-deps.d.ts +31 -34
- package/dist/core/runtime-caps.d.ts +12 -3
- package/dist/core/safety-axis-vocab.d.ts +2 -2
- package/dist/core/safety-axis-vocab.js +1 -1
- package/dist/core/sensitive-path-policy.d.ts +4 -2
- package/dist/core/sensitive-path-policy.js +39 -30
- package/dist/core/task-event.d.ts +1 -1
- package/dist/core/task-result.d.ts +1 -1
- package/dist/core/task-spec.d.ts +41 -89
- package/dist/core/task-stream.d.ts +16 -0
- package/dist/core/terminal-cause.js +1 -0
- package/dist/core/tighten-task-spec.d.ts +2 -2
- package/dist/core/tighten-task-spec.js +12 -14
- package/dist/core/tool-catalog-entries.js +4 -3
- package/dist/core/tool-policy.d.ts +54 -25
- package/dist/core/tool-policy.js +43 -2
- package/dist/core/tool-registry.d.ts +2 -0
- package/dist/core/tool-registry.js +3 -0
- package/dist/core/tool-spec.d.ts +8 -9
- package/dist/core/trace.d.ts +7 -6
- package/dist/core/types.d.ts +2 -1
- package/dist/core/wiring-manifest.d.ts +31 -5
- package/dist/core/wiring-manifest.js +28 -1
- package/dist/core/write-protect.d.ts +11 -1
- package/dist/core/write-protect.js +26 -15
- package/dist/engine/execution-env/node-execution-env.d.ts +3 -0
- package/dist/engine/execution-env/node-execution-env.js +11 -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/index.d.ts +15 -5
- package/dist/index.js +12 -3
- package/dist/orchestration/workflow-governance.js +1 -1
- package/dist/orchestration/workflow-primitives.d.ts +1 -1
- package/dist/orchestration/workflow-size-guideline.js +2 -2
- package/dist/tools/artifact/artifact-tool.d.ts +17 -0
- package/dist/tools/artifact/artifact-tool.js +16 -1
- package/dist/tools/fs/bash-readonly-classifier.d.ts +28 -7
- package/dist/tools/fs/bash-readonly-classifier.js +88 -0
- package/dist/tools/fs/containment-face.d.ts +9 -3
- package/dist/tools/fs/containment-face.js +2 -0
- package/dist/tools/fs/file-read-check.d.ts +49 -0
- package/dist/tools/fs/file-read-check.js +71 -0
- package/dist/tools/fs/fs-bash.d.ts +3 -3
- package/dist/tools/fs/fs-read.d.ts +1 -0
- package/dist/tools/fs/fs-search-tools.d.ts +2 -2
- package/dist/tools/fs/fs-search-tools.js +4 -4
- package/dist/tools/fs/fs-shared.js +20 -6
- package/dist/tools/fs/fs-write.d.ts +4 -3
- package/dist/tools/fs/fs-write.js +9 -9
- package/dist/tools/fs/index.d.ts +39 -3
- package/dist/tools/fs/index.js +38 -15
- package/dist/tools/fs/read-deny-home.d.ts +93 -0
- package/dist/tools/fs/read-deny-home.js +225 -0
- package/dist/tools/fs/read-deny.d.ts +176 -34
- package/dist/tools/fs/read-deny.js +268 -61
- package/dist/tools/fs/safety.d.ts +6 -1
- package/dist/tools/fs/safety.js +18 -5
- package/dist/tools/fs/search.d.ts +3 -0
- package/dist/tools/fs/search.js +47 -9
- package/dist/tools/fs/shell-read-check.d.ts +42 -11
- package/dist/tools/fs/shell-read-check.js +188 -13
- package/dist/tools/fs/shell-read-establish.d.ts +7 -3
- package/dist/tools/fs/shell-read-establish.js +134 -21
- package/dist/tools/fs/shell-read-identity.d.ts +7 -0
- package/dist/tools/fs/shell-read-identity.js +21 -0
- package/dist/tools/fs/shell-read-judge.d.ts +92 -32
- package/dist/tools/fs/shell-read-judge.js +43 -18
- package/dist/tools/fs/shell-read-plan.d.ts +4 -0
- package/dist/tools/fs/shell-read-plan.js +26 -4
- package/dist/tools/fs/shell-read-write-targets.d.ts +73 -0
- package/dist/tools/fs/shell-read-write-targets.js +173 -0
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +103 -11
- package/wire-changes/core@7.34.0.json +2621 -0
|
@@ -1,21 +1,137 @@
|
|
|
1
1
|
import { sliceHeadSafe, sliceTailSafe } from "../../core/surrogate-safe-slice.js";
|
|
2
|
+
import { isAbsolutePathForm, normalizeAbsPathLexically, pathFamilyOf, pathIdentityOf, spellingOfIdentity } from "./safety.js";
|
|
2
3
|
import { PROBE_REASON_MAX } from "../../core/untrusted-text.js";
|
|
4
|
+
export const READ_DENY_HOME_WORD = "~";
|
|
5
|
+
export function readDenyPatternRun(pattern) {
|
|
6
|
+
const segments = pattern.split("/").filter(Boolean);
|
|
7
|
+
return segments[0] === READ_DENY_HOME_WORD ? { homeAnchored: true, run: segments.slice(1).join("/") } : { homeAnchored: false, run: pattern };
|
|
8
|
+
}
|
|
9
|
+
export function readDenyRunHead(run) {
|
|
10
|
+
const segments = run.split("/").filter(Boolean);
|
|
11
|
+
const wild = segments.findIndex((seg) => seg.includes("*"));
|
|
12
|
+
return wild === -1 ? segments.length : wild;
|
|
13
|
+
}
|
|
14
|
+
export const DENY_FACES = ["read", "write"];
|
|
15
|
+
export async function withRuleRowsWhereTheyLead(rows, leadsTo) {
|
|
16
|
+
const leads = new Map();
|
|
17
|
+
const partsOf = (head) => {
|
|
18
|
+
const family = pathFamilyOf({ root: head });
|
|
19
|
+
const id = pathIdentityOf(head, family, head);
|
|
20
|
+
if (!id.ok)
|
|
21
|
+
return [head];
|
|
22
|
+
const unc = id.identity.startsWith("//");
|
|
23
|
+
const segments = id.identity.split("/").filter((seg) => seg !== "");
|
|
24
|
+
const parts = [];
|
|
25
|
+
for (let k = unc ? 2 : 1; k <= segments.length; k++)
|
|
26
|
+
parts.push(spellingOfIdentity(`${unc ? "//" : "/"}${segments.slice(0, k).join("/")}`, family));
|
|
27
|
+
return parts.length > 0 ? parts : [head];
|
|
28
|
+
};
|
|
29
|
+
const partsByRow = rows.map((row) => (row.head === undefined ? [] : partsOf(row.head)));
|
|
30
|
+
for (const parts of partsByRow)
|
|
31
|
+
for (const part of parts)
|
|
32
|
+
if (!leads.has(part))
|
|
33
|
+
leads.set(part, leadsTo(part).catch(() => undefined));
|
|
34
|
+
const out = [];
|
|
35
|
+
for (const [i, row] of rows.entries()) {
|
|
36
|
+
out.push(row);
|
|
37
|
+
let parentLed;
|
|
38
|
+
let parentPart;
|
|
39
|
+
for (const part of partsByRow[i]) {
|
|
40
|
+
const led = await leads.get(part);
|
|
41
|
+
const extendsParent = led !== undefined && parentLed !== undefined && parentPart !== undefined && sameLocation(led, `${parentLed.replace(/[\\/]+$/, "")}/${part.slice(parentPart.replace(/[\\/]+$/, "").length).replace(/^[\\/]+/, "")}`);
|
|
42
|
+
const twin = extendsParent ? undefined : rowLedTo(row, part, led);
|
|
43
|
+
if (twin !== undefined)
|
|
44
|
+
out.push(twin);
|
|
45
|
+
parentLed = led;
|
|
46
|
+
parentPart = part;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
function sameLocation(a, b) {
|
|
52
|
+
const family = pathFamilyOf({ root: a });
|
|
53
|
+
const x = pathIdentityOf(a, family, a);
|
|
54
|
+
const y = pathIdentityOf(b, family, b);
|
|
55
|
+
return x.ok && y.ok && x.identity === y.identity;
|
|
56
|
+
}
|
|
57
|
+
function rowLedTo(row, head, at) {
|
|
58
|
+
if (at === undefined)
|
|
59
|
+
return undefined;
|
|
60
|
+
const family = pathFamilyOf({ root: head });
|
|
61
|
+
const from = pathIdentityOf(at, family, head);
|
|
62
|
+
const to = pathIdentityOf(head, family, head);
|
|
63
|
+
if (!from.ok || !to.ok || from.identity === to.identity)
|
|
64
|
+
return undefined;
|
|
65
|
+
const base = from.identity;
|
|
66
|
+
const under = base.endsWith("/") ? base : `${base}/`;
|
|
67
|
+
const onto = to.identity.endsWith("/") ? to.identity.slice(0, -1) : to.identity;
|
|
68
|
+
return {
|
|
69
|
+
rule: row.rule,
|
|
70
|
+
tool: row.tool,
|
|
71
|
+
pattern: row.pattern,
|
|
72
|
+
faces: row.faces,
|
|
73
|
+
reaches: (absolutePath) => {
|
|
74
|
+
const id = pathIdentityOf(absolutePath, family, head);
|
|
75
|
+
if (!id.ok)
|
|
76
|
+
return false;
|
|
77
|
+
if (id.identity === base)
|
|
78
|
+
return row.reaches(to.identity);
|
|
79
|
+
return id.identity.startsWith(under) && row.reaches(`${onto}/${id.identity.slice(under.length)}`);
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function ruleRowHit(rows, face, path) {
|
|
84
|
+
for (const r of rows)
|
|
85
|
+
if (r.faces.includes(face) && r.reaches(path))
|
|
86
|
+
return r;
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
export function compileWriteDeny(rules = []) {
|
|
90
|
+
const ruleRows = rules.filter((r) => r.faces.includes("write"));
|
|
91
|
+
const matchPath = (path) => {
|
|
92
|
+
const hit = ruleRowHit(ruleRows, "write", path);
|
|
93
|
+
return hit === undefined ? null : { pattern: hit.rule };
|
|
94
|
+
};
|
|
95
|
+
return { face: "write", ruleRows, matchPath, matchTarget: (canonicalKey, lexicalView, opts) => matchTargetOver(matchPath, canonicalKey, lexicalView, opts) };
|
|
96
|
+
}
|
|
97
|
+
function matchTargetOver(matchPath, canonicalKey, lexicalView, opts) {
|
|
98
|
+
const canonical = matchPath(canonicalKey, opts);
|
|
99
|
+
if (canonical !== null)
|
|
100
|
+
return { ...canonical, matchedView: canonicalKey };
|
|
101
|
+
if (lexicalView !== undefined) {
|
|
102
|
+
const lexical = matchPath(lexicalView);
|
|
103
|
+
if (lexical !== null)
|
|
104
|
+
return { ...lexical, matchedView: lexicalView };
|
|
105
|
+
}
|
|
106
|
+
for (const hop of opts?.hops ?? []) {
|
|
107
|
+
const viaHop = matchPath(hop);
|
|
108
|
+
if (viaHop !== null)
|
|
109
|
+
return { ...viaHop, matchedView: hop };
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
3
113
|
export function readDenySeat(matcher) {
|
|
4
|
-
const entries = (matcher?.entries ?? []).map((e) => e.pattern);
|
|
114
|
+
const entries = [...(matcher?.entries ?? []).map((e) => e.pattern), ...(matcher?.ruleRows ?? []).map((r) => r.rule)];
|
|
5
115
|
if (matcher === undefined || entries.length === 0)
|
|
6
116
|
return { denyEntries: entries };
|
|
7
|
-
return {
|
|
117
|
+
return {
|
|
118
|
+
denyMatch: (p, opts) => matcher.matchPath(p, opts)?.pattern ?? null,
|
|
119
|
+
denyAtOrBelow: (p) => (matcher.rowAtOrBelow !== undefined ? (matcher.rowAtOrBelow(p)?.pattern ?? null) : entries[0]),
|
|
120
|
+
denyEntries: entries,
|
|
121
|
+
};
|
|
8
122
|
}
|
|
9
|
-
export function sensitivePathDenyList(pattern) {
|
|
123
|
+
export function sensitivePathDenyList(pattern, face = "read") {
|
|
10
124
|
const rows = typeof pattern === "string" ? [pattern] : pattern;
|
|
11
|
-
return `the sensitive-path read deny list (pattern${rows.length === 1 ? "" : "s"} ${rows.map((p) => `"${p}"`).join(", ")})`;
|
|
125
|
+
return `the ${face === "write" ? "write" : "sensitive-path read"} deny list (pattern${rows.length === 1 ? "" : "s"} ${rows.map((p) => `"${p}"`).join(", ")})`;
|
|
12
126
|
}
|
|
13
127
|
export const SENSITIVE_PATH_DENY_SCOPE = "This list guards credential-class paths and applies regardless of the containment roots.";
|
|
128
|
+
export const WRITE_DENY_SCOPE = "A write deny row holds regardless of the containment roots and of the permission mode.";
|
|
14
129
|
export function readDenySentence(finding) {
|
|
130
|
+
const write = finding.face === "write";
|
|
15
131
|
const canonicalNote = finding.canonical !== undefined && finding.canonical !== finding.target ? `(canonical target: "${fitMiddle(finding.canonical, READ_DENY_SENTENCE_PART_MAX)}") ` : "";
|
|
16
132
|
const lexicalHit = finding.matchedView !== undefined && finding.matchedView !== (finding.canonical ?? finding.target);
|
|
17
133
|
const judged = lexicalHit ? `the requested path's spelling ("${fitMiddle(finding.matchedView, READ_DENY_SENTENCE_PART_MAX)}") matches` : "the target matches";
|
|
18
|
-
return `${finding.action ?? "reading"} "${fitMiddle(finding.target, READ_DENY_SENTENCE_PART_MAX)}" ${canonicalNote}is refused: ${judged} ${sensitivePathDenyList(fitMiddle(finding.pattern, READ_DENY_SENTENCE_PART_MAX))}. ${SENSITIVE_PATH_DENY_SCOPE}`;
|
|
134
|
+
return `${finding.action ?? (write ? "writing" : "reading")} "${fitMiddle(finding.target, READ_DENY_SENTENCE_PART_MAX)}" ${canonicalNote}is refused: ${judged} ${sensitivePathDenyList(fitMiddle(finding.pattern, READ_DENY_SENTENCE_PART_MAX), write ? "write" : "read")}. ${write ? WRITE_DENY_SCOPE : SENSITIVE_PATH_DENY_SCOPE}`;
|
|
19
135
|
}
|
|
20
136
|
export const READ_DENY_SENTENCE_PART_MAX = PROBE_REASON_MAX;
|
|
21
137
|
export function fitMiddle(s, budget) {
|
|
@@ -49,6 +165,7 @@ export const READ_FACE_BUILTIN_DENY_TABLE = [
|
|
|
49
165
|
{ pattern: ".local/share/keyrings", tier: "credentials" },
|
|
50
166
|
{ pattern: "Library/Keychains", tier: "credentials" },
|
|
51
167
|
{ pattern: ".credentials.json", tier: "credentials" },
|
|
168
|
+
{ pattern: "credentials.env", tier: "credentials" },
|
|
52
169
|
{ pattern: ".codex/auth.json", tier: "credentials" },
|
|
53
170
|
{ pattern: ".config/github-copilot", tier: "credentials" },
|
|
54
171
|
{ pattern: ".gemini/oauth_creds.json", tier: "credentials" },
|
|
@@ -69,23 +186,23 @@ export const READ_FACE_BUILTIN_DENY_TABLE = [
|
|
|
69
186
|
{ pattern: "Library/Application Support/Exodus", tier: "wallet" },
|
|
70
187
|
{ pattern: "Library/Application Support/Ledger Live", tier: "wallet" },
|
|
71
188
|
{ pattern: "wallet.dat", tier: "wallet" },
|
|
72
|
-
{ pattern: "
|
|
73
|
-
{ pattern: "
|
|
74
|
-
{ pattern: "
|
|
75
|
-
{ pattern: "
|
|
76
|
-
{ pattern: "
|
|
77
|
-
{ pattern: "
|
|
78
|
-
{ pattern: "
|
|
79
|
-
{ pattern: "
|
|
80
|
-
{ pattern: "
|
|
81
|
-
{ pattern: "
|
|
82
|
-
{ pattern: "
|
|
83
|
-
{ pattern: "
|
|
84
|
-
{ pattern: "
|
|
85
|
-
{ pattern: "
|
|
86
|
-
{ pattern: "
|
|
189
|
+
{ pattern: "~/.sema/settings.json", tier: "agent-config" },
|
|
190
|
+
{ pattern: "~/.sema/settings.local.json", tier: "agent-config" },
|
|
191
|
+
{ pattern: "~/.sema.*", tier: "agent-config" },
|
|
192
|
+
{ pattern: "~/.claude/settings.json", tier: "agent-config" },
|
|
193
|
+
{ pattern: "~/.claude/settings.local.json", tier: "agent-config" },
|
|
194
|
+
{ pattern: "~/.claude.*", tier: "agent-config" },
|
|
195
|
+
{ pattern: "~/.mcp.json", tier: "agent-config" },
|
|
196
|
+
{ pattern: "~/.ai-agent/.env", tier: "agent-config" },
|
|
197
|
+
{ pattern: "~/.sema/engine-data/.env", tier: "agent-config" },
|
|
198
|
+
{ pattern: "~/.codex/config.toml", tier: "agent-config" },
|
|
199
|
+
{ pattern: "~/.cursor/mcp.json", tier: "agent-config" },
|
|
200
|
+
{ pattern: "~/.continue/config.json", tier: "agent-config" },
|
|
201
|
+
{ pattern: "~/.continue/config.yaml", tier: "agent-config" },
|
|
202
|
+
{ pattern: "~/.aider.conf.yml", tier: "agent-config" },
|
|
203
|
+
{ pattern: "~/.gemini/settings.json", tier: "agent-config" },
|
|
87
204
|
];
|
|
88
|
-
export const READ_DENY_DEFAULT_TIERS = [];
|
|
205
|
+
export const READ_DENY_DEFAULT_TIERS = ["credentials", "agent-config"];
|
|
89
206
|
export function resolveReadDenyBuiltins(config) {
|
|
90
207
|
let activeTiers;
|
|
91
208
|
if (config?.tiers === undefined) {
|
|
@@ -112,13 +229,21 @@ export function resolveReadDenyBuiltins(config) {
|
|
|
112
229
|
}
|
|
113
230
|
for (const name of config.exclude) {
|
|
114
231
|
if (typeof name !== "string" || !READ_FACE_BUILTIN_DENY_TABLE.some((r) => r.pattern === name)) {
|
|
115
|
-
|
|
232
|
+
const anchored = typeof name === "string" ? READ_FACE_BUILTIN_DENY_TABLE.find((r) => r.pattern === `${READ_DENY_HOME_WORD}/${name}`) : undefined;
|
|
233
|
+
throw new Error(`readDenyBuiltinExclude: ${JSON.stringify(name)} names no built-in row — ${anchored !== undefined ? `the row is anchored at the home directory and is named ${JSON.stringify(anchored.pattern)}; ` : ""}row names are the canonical pattern texts of READ_FACE_BUILTIN_DENY_TABLE (e.g. ".ssh", ".config/gcloud", "~/.sema/settings.json").`);
|
|
116
234
|
}
|
|
117
235
|
}
|
|
118
236
|
excluded = new Set(config.exclude);
|
|
119
237
|
}
|
|
120
238
|
return READ_FACE_BUILTIN_DENY_TABLE.filter((r) => activeTiers.has(r.tier) && !excluded.has(r.pattern));
|
|
121
239
|
}
|
|
240
|
+
export function readDenyBuiltinConfigOf(seats) {
|
|
241
|
+
resolveReadDenyBuiltins(seats);
|
|
242
|
+
return {
|
|
243
|
+
...(seats.tiers !== undefined ? { tiers: [...seats.tiers] } : {}),
|
|
244
|
+
...(seats.exclude !== undefined ? { exclude: [...seats.exclude] } : {}),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
122
247
|
export function activeReadDenyBuiltinTiers(config) {
|
|
123
248
|
const rows = resolveReadDenyBuiltins(config);
|
|
124
249
|
return READ_DENY_BUILTIN_TIERS.filter((t) => rows.some((r) => r.tier === t));
|
|
@@ -145,6 +270,30 @@ export function compileSegmentPattern(raw, fold) {
|
|
|
145
270
|
return { raw, segments, foldAscii: fold === "ascii" };
|
|
146
271
|
}
|
|
147
272
|
export function matchSegmentPatterns(path, compiled, opts) {
|
|
273
|
+
const views = segmentViewsOf(path, opts);
|
|
274
|
+
const rooted = isAbsolutePathForm(path);
|
|
275
|
+
const lowered = new Map();
|
|
276
|
+
for (const pat of compiled) {
|
|
277
|
+
for (const view of views) {
|
|
278
|
+
const segs = pat.foldAscii ? (lowered.get(view) ?? lowered.set(view, view.map(asciiLower)).get(view)) : view;
|
|
279
|
+
const n = pat.segments.length;
|
|
280
|
+
const runAt = (i) => i + n <= segs.length && pat.segments.every((re, j) => re.test(segs[i + j] ?? ""));
|
|
281
|
+
if (pat.anchors !== undefined && rooted) {
|
|
282
|
+
for (const anchor of pat.anchors)
|
|
283
|
+
if (anchor.every((a, j) => segs[j] === a) && runAt(anchor.length))
|
|
284
|
+
return pat.raw;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (pat.rootedOnly === true)
|
|
288
|
+
continue;
|
|
289
|
+
for (let i = 0; i + n <= segs.length; i++)
|
|
290
|
+
if (runAt(i))
|
|
291
|
+
return pat.raw;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
function segmentViewsOf(path, opts) {
|
|
148
297
|
const literalSegs = path.split(/[\\/]/).filter(Boolean);
|
|
149
298
|
const win32Segs = [];
|
|
150
299
|
for (const raw of literalSegs) {
|
|
@@ -164,20 +313,41 @@ export function matchSegmentPatterns(path, compiled, opts) {
|
|
|
164
313
|
}
|
|
165
314
|
const sameView = opts?.aliasResolved === true ||
|
|
166
315
|
(win32Segs.length === literalSegs.length && win32Segs.every((s, i) => s === literalSegs[i]));
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
316
|
+
return sameView ? [literalSegs] : [literalSegs, win32Segs];
|
|
317
|
+
}
|
|
318
|
+
function rowMeetsPath(pat, segs, rooted) {
|
|
319
|
+
const n = pat.segments.length;
|
|
320
|
+
if (pat.anchors !== undefined && rooted) {
|
|
321
|
+
return pat.anchors.some((a) => {
|
|
322
|
+
const k = Math.min(segs.length, a.length + n);
|
|
323
|
+
for (let i = 0; i < k; i++)
|
|
324
|
+
if (i < a.length ? segs[i] !== a[i] : !pat.segments[i - a.length].test(segs[i]))
|
|
325
|
+
return false;
|
|
326
|
+
return true;
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
if (pat.rootedOnly === true)
|
|
330
|
+
return false;
|
|
331
|
+
for (let i = 0; i < segs.length; i++) {
|
|
332
|
+
const m = Math.min(n, segs.length - i);
|
|
333
|
+
if (pat.segments.slice(0, m).every((re, j) => re.test(segs[i + j])))
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
function rowAtOrBelowOf(rows, path) {
|
|
339
|
+
const rooted = isAbsolutePathForm(path);
|
|
340
|
+
const views = segmentViewsOf(path);
|
|
341
|
+
for (const pat of rows) {
|
|
342
|
+
for (const view of views)
|
|
343
|
+
if (rowMeetsPath(pat, pat.foldAscii ? view.map(asciiLower) : view, rooted))
|
|
344
|
+
return pat.raw;
|
|
178
345
|
}
|
|
179
346
|
return null;
|
|
180
347
|
}
|
|
348
|
+
function homeAnchorSegments(home) {
|
|
349
|
+
return normalizeAbsPathLexically(home).split(/[\\/]/).filter(Boolean);
|
|
350
|
+
}
|
|
181
351
|
function rgGlobEscape(pattern) {
|
|
182
352
|
return pattern.replace(/[?[\]{}]/g, (c) => `[${c}]`);
|
|
183
353
|
}
|
|
@@ -193,9 +363,10 @@ function normalizeEntry(entry, layer) {
|
|
|
193
363
|
throw new Error(`readDenyPatterns (${layer}): pattern ${JSON.stringify(shape.pattern)} contains a backslash — patterns are "/"-separated segment runs (both path families are matched); spell the segments with "/".`);
|
|
194
364
|
}
|
|
195
365
|
const segments = shape.pattern.split("/").filter(Boolean);
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
366
|
+
const { homeAnchored, run } = readDenyPatternRun(segments.join("/"));
|
|
367
|
+
if (run.length === 0) {
|
|
368
|
+
throw new Error(`readDenyPatterns (${layer}): pattern ${JSON.stringify(shape.pattern)} contains no path segments${homeAnchored ? " below the home word" : ""} and would ${homeAnchored ? "name the whole home directory" : "deny nothing"}. ` +
|
|
369
|
+
`Patterns are "/"-separated runs of path SEGMENTS (e.g. ".ssh", ".config/gcloud", "id_rsa*", "~/.sema/settings.json"); remove the entry or spell the segments.`);
|
|
199
370
|
}
|
|
200
371
|
return { pattern: segments.join("/"), caseSensitive: shape.caseSensitive === true };
|
|
201
372
|
}
|
|
@@ -211,13 +382,27 @@ export function persistedReadDenyEntryProblem(e) {
|
|
|
211
382
|
if (pattern.includes("\\"))
|
|
212
383
|
return "pattern contains a backslash";
|
|
213
384
|
const segments = pattern.split("/").filter(Boolean);
|
|
214
|
-
if (segments.length === 0)
|
|
385
|
+
if (readDenyPatternRun(segments.join("/")).run.length === 0)
|
|
215
386
|
return "pattern has no path segments";
|
|
216
387
|
if (segments.join("/") !== pattern)
|
|
217
388
|
return "pattern is not in canonical a/b form";
|
|
218
389
|
return null;
|
|
219
390
|
}
|
|
220
|
-
export function
|
|
391
|
+
export function checkedReadDenyHomes(homes, seat = "readDenyHomes") {
|
|
392
|
+
if (!Array.isArray(homes) || !homes.every((h) => typeof h === "string" && isAbsolutePathForm(h))) {
|
|
393
|
+
let shown;
|
|
394
|
+
try {
|
|
395
|
+
shown = JSON.stringify(homes) ?? String(homes);
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
shown = String(homes);
|
|
399
|
+
}
|
|
400
|
+
throw new Error(`${seat}: expected an array of absolute ${seat === "readDenyHomes" ? "home paths" : "locations"}, got ${shown} — a home-anchored row ("~/…") is placed ${seat === "readDenyHomes" ? "under each" : "at each"}, so a relative or empty spelling would guard a place that is not the home's.`);
|
|
401
|
+
}
|
|
402
|
+
return homes;
|
|
403
|
+
}
|
|
404
|
+
export function compileReadDeny(additions = [], layer = "additions", builtin, rules = [], homes = [], linked = new Map()) {
|
|
405
|
+
const ruleRows = rules.filter((r) => r.faces.includes("read"));
|
|
221
406
|
const normalized = [];
|
|
222
407
|
const seen = new Set();
|
|
223
408
|
const push = (e) => {
|
|
@@ -231,44 +416,66 @@ export function compileReadDeny(additions = [], layer = "additions", builtin) {
|
|
|
231
416
|
push(normalizeEntry(row.pattern, "built-in"));
|
|
232
417
|
for (const entry of additions)
|
|
233
418
|
push(normalizeEntry(entry, layer));
|
|
419
|
+
const homeSegments = [...new Set(checkedReadDenyHomes(homes))].map(homeAnchorSegments);
|
|
234
420
|
const compiled = normalized.map((e) => {
|
|
235
|
-
const
|
|
421
|
+
const { homeAnchored, run } = readDenyPatternRun(e.pattern);
|
|
422
|
+
const fold = e.caseSensitive ? "none" : "ascii";
|
|
423
|
+
const c = compileSegmentPattern(run, fold);
|
|
236
424
|
if (c === null)
|
|
237
425
|
throw new Error(`readDenyPatterns: pattern ${JSON.stringify(e.pattern)} compiled to zero segments.`);
|
|
238
|
-
|
|
426
|
+
if (!homeAnchored || homeSegments.length === 0)
|
|
427
|
+
return { ...c, raw: e.pattern };
|
|
428
|
+
return { ...c, raw: e.pattern, anchors: homeSegments.map((segs) => (fold === "ascii" ? segs.map(asciiLower) : segs)) };
|
|
429
|
+
});
|
|
430
|
+
if (!(linked instanceof Map))
|
|
431
|
+
throw new Error(`readDenyHomeLinks: expected a Map from a row's pattern to its locations, got ${typeof linked}.`);
|
|
432
|
+
const linkedPlacements = [];
|
|
433
|
+
normalized.forEach((e, i) => {
|
|
434
|
+
const locations = linked.get(e.pattern);
|
|
435
|
+
const { homeAnchored, run } = readDenyPatternRun(e.pattern);
|
|
436
|
+
if (locations === undefined || !homeAnchored)
|
|
437
|
+
return;
|
|
438
|
+
const c = compiled[i];
|
|
439
|
+
const rest = c.segments.slice(readDenyRunHead(run));
|
|
440
|
+
for (const location of new Set(checkedReadDenyHomes(locations, "readDenyHomeLinks"))) {
|
|
441
|
+
const segs = homeAnchorSegments(location);
|
|
442
|
+
linkedPlacements.push({ raw: e.pattern, segments: rest, foldAscii: c.foldAscii, anchors: [c.foldAscii ? segs.map(asciiLower) : segs], rootedOnly: true });
|
|
443
|
+
}
|
|
239
444
|
});
|
|
445
|
+
const judged = linkedPlacements.length === 0 ? compiled : [...compiled, ...linkedPlacements];
|
|
240
446
|
const rgExclusionGlobs = [];
|
|
241
447
|
const rgProbeGlobs = [];
|
|
242
|
-
|
|
448
|
+
normalized.forEach((e, i) => {
|
|
449
|
+
if (compiled[i].anchors !== undefined)
|
|
450
|
+
return;
|
|
243
451
|
const flag = e.caseSensitive ? "--glob" : "--iglob";
|
|
244
|
-
const g = rgGlobEscape(e.pattern);
|
|
452
|
+
const g = rgGlobEscape(readDenyPatternRun(e.pattern).run);
|
|
245
453
|
rgExclusionGlobs.push({ flag, glob: `!**/${g}` }, { flag, glob: `!**/${g}/**` });
|
|
246
454
|
rgProbeGlobs.push({ flag, glob: `**/${g}` }, { flag, glob: `**/${g}/**` });
|
|
247
|
-
}
|
|
455
|
+
});
|
|
456
|
+
const placed = judged.filter((c) => c.anchors !== undefined);
|
|
457
|
+
const unexcludableUnder = (scope) => placed.length > 0 && (!isAbsolutePathForm(scope) || rowAtOrBelowOf(placed, normalizeAbsPathLexically(scope)) !== null);
|
|
458
|
+
const rowAtOrBelow = (path) => {
|
|
459
|
+
const hit = rowAtOrBelowOf(judged, path);
|
|
460
|
+
if (hit !== null)
|
|
461
|
+
return { pattern: hit };
|
|
462
|
+
return ruleRows.length === 0 ? null : { pattern: ruleRows[0].rule };
|
|
463
|
+
};
|
|
248
464
|
const matchPath = (path, opts) => {
|
|
249
|
-
const hit = matchSegmentPatterns(path,
|
|
250
|
-
|
|
465
|
+
const hit = matchSegmentPatterns(path, judged, opts);
|
|
466
|
+
if (hit !== null)
|
|
467
|
+
return { pattern: hit };
|
|
468
|
+
const rule = ruleRows.length === 0 || opts?.walked === true ? undefined : ruleRowHit(ruleRows, "read", path);
|
|
469
|
+
return rule === undefined ? null : { pattern: rule.rule };
|
|
251
470
|
};
|
|
252
471
|
return {
|
|
253
472
|
entries: normalized,
|
|
473
|
+
...(ruleRows.length > 0 ? { ruleRows } : {}),
|
|
254
474
|
matchPath,
|
|
255
|
-
matchTarget(canonicalKey, lexicalView, opts)
|
|
256
|
-
const canonical = matchPath(canonicalKey, opts);
|
|
257
|
-
if (canonical !== null)
|
|
258
|
-
return { ...canonical, matchedView: canonicalKey };
|
|
259
|
-
if (lexicalView !== undefined) {
|
|
260
|
-
const lexical = matchPath(lexicalView);
|
|
261
|
-
if (lexical !== null)
|
|
262
|
-
return { ...lexical, matchedView: lexicalView };
|
|
263
|
-
}
|
|
264
|
-
for (const hop of opts?.hops ?? []) {
|
|
265
|
-
const viaHop = matchPath(hop);
|
|
266
|
-
if (viaHop !== null)
|
|
267
|
-
return { ...viaHop, matchedView: hop };
|
|
268
|
-
}
|
|
269
|
-
return null;
|
|
270
|
-
},
|
|
475
|
+
matchTarget: (canonicalKey, lexicalView, opts) => matchTargetOver(matchPath, canonicalKey, lexicalView, opts),
|
|
271
476
|
rgExclusionGlobs,
|
|
272
477
|
rgProbeGlobs,
|
|
478
|
+
unexcludableUnder,
|
|
479
|
+
rowAtOrBelow,
|
|
273
480
|
};
|
|
274
481
|
}
|
|
@@ -93,7 +93,7 @@ export type ReadFileState = Map<string, ReadEntry>;
|
|
|
93
93
|
export declare function sha256(content: string): string;
|
|
94
94
|
/** A failed path resolution / invariant check, surfaced to the model as a self-correctable tool error. */
|
|
95
95
|
export interface FsViolation {
|
|
96
|
-
code: "path_not_in_root" | "not_read" | "stale" | "ambiguous_edit" | "invalid" | "read_path_denied";
|
|
96
|
+
code: "path_not_in_root" | "not_read" | "stale" | "ambiguous_edit" | "invalid" | "read_path_denied" | "write_path_denied";
|
|
97
97
|
/** Set on the `not_read` violation raised by an AUTO-TRUNCATED read
|
|
98
98
|
* ({@link ReadEntry.isPartialView}) rather than by a never-read file. Both carry the same CC-verbatim
|
|
99
99
|
* message; only this one needs {@link PARTIAL_VIEW_READ_ESCAPE_HINT} appended, because for it a plain
|
|
@@ -378,6 +378,10 @@ export declare function isPathIdentity(s: string, family: PathFamily | undefined
|
|
|
378
378
|
/** Is the identity a DIRECTORY a rule may speak for — anything but the tree's bare root (`/`, a drive root
|
|
379
379
|
* `/c`, a share root `//host/share`), which admits nothing? */
|
|
380
380
|
export declare function isIdentityDir(identity: PathIdentity, family: PathFamily | undefined): boolean;
|
|
381
|
+
/** A spelling of an identity that the environment resolves as a path of the family, and that reads as that family on its
|
|
382
|
+
* own ({@link isWinFormPath}) — the one way back from the comparison form: posix, the identity; a win drive identity
|
|
383
|
+
* `/c/rest` ⇒ `c:\rest`; a win UNC identity `//host/share/rest` ⇒ `\\host\share\rest`. */
|
|
384
|
+
export declare function spellingOfIdentity(identity: PathIdentity, family: PathFamily | undefined): string;
|
|
381
385
|
/** Does the spelling carry a `.` or `..` segment under the family's separators (a backslash is a name byte on
|
|
382
386
|
* POSIX)? A rule body with one admits nothing: folding it could make it name a second directory. */
|
|
383
387
|
export declare function carriesDotSegment(spelled: string, family: PathFamily | undefined): boolean;
|
|
@@ -444,6 +448,7 @@ export type RootsSeat = readonly string[] | (() => readonly string[] | undefined
|
|
|
444
448
|
* violation. Defense-in-depth at the tool layer; NOT a substitute for a sandboxed `executionEnv`.
|
|
445
449
|
*/
|
|
446
450
|
export declare function resolveKey(env: ExecutionEnv, rootCanonical: string, path: string, signal?: AbortSignal, baseCwd?: string, additionalRoots?: RootsSeat, exactFileReadExemption?: (canonicalKey: string) => boolean, readDeny?: {
|
|
451
|
+
readonly face?: "write";
|
|
447
452
|
matchTarget(canonicalKey: string, lexicalView?: string, opts?: {
|
|
448
453
|
readonly aliasResolved?: boolean;
|
|
449
454
|
}): {
|
package/dist/tools/fs/safety.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { readDenySentence } from "./read-deny.js";
|
|
3
|
+
import { homeViewsOf } from "./read-deny-home.js";
|
|
3
4
|
import { sliceHeadSafe } from "../../core/surrogate-safe-slice.js";
|
|
4
5
|
import { pathTargetValue, protectivePathTargetOf } from "../../core/tool-registry.js";
|
|
5
6
|
import { withGateTimeout, DEFAULT_GATE_AWAIT_TIMEOUT_MS } from "../../core/gate-bounded-await.js";
|
|
@@ -399,6 +400,13 @@ export function isIdentityDir(identity, family) {
|
|
|
399
400
|
return identity !== "/";
|
|
400
401
|
return identity.length > identityAnchorOf(identity).length;
|
|
401
402
|
}
|
|
403
|
+
export function spellingOfIdentity(identity, family) {
|
|
404
|
+
if (family !== "win")
|
|
405
|
+
return identity;
|
|
406
|
+
if (isUncIdentity(identity))
|
|
407
|
+
return identity.replace(/\//g, "\\");
|
|
408
|
+
return `${identity.slice(1, 2)}:${(identity.length > 2 ? identity.slice(2) : "/").replace(/\//g, "\\")}`;
|
|
409
|
+
}
|
|
402
410
|
export function carriesDotSegment(spelled, family) {
|
|
403
411
|
return (family === "win" ? /(^|[\\/])\.{1,2}([\\/]|$)/ : /(^|\/)\.{1,2}(\/|$)/).test(spelled);
|
|
404
412
|
}
|
|
@@ -458,15 +466,20 @@ export async function resolveKey(env, rootCanonical, path, signal, baseCwd, addi
|
|
|
458
466
|
return { ok: false, violation: { code: "invalid", message: `path "${path}" is a blocked device/special file (reading it can hang the process); refused.` } };
|
|
459
467
|
}
|
|
460
468
|
if (readDeny !== undefined) {
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
469
|
+
const judged = { aliasResolved: canon.aliasResolved === true };
|
|
470
|
+
let hit = readDeny.matchTarget(key, lexicalViewOf(path, baseCwd ?? rootCanonical), judged);
|
|
471
|
+
if (hit === null) {
|
|
472
|
+
const homeView = await homeViewsOf(env, signal).of(key);
|
|
473
|
+
if (homeView !== undefined)
|
|
474
|
+
hit = readDeny.matchTarget(key, homeView, judged);
|
|
475
|
+
}
|
|
464
476
|
if (hit !== null) {
|
|
477
|
+
const write = readDeny.face === "write";
|
|
465
478
|
return {
|
|
466
479
|
ok: false,
|
|
467
480
|
violation: {
|
|
468
|
-
code: "read_path_denied",
|
|
469
|
-
message: readDenySentence({ target: path, canonical: key, pattern: hit.pattern, ...(hit.matchedView !== undefined ? { matchedView: hit.matchedView } : {}) }),
|
|
481
|
+
code: write ? "write_path_denied" : "read_path_denied",
|
|
482
|
+
message: readDenySentence({ target: path, canonical: key, pattern: hit.pattern, ...(hit.matchedView !== undefined ? { matchedView: hit.matchedView } : {}), ...(write ? { face: "write" } : {}) }),
|
|
470
483
|
target: key,
|
|
471
484
|
pattern: hit.pattern,
|
|
472
485
|
},
|
|
@@ -170,6 +170,8 @@ export interface ReadDenyJudge {
|
|
|
170
170
|
flag: "--glob" | "--iglob";
|
|
171
171
|
glob: string;
|
|
172
172
|
}>;
|
|
173
|
+
/** read-deny.ts `ReadDenyMatcher.unexcludableUnder`: a scope where an entry the rg globs cannot exclude may lie. */
|
|
174
|
+
unexcludableUnder?(scope: string): boolean;
|
|
173
175
|
}
|
|
174
176
|
/** JS-fallback grep: ignore-aware walk + per-line scan, honoring output_mode / context / head_limit.
|
|
175
177
|
* `deny` prunes the walk (note appended to every output shape); `denyOut` is the
|
|
@@ -286,6 +288,7 @@ type GrepOutputMode = "content" | "files_with_matches" | "count";
|
|
|
286
288
|
*/
|
|
287
289
|
export declare function rgOutputDenyTripwire(stdout: string, mode: GrepOutputMode, judge: Pick<ReadDenyJudge, "matchPath">, opts?: {
|
|
288
290
|
dropIncompleteTail?: boolean;
|
|
291
|
+
base?: string;
|
|
289
292
|
}): {
|
|
290
293
|
trip: false;
|
|
291
294
|
} | {
|
package/dist/tools/fs/search.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { delimitUntrusted } from "../../core/untrusted-text.js";
|
|
2
2
|
import { MAX_EXEC_OUTPUT_BYTES } from "../../core/exec-output-tail.js";
|
|
3
3
|
import { hasBinaryExtension, isAbsoluteForFamily, isWinFormPath, joinForFamily, pathFamilyOf } from "./safety.js";
|
|
4
|
+
import { homeViewsOf } from "./read-deny-home.js";
|
|
4
5
|
import { thrownText } from "../../core/thrown-value.js";
|
|
5
6
|
const WALK_MAX_FILES = 5000;
|
|
6
7
|
const WALK_MAX_DEPTH = 32;
|
|
@@ -227,8 +228,14 @@ export async function walk(env, root, start, ignore, signal, deny) {
|
|
|
227
228
|
let aborted = false;
|
|
228
229
|
let denyPruned = 0;
|
|
229
230
|
const denyPatterns = [];
|
|
230
|
-
const
|
|
231
|
-
|
|
231
|
+
const views = deny !== undefined ? homeViewsOf(env, signal) : undefined;
|
|
232
|
+
const denyHit = async (path, leaf) => {
|
|
233
|
+
let hit = deny?.matchPath(path);
|
|
234
|
+
if (!hit && views !== undefined) {
|
|
235
|
+
const view = await views.of(path, leaf !== undefined ? { leaf } : undefined);
|
|
236
|
+
if (view !== undefined)
|
|
237
|
+
hit = deny.matchPath(view);
|
|
238
|
+
}
|
|
232
239
|
if (!hit)
|
|
233
240
|
return false;
|
|
234
241
|
denyPruned++;
|
|
@@ -265,7 +272,7 @@ export async function walk(env, root, start, ignore, signal, deny) {
|
|
|
265
272
|
if (info.kind === "directory") {
|
|
266
273
|
if (ignore(rel(info.path), true) || visited.has(info.path))
|
|
267
274
|
continue;
|
|
268
|
-
if (denyHit(info.path))
|
|
275
|
+
if (await denyHit(info.path))
|
|
269
276
|
continue;
|
|
270
277
|
if (depth < WALK_MAX_DEPTH)
|
|
271
278
|
stack.push({ dir: info.path, depth: depth + 1 });
|
|
@@ -275,7 +282,7 @@ export async function walk(env, root, start, ignore, signal, deny) {
|
|
|
275
282
|
else if (info.kind === "file") {
|
|
276
283
|
if (ignore(rel(info.path), false))
|
|
277
284
|
continue;
|
|
278
|
-
if (denyHit(info.path))
|
|
285
|
+
if (await denyHit(info.path, "file"))
|
|
279
286
|
continue;
|
|
280
287
|
if (typeof info.mtimeMs === "number")
|
|
281
288
|
mtimes.set(info.path, info.mtimeMs);
|
|
@@ -1131,18 +1138,40 @@ function formatRgRecords(records, p, caveat = "") {
|
|
|
1131
1138
|
export function rgOutputDenyTripwire(stdout, mode, judge, opts = {}) {
|
|
1132
1139
|
if (stdout.length === 0)
|
|
1133
1140
|
return { trip: false };
|
|
1141
|
+
const absolute = rgRecordAbsolute(opts.base);
|
|
1134
1142
|
for (const record of parseRgRecords(stdout, mode, opts.dropIncompleteTail === true)) {
|
|
1135
1143
|
if (record.path === undefined) {
|
|
1136
1144
|
if (record.text === "--")
|
|
1137
1145
|
continue;
|
|
1138
1146
|
return { trip: true, reason: "ambiguous-record" };
|
|
1139
1147
|
}
|
|
1140
|
-
const hit = judge.matchPath(record.path);
|
|
1148
|
+
const hit = judge.matchPath(absolute(record.path));
|
|
1141
1149
|
if (hit !== null)
|
|
1142
1150
|
return { trip: true, reason: "deny-hit", pattern: hit.pattern };
|
|
1143
1151
|
}
|
|
1144
1152
|
return { trip: false };
|
|
1145
1153
|
}
|
|
1154
|
+
function rgRecordAbsolute(base) {
|
|
1155
|
+
const family = base !== undefined ? pathFamilyOf({ root: base }) : undefined;
|
|
1156
|
+
return (p) => (base === undefined || family === undefined || isAbsoluteForFamily(family, p) ? p : joinForFamily(family, base, p.replace(/^\.[\\/]/, "")));
|
|
1157
|
+
}
|
|
1158
|
+
async function rgOutputDenyTripwireAt(env, stdout, mode, deny, opts, signal) {
|
|
1159
|
+
const views = homeViewsOf(env, signal);
|
|
1160
|
+
const absolute = rgRecordAbsolute(opts.base);
|
|
1161
|
+
const placed = new Map();
|
|
1162
|
+
for (const record of parseRgRecords(stdout, mode, opts.dropIncompleteTail === true)) {
|
|
1163
|
+
if (record.path === undefined)
|
|
1164
|
+
continue;
|
|
1165
|
+
const path = absolute(record.path);
|
|
1166
|
+
if (placed.has(path))
|
|
1167
|
+
continue;
|
|
1168
|
+
const view = await views.of(path, { leaf: "file" });
|
|
1169
|
+
if (view !== undefined)
|
|
1170
|
+
placed.set(path, view);
|
|
1171
|
+
}
|
|
1172
|
+
const judge = placed.size === 0 ? deny : { matchPath: (p) => deny.matchPath(p) ?? (placed.has(p) ? deny.matchPath(placed.get(p)) : null) };
|
|
1173
|
+
return rgOutputDenyTripwire(stdout, mode, judge, opts);
|
|
1174
|
+
}
|
|
1146
1175
|
async function jsGrepDenyTripFallback(env, root, p, signal, trip, deny) {
|
|
1147
1176
|
const denyOut = {};
|
|
1148
1177
|
const { text, rows } = await jsGrepDetailed(env, root, p, signal, undefined, deny, denyOut);
|
|
@@ -1212,7 +1241,7 @@ export async function rgGrepDetailed(env, root, p, signal, deny) {
|
|
|
1212
1241
|
const partial = err.partialStdout ?? "";
|
|
1213
1242
|
if (partial.trim().length > 0) {
|
|
1214
1243
|
if (deny !== undefined) {
|
|
1215
|
-
const trip =
|
|
1244
|
+
const trip = await rgOutputDenyTripwireAt(env, partial, mode, deny, { dropIncompleteTail: true, base: root }, signal);
|
|
1216
1245
|
if (trip.trip)
|
|
1217
1246
|
return jsGrepDenyTripFallback(env, root, p, signal, trip, deny);
|
|
1218
1247
|
}
|
|
@@ -1253,7 +1282,7 @@ export async function rgGrepDetailed(env, root, p, signal, deny) {
|
|
|
1253
1282
|
if (exitCode >= 2) {
|
|
1254
1283
|
if (stdout.trim().length > 0) {
|
|
1255
1284
|
if (deny !== undefined) {
|
|
1256
|
-
const trip =
|
|
1285
|
+
const trip = await rgOutputDenyTripwireAt(env, stdout, mode, deny, { dropIncompleteTail: true, base: root }, signal);
|
|
1257
1286
|
if (trip.trip)
|
|
1258
1287
|
return jsGrepDenyTripFallback(env, root, p, signal, trip, deny);
|
|
1259
1288
|
}
|
|
@@ -1268,7 +1297,7 @@ export async function rgGrepDetailed(env, root, p, signal, deny) {
|
|
|
1268
1297
|
return jsGrepFallback(env, root, p, signal, `exited with code ${exitCode} and produced no output`, deny);
|
|
1269
1298
|
}
|
|
1270
1299
|
if (deny !== undefined) {
|
|
1271
|
-
const trip =
|
|
1300
|
+
const trip = await rgOutputDenyTripwireAt(env, stdout, mode, deny, { base: root }, signal);
|
|
1272
1301
|
if (trip.trip)
|
|
1273
1302
|
return jsGrepDenyTripFallback(env, root, p, signal, trip, deny);
|
|
1274
1303
|
}
|
|
@@ -1308,7 +1337,16 @@ async function rgDenyExistenceProbe(env, root, deny, target, signal) {
|
|
|
1308
1337
|
}
|
|
1309
1338
|
}
|
|
1310
1339
|
export async function runGrepDetailed(env, root, p, signal, deny) {
|
|
1311
|
-
|
|
1340
|
+
const scope = p.path ? (isAbsoluteForFamily(pathFamilyOf({ root }), p.path) ? p.path : joinForFamily(pathFamilyOf({ root }), root, p.path)) : root;
|
|
1341
|
+
const unexcludableUnder = async () => {
|
|
1342
|
+
if (deny?.unexcludableUnder === undefined)
|
|
1343
|
+
return false;
|
|
1344
|
+
if (deny.unexcludableUnder(scope))
|
|
1345
|
+
return true;
|
|
1346
|
+
const view = await homeViewsOf(env, signal).of(scope);
|
|
1347
|
+
return view !== undefined && deny.unexcludableUnder(view);
|
|
1348
|
+
};
|
|
1349
|
+
if (!(await unexcludableUnder()) && (await detectRipgrep(env)))
|
|
1312
1350
|
return rgGrepDetailed(env, root, p, signal, deny);
|
|
1313
1351
|
const denyOut = {};
|
|
1314
1352
|
const { text, rows } = await jsGrepDetailed(env, root, p, signal, undefined, deny, denyOut);
|