@sema-agent/core 5.29.0 → 5.31.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 +141 -0
- package/dist/agents/send-message-tool.js +2 -0
- package/dist/agents/subagent.d.ts +2 -0
- package/dist/agents/subagent.js +6 -0
- package/dist/agents/teacher.js +2 -0
- package/dist/agents/verify.js +2 -0
- package/dist/core/auto-compaction.d.ts +5 -1
- package/dist/core/auto-compaction.js +10 -1
- package/dist/core/checkpoint-store.d.ts +51 -5
- package/dist/core/checkpoint-store.js +2 -1
- package/dist/core/hooks.d.ts +12 -1
- package/dist/core/hooks.js +8 -2
- package/dist/core/permission-rules.js +2 -2
- package/dist/core/runner/prepare-task.d.ts +21 -5
- package/dist/core/runner/prepare-task.js +115 -26
- package/dist/core/runner/runtask.js +29 -3
- package/dist/core/runner/session-rule-policy.d.ts +3 -2
- package/dist/core/runner/tool-output-projection.js +1 -1
- package/dist/core/sensitive-path-policy.js +5 -16
- package/dist/core/store-contracts/tool-result-store-contract.d.ts +4 -1
- package/dist/core/store-contracts/tool-result-store-contract.js +26 -1
- package/dist/core/tighten-task-spec.js +18 -0
- package/dist/core/tool-policy.d.ts +20 -1
- package/dist/core/tool-policy.js +31 -4
- package/dist/core/tool-result-store.d.ts +6 -4
- package/dist/core/tool-result-store.js +3 -1
- package/dist/core/types.d.ts +63 -1
- package/dist/engine/harness/types.d.ts +10 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/orchestration/run-workflow-tool.d.ts +26 -0
- package/dist/orchestration/run-workflow-tool.js +7 -4
- package/dist/orchestration/workflow-governance.d.ts +53 -3
- package/dist/orchestration/workflow-governance.js +162 -25
- package/dist/orchestration/workflow-primitives.d.ts +15 -1
- package/dist/orchestration/workflow-primitives.js +13 -2
- package/dist/prompt-assembly/epoch.js +2 -0
- package/dist/prompt-assembly/packs/sema-default.js +2 -2
- package/dist/prompt-assembly/types.d.ts +4 -0
- package/dist/prompts/default.d.ts +14 -9
- package/dist/prompts/default.js +13 -3
- package/dist/tools/fs/bash-readonly-classifier.d.ts +53 -4
- package/dist/tools/fs/bash-readonly-classifier.js +148 -16
- package/dist/tools/fs/fs-bash.d.ts +7 -0
- package/dist/tools/fs/fs-bash.js +8 -3
- package/dist/tools/fs/fs-pdf.d.ts +1 -1
- package/dist/tools/fs/fs-pdf.js +2 -2
- package/dist/tools/fs/fs-read.d.ts +1 -1
- package/dist/tools/fs/fs-read.js +11 -7
- package/dist/tools/fs/fs-search-tools.d.ts +4 -2
- package/dist/tools/fs/fs-search-tools.js +15 -8
- package/dist/tools/fs/fs-shared.d.ts +5 -1
- package/dist/tools/fs/fs-shared.js +8 -3
- package/dist/tools/fs/index.d.ts +18 -0
- package/dist/tools/fs/index.js +13 -2
- package/dist/tools/fs/read-deny.d.ts +110 -0
- package/dist/tools/fs/read-deny.js +159 -0
- package/dist/tools/fs/read-face.d.ts +49 -0
- package/dist/tools/fs/read-face.js +38 -0
- package/dist/tools/fs/repo-map.d.ts +3 -1
- package/dist/tools/fs/repo-map.js +11 -5
- package/dist/tools/fs/safety.d.ts +34 -11
- package/dist/tools/fs/safety.js +108 -8
- package/dist/tools/fs/search.d.ts +54 -5
- package/dist/tools/fs/search.js +107 -23
- package/package.json +1 -1
package/dist/tools/fs/fs-read.js
CHANGED
|
@@ -7,14 +7,17 @@ import { MCP_IMAGE_MAX_BASE64, IMAGE_TARGET_RAW_SIZE } from "../../core/mcp.js";
|
|
|
7
7
|
import { PDF_MAX_PAGES_PER_READ, pdfMagicMatches } from "./pdf.js";
|
|
8
8
|
import { MAX_READ_BYTES, SLICED_READ_MAX_BYTES, MAX_IMAGE_READ_BYTES, MAX_IMAGE_DOWNSAMPLE_INPUT_BYTES, NO_DOWNSAMPLER_IMAGE_CAP_HINT, resolveAutoDownsampler, MAX_READ_OUTPUT_CHARS, READ_CYBER_REMINDER, FILE_PATH_PARAMS, countLines, seededFileUnchangedReminder, enoentMessage, } from "./fs-shared.js";
|
|
9
9
|
import { readPdfFile, pdfResultToToolReturn } from "./fs-pdf.js";
|
|
10
|
-
export function createReadFileTool(env, state, rootCanonical, cwdRef, additionalRoots, imageDownsampler, pdfCapabilities, bgOutputReadExemption, readCyberReminder) {
|
|
10
|
+
export function createReadFileTool(env, state, rootCanonical, cwdRef, additionalRoots, imageDownsampler, pdfCapabilities, bgOutputReadExemption, readCyberReminder, readDeny, readFace) {
|
|
11
11
|
const cyberReminder = readCyberReminder === false ? "" : READ_CYBER_REMINDER;
|
|
12
|
+
const pathBoundLine = readFace === "open"
|
|
13
|
+
? "- `file_path` may be relative (resolved against the tracked working directory) or absolute. Reads are not confined to the workspace roots; a small sensitive-path deny list applies.\n"
|
|
14
|
+
: "- `file_path` may be relative (resolved against the tracked working directory) or absolute (within the configured roots).\n";
|
|
12
15
|
return defineTool({
|
|
13
16
|
name: "Read",
|
|
14
17
|
contract: { contractId: "core.read@1", implementationRevision: "1" },
|
|
15
18
|
description: "Reads a file from the local filesystem.\n" +
|
|
16
19
|
"\n" +
|
|
17
|
-
|
|
20
|
+
pathBoundLine +
|
|
18
21
|
"- By default, it reads the whole file (from `offset`, 1-based); a very large file is served as a partial view with an explicit marker and the next-page call.\n" +
|
|
19
22
|
"- You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to read the whole file by not providing these parameters\n" +
|
|
20
23
|
"- Results are returned using cat -n format, with line numbers starting at 1\n" +
|
|
@@ -53,7 +56,7 @@ export function createReadFileTool(env, state, rootCanonical, cwdRef, additional
|
|
|
53
56
|
content: text,
|
|
54
57
|
details: { type: "file_unchanged", reason, file: { filePath: path, startLine, endLine, totalLines } },
|
|
55
58
|
});
|
|
56
|
-
const r = await resolveKey(env, rootCanonical, path, ctx.signal, cwdRef?.current, additionalRoots, bgOutputReadExemption === undefined ? undefined : (key) => bgOutputReadExemption(key, ctx));
|
|
59
|
+
const r = await resolveKey(env, rootCanonical, path, ctx.signal, cwdRef?.current, additionalRoots, bgOutputReadExemption === undefined ? undefined : (key) => bgOutputReadExemption(key, ctx), readDeny, readFace);
|
|
57
60
|
if (!r.ok)
|
|
58
61
|
return errorResult(violationText("Read", r.violation), violationDetails(r.violation));
|
|
59
62
|
const isNb = isNotebookPath(r.key);
|
|
@@ -63,7 +66,7 @@ export function createReadFileTool(env, state, rootCanonical, cwdRef, additional
|
|
|
63
66
|
if (!meta.ok) {
|
|
64
67
|
const ex = await env.exists(r.key, ctx.signal);
|
|
65
68
|
if (ex.ok && !ex.value)
|
|
66
|
-
return errorResult(`Error (Read): ${await enoentMessage(env, r.key, cwdRef?.current ?? rootCanonical, ctx.signal)}`);
|
|
69
|
+
return errorResult(`Error (Read): ${await enoentMessage(env, r.key, cwdRef?.current ?? rootCanonical, ctx.signal, readDeny)}`);
|
|
67
70
|
return errorResult(`Error (Read): cannot stat image "${path}" to verify its size before reading: ${meta.error.message}`);
|
|
68
71
|
}
|
|
69
72
|
if (meta.value.kind === "directory") {
|
|
@@ -121,7 +124,7 @@ export function createReadFileTool(env, state, rootCanonical, cwdRef, additional
|
|
|
121
124
|
};
|
|
122
125
|
}
|
|
123
126
|
if (r.key.toLowerCase().endsWith(".pdf")) {
|
|
124
|
-
return pdfResultToToolReturn(await readPdfFile(env, path, r.key, pages, ctx.signal, imageDownsampler, cwdRef?.current ?? rootCanonical, undefined, pdfCapabilities));
|
|
127
|
+
return pdfResultToToolReturn(await readPdfFile(env, path, r.key, pages, ctx.signal, imageDownsampler, cwdRef?.current ?? rootCanonical, undefined, pdfCapabilities, readDeny));
|
|
125
128
|
}
|
|
126
129
|
const binaryExt = hasBinaryExtension(r.key);
|
|
127
130
|
const binaryExtRefusal = () => errorResult(`Error (Read): "${path}" appears to be a binary file (by extension); this tool reads UTF-8 text only.`);
|
|
@@ -146,7 +149,8 @@ export function createReadFileTool(env, state, rootCanonical, cwdRef, additional
|
|
|
146
149
|
else {
|
|
147
150
|
const ex = await env.exists(r.key, ctx.signal);
|
|
148
151
|
if (ex.ok && !ex.value)
|
|
149
|
-
return errorResult(`Error (Read): ${await enoentMessage(env, r.key, cwdRef?.current ?? rootCanonical, ctx.signal)}`);
|
|
152
|
+
return errorResult(`Error (Read): ${await enoentMessage(env, r.key, cwdRef?.current ?? rootCanonical, ctx.signal, readDeny)}`);
|
|
153
|
+
return errorResult(`Error (Read): cannot determine the type of "${path.slice(0, 300)}" (stat failed: ${info.error.message.slice(0, 200)}); refusing to open it — if it is a FIFO/socket/device, reading it would hang. Retry if this was transient, or inspect it with Bash (e.g. \`file\`, \`ls -l\`).`);
|
|
150
154
|
}
|
|
151
155
|
const readBin = await env.readBinaryFile(r.key, ctx.signal);
|
|
152
156
|
if (!readBin.ok)
|
|
@@ -165,7 +169,7 @@ export function createReadFileTool(env, state, rootCanonical, cwdRef, additional
|
|
|
165
169
|
return errorResult(`Error (Read): "${path}" is too large to read in full (${readSize} bytes > ${MAX_READ_BYTES}-byte cap); pass an explicit offset/limit to read a slice, or use grep to search it instead.`);
|
|
166
170
|
}
|
|
167
171
|
if (pdfMagicMatches(readBin.value)) {
|
|
168
|
-
return pdfResultToToolReturn(await readPdfFile(env, path, r.key, pages, ctx.signal, imageDownsampler, cwdRef?.current ?? rootCanonical, readBin.value, pdfCapabilities));
|
|
172
|
+
return pdfResultToToolReturn(await readPdfFile(env, path, r.key, pages, ctx.signal, imageDownsampler, cwdRef?.current ?? rootCanonical, readBin.value, pdfCapabilities, readDeny));
|
|
169
173
|
}
|
|
170
174
|
const magicFormat = binaryMagicFormat(readBin.value);
|
|
171
175
|
if (magicFormat !== undefined) {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { AgentTool, ExecutionEnv } from "../../internal/harness-types.js";
|
|
2
2
|
import type { ToolEffect } from "../../core/types.js";
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
import type { ReadDenyMatcher } from "./read-deny.js";
|
|
4
|
+
import type { ReadFace } from "./read-face.js";
|
|
5
|
+
export declare function createGrepTool(env: ExecutionEnv, rootCanonical: string, additionalRoots?: readonly string[], readDeny?: ReadDenyMatcher, readFace?: ReadFace): AgentTool;
|
|
6
|
+
export declare function createGlobTool(env: ExecutionEnv, rootCanonical: string, additionalRoots?: readonly string[], readDeny?: ReadDenyMatcher, readFace?: ReadFace): AgentTool;
|
|
5
7
|
/** Static side-effect class of every hand tool, by name (design/44 §3). Used by prepare-task to (a) feed
|
|
6
8
|
* wake/resume reconciliation and (b) drive the verifier read-only boundary. Every mutating hand tool is
|
|
7
9
|
* `write` (RB-264 ⑥W1 folded `Write` back in — see below); `bash` is `write` (a command can do anything);
|
|
@@ -2,7 +2,7 @@ import { Type } from "typebox";
|
|
|
2
2
|
import { defineTool, errorResult } from "../../core/tools.js";
|
|
3
3
|
import { resolveKey, violationText, violationDetails } from "./safety.js";
|
|
4
4
|
import { runGrepDetailed, runGlobDetailed, splitAbsoluteGlobPattern, invalidGlobTokens } from "./search.js";
|
|
5
|
-
export function createGrepTool(env, rootCanonical, additionalRoots) {
|
|
5
|
+
export function createGrepTool(env, rootCanonical, additionalRoots, readDeny, readFace) {
|
|
6
6
|
return defineTool({
|
|
7
7
|
name: "Grep",
|
|
8
8
|
contract: { contractId: "core.grep@1", implementationRevision: "1" },
|
|
@@ -69,10 +69,17 @@ export function createGrepTool(env, rootCanonical, additionalRoots) {
|
|
|
69
69
|
}
|
|
70
70
|
let scoped = a.path;
|
|
71
71
|
if (a.path !== undefined) {
|
|
72
|
-
const r = await resolveKey(env, rootCanonical, a.path, ctx.signal, rootCanonical, additionalRoots);
|
|
72
|
+
const r = await resolveKey(env, rootCanonical, a.path, ctx.signal, rootCanonical, additionalRoots, undefined, readDeny, readFace);
|
|
73
73
|
if (!r.ok)
|
|
74
74
|
return errorResult(violationText("Grep", r.violation), violationDetails(r.violation));
|
|
75
75
|
scoped = r.key;
|
|
76
|
+
const info = await env.fileInfo(scoped, ctx.signal);
|
|
77
|
+
if (!info.ok) {
|
|
78
|
+
const ex = await env.exists(scoped, ctx.signal);
|
|
79
|
+
if (!(ex.ok && !ex.value)) {
|
|
80
|
+
return errorResult(`Error (Grep): cannot determine the type of "${a.path.slice(0, 300)}" (stat failed: ${info.error.message.slice(0, 200)}); refusing to search it — if it is a FIFO/socket/device, reading it would hang. Retry if this was transient.`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
76
83
|
}
|
|
77
84
|
const grepRun = await runGrepDetailed(env, rootCanonical, {
|
|
78
85
|
...a,
|
|
@@ -83,7 +90,7 @@ export function createGrepTool(env, rootCanonical, additionalRoots) {
|
|
|
83
90
|
context_before: a.context_before ?? a["-B"],
|
|
84
91
|
ignore_case: a.ignore_case ?? a["-i"],
|
|
85
92
|
only_matching: a.only_matching ?? a["-o"],
|
|
86
|
-
}, ctx.signal);
|
|
93
|
+
}, ctx.signal, readDeny);
|
|
87
94
|
const text = grepRun.text;
|
|
88
95
|
if (text.startsWith("Error (grep)") || text.startsWith("Error (Grep)"))
|
|
89
96
|
return errorResult(text);
|
|
@@ -148,12 +155,12 @@ export function createGrepTool(env, rootCanonical, additionalRoots) {
|
|
|
148
155
|
}
|
|
149
156
|
return {
|
|
150
157
|
content: text,
|
|
151
|
-
details: { type: "grep", mode, ...detailFields, ...(grepRun.degraded ?? {}) },
|
|
158
|
+
details: { type: "grep", mode, ...detailFields, ...(grepRun.degraded ?? {}), ...(grepRun.withheld !== undefined ? { withheld: grepRun.withheld } : {}) },
|
|
152
159
|
};
|
|
153
160
|
},
|
|
154
161
|
});
|
|
155
162
|
}
|
|
156
|
-
export function createGlobTool(env, rootCanonical, additionalRoots) {
|
|
163
|
+
export function createGlobTool(env, rootCanonical, additionalRoots, readDeny, readFace) {
|
|
157
164
|
return defineTool({
|
|
158
165
|
name: "Glob",
|
|
159
166
|
contract: { contractId: "core.glob@1", implementationRevision: "1" },
|
|
@@ -188,17 +195,17 @@ export function createGlobTool(env, rootCanonical, additionalRoots) {
|
|
|
188
195
|
}
|
|
189
196
|
let scoped = path;
|
|
190
197
|
if (path !== undefined) {
|
|
191
|
-
const r = await resolveKey(env, rootCanonical, path, ctx.signal, rootCanonical, additionalRoots);
|
|
198
|
+
const r = await resolveKey(env, rootCanonical, path, ctx.signal, rootCanonical, additionalRoots, undefined, readDeny, readFace);
|
|
192
199
|
if (!r.ok)
|
|
193
200
|
return errorResult(violationText("Glob", r.violation), violationDetails(r.violation));
|
|
194
201
|
scoped = r.key;
|
|
195
202
|
}
|
|
196
|
-
const r2 = await runGlobDetailed(env, rootCanonical, pattern, { path: scoped, max: max_results }, ctx.signal);
|
|
203
|
+
const r2 = await runGlobDetailed(env, rootCanonical, pattern, { path: scoped, max: max_results, ...(readDeny !== undefined ? { deny: readDeny } : {}) }, ctx.signal);
|
|
197
204
|
if (r2.error !== undefined)
|
|
198
205
|
return errorResult(r2.error);
|
|
199
206
|
return {
|
|
200
207
|
content: r2.text,
|
|
201
|
-
details: { type: "glob", filenames: r2.filenames, numFiles: r2.numFiles, truncated: r2.truncated, durationMs: r2.durationMs, totalMatches: r2.totalMatches, countIsComplete: r2.countIsComplete },
|
|
208
|
+
details: { type: "glob", filenames: r2.filenames, numFiles: r2.numFiles, truncated: r2.truncated, durationMs: r2.durationMs, totalMatches: r2.totalMatches, countIsComplete: r2.countIsComplete, ...(r2.withheld !== undefined ? { withheld: r2.withheld } : {}) },
|
|
202
209
|
};
|
|
203
210
|
},
|
|
204
211
|
});
|
|
@@ -441,7 +441,11 @@ export declare function applyCompactionToReadFileState(state: ReadFileState, att
|
|
|
441
441
|
* plus a `Did you mean <sibling>?` correction when the parent directory holds a near-name (case
|
|
442
442
|
* variant / same stem different extension — the high-frequency self-heal path). Best-effort: a
|
|
443
443
|
* listDir failure just omits the suggestion. */
|
|
444
|
-
export declare function enoentMessage(env: ExecutionEnv, key: string, cwd: string, signal?: AbortSignal
|
|
444
|
+
export declare function enoentMessage(env: ExecutionEnv, key: string, cwd: string, signal?: AbortSignal, readDeny?: {
|
|
445
|
+
matchPath(path: string): {
|
|
446
|
+
pattern: string;
|
|
447
|
+
} | null;
|
|
448
|
+
}): Promise<string>;
|
|
445
449
|
/** Per-task mutable working directory shared by the shell and the path-taking fs tools (design/64 §16.3).
|
|
446
450
|
* Holds the RAW path (never canonicalized): bash `cd` updates `current`, and the fs tools resolve relative
|
|
447
451
|
* paths against it. Containment is still enforced per-op by resolveKey (canonicalize + within), so a `cd`
|
|
@@ -275,22 +275,27 @@ export function applyCompactionToReadFileState(state, attachedComplete, preserve
|
|
|
275
275
|
state.set(f.path, { hash: sha256(f.content), totalLines, truncated: false, view: { start: 1, end: displayLines }, lastReadAt: Date.now() });
|
|
276
276
|
}
|
|
277
277
|
}
|
|
278
|
-
export async function enoentMessage(env, key, cwd, signal) {
|
|
278
|
+
export async function enoentMessage(env, key, cwd, signal, readDeny) {
|
|
279
279
|
let msg = `File does not exist. Note: your current working directory is ${cwd}.`;
|
|
280
280
|
const sep = Math.max(key.lastIndexOf("/"), key.lastIndexOf("\\"));
|
|
281
281
|
if (sep > 0) {
|
|
282
282
|
const parent = key.slice(0, sep);
|
|
283
283
|
const name = key.slice(sep + 1);
|
|
284
|
+
if (readDeny?.matchPath(parent) != null)
|
|
285
|
+
return msg;
|
|
284
286
|
const cwdSepped = cwd.replace(/[\\/]+$/, "");
|
|
285
287
|
const cwdCandidate = `${cwdSepped}${cwd.includes("\\") ? "\\" : "/"}${name}`;
|
|
286
|
-
if (cwdCandidate !== key) {
|
|
288
|
+
if (cwdCandidate !== key && readDeny?.matchPath(cwdCandidate) == null) {
|
|
287
289
|
const hit = await env.exists(cwdCandidate, signal).catch(() => undefined);
|
|
288
290
|
if (hit?.ok && hit.value)
|
|
289
291
|
return `${msg} Did you mean ${cwdCandidate}?`;
|
|
290
292
|
}
|
|
291
293
|
const listing = await env.listDir(parent, signal).catch(() => undefined);
|
|
292
294
|
if (listing?.ok) {
|
|
293
|
-
const names = listing.value
|
|
295
|
+
const names = listing.value
|
|
296
|
+
.filter((e) => e.kind !== "directory")
|
|
297
|
+
.map((e) => e.path.slice(Math.max(e.path.lastIndexOf("/"), e.path.lastIndexOf("\\")) + 1))
|
|
298
|
+
.filter((n2) => readDeny?.matchPath(`${parent}/${n2}`) == null);
|
|
294
299
|
const suggestion = similarNameSuggestion(names, name);
|
|
295
300
|
if (suggestion)
|
|
296
301
|
msg += ` Did you mean ${suggestion}?`;
|
package/dist/tools/fs/index.d.ts
CHANGED
|
@@ -10,6 +10,9 @@ export * from "./fs-write.js";
|
|
|
10
10
|
export * from "./fs-search-tools.js";
|
|
11
11
|
export * from "./bash-readonly-classifier.js";
|
|
12
12
|
export * from "./fs-bash.js";
|
|
13
|
+
export * from "./read-deny.js";
|
|
14
|
+
import { type ReadDenyEntry } from "./read-deny.js";
|
|
15
|
+
export * from "./read-face.js";
|
|
13
16
|
import { type CwdRef, type ReadImageDownsamplerOption } from "./fs-shared.js";
|
|
14
17
|
/** Options for {@link createHandsToolkit}. */
|
|
15
18
|
export interface HandsToolkitOptions {
|
|
@@ -115,6 +118,21 @@ export interface HandsToolkitOptions {
|
|
|
115
118
|
* this run's roster (the Runner mounts Monitor, this band never does). `false` drops the gh
|
|
116
119
|
* rate-limit hint's Monitor clause; absent ⇒ historic full wording (byte-compat). */
|
|
117
120
|
monitorToolActive?: boolean;
|
|
121
|
+
/** design/199 件B — ADDITIONS to the built-in sensitive-path READ deny set
|
|
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 at every
|
|
124
|
+
* layer (D-4 zero-shrink ruling): the built-ins are always in force, `[]` ≡ absent (union
|
|
125
|
+
* identity), and there is no replacement escape hatch. Bad entry shapes throw at wiring time
|
|
126
|
+
* (#123). The write faces are untouched (their guard is createSensitivePathPolicy). */
|
|
127
|
+
readDenyPatterns?: readonly ReadDenyEntry[];
|
|
128
|
+
/** design/199 件A — the READ-face containment state for the structured read faces
|
|
129
|
+
* ({@link import("./read-face.js").ReadFace}; resolved through the SAME
|
|
130
|
+
* {@link import("./read-face.js").resolveReadFace} order prepare-task uses). Absent ⇒ the
|
|
131
|
+
* resolution order's default = "roots" (D-1b: the engine never opens implicitly). "open" skips
|
|
132
|
+
* 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). Refused loudly beside `readOnly: true`
|
|
134
|
+
* (the verifier mount's containment is load-bearing). Never affects the write faces. */
|
|
135
|
+
readFace?: "open" | "roots";
|
|
118
136
|
}
|
|
119
137
|
/**
|
|
120
138
|
* Build the per-task hand tool band over an injected env + fresh per-task read state (design/44 §11 A).
|
package/dist/tools/fs/index.js
CHANGED
|
@@ -9,7 +9,11 @@ export * from "./fs-write.js";
|
|
|
9
9
|
export * from "./fs-search-tools.js";
|
|
10
10
|
export * from "./bash-readonly-classifier.js";
|
|
11
11
|
export * from "./fs-bash.js";
|
|
12
|
+
export * from "./read-deny.js";
|
|
12
13
|
import { BASH_READONLY_DEFAULT_ALLOW, } from "./bash-readonly-classifier.js";
|
|
14
|
+
import { compileReadDeny } from "./read-deny.js";
|
|
15
|
+
import { resolveReadFace } from "./read-face.js";
|
|
16
|
+
export * from "./read-face.js";
|
|
13
17
|
import {} from "./fs-shared.js";
|
|
14
18
|
import { createReadFileTool } from "./fs-read.js";
|
|
15
19
|
import { createEditFileTool, createWriteFileTool, createNotebookEditTool } from "./fs-write.js";
|
|
@@ -29,13 +33,20 @@ export function createHandsToolkit(env, readFileState, rootCanonical, opts = {})
|
|
|
29
33
|
scope: c.principal ?? opts.taskScope,
|
|
30
34
|
...(opts.sessionId !== undefined ? { sessionId: opts.sessionId } : {}),
|
|
31
35
|
}, env);
|
|
36
|
+
const readDeny = compileReadDeny(opts.readDenyPatterns ?? [], "HandsToolkitOptions.readDenyPatterns");
|
|
37
|
+
const readFace = resolveReadFace({
|
|
38
|
+
depsReadFace: opts.readFace,
|
|
39
|
+
readOnlyMount: readOnly,
|
|
40
|
+
orgGoverned: false,
|
|
41
|
+
fullShellReachable: includeShell && !readOnly,
|
|
42
|
+
});
|
|
32
43
|
const tools = [
|
|
33
|
-
createReadFileTool(env, readFileState, rootCanonical, readOnly ? undefined : cwdRef, readFaceRoots, opts.readImageDownsampler, opts.pdfModelCapabilities, bgOutputReadExemption, opts.readCyberReminder),
|
|
44
|
+
createReadFileTool(env, readFileState, rootCanonical, readOnly ? undefined : cwdRef, readFaceRoots, opts.readImageDownsampler, opts.pdfModelCapabilities, bgOutputReadExemption, opts.readCyberReminder, readDeny, readFace),
|
|
34
45
|
];
|
|
35
46
|
if (!readOnly) {
|
|
36
47
|
tools.push(createEditFileTool(env, readFileState, rootCanonical, cwdRef, additionalRoots, opts.beforeWrite), createWriteFileTool(env, readFileState, rootCanonical, cwdRef, additionalRoots, opts.beforeWrite), createNotebookEditTool(env, readFileState, rootCanonical, cwdRef, additionalRoots, opts.beforeWrite));
|
|
37
48
|
}
|
|
38
|
-
tools.push(createGrepTool(env, rootCanonical, readFaceRoots), createGlobTool(env, rootCanonical, readFaceRoots), createRepoMapTool(env, rootCanonical, readFaceRoots));
|
|
49
|
+
tools.push(createGrepTool(env, rootCanonical, readFaceRoots, readDeny, readFace), createGlobTool(env, rootCanonical, readFaceRoots, readDeny, readFace), createRepoMapTool(env, rootCanonical, readFaceRoots, readDeny, readFace));
|
|
39
50
|
if (includeShell) {
|
|
40
51
|
tools.push(readOnly
|
|
41
52
|
? createBashReadonlyTool(env, rootCanonical, new Set(bashReadonlyAllow ?? BASH_READONLY_DEFAULT_ALLOW), {
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One deny entry: a `/`-separated run of path SEGMENTS. `*` matches any run of non-separator
|
|
3
|
+
* characters within one segment and is the ONLY metacharacter (everything else is literal). The run
|
|
4
|
+
* may match ANYWHERE in the judged path (`.ssh` covers `~/.ssh/…` and any other `.ssh` directory).
|
|
5
|
+
* String shorthand ≡ `{ pattern }`. Matching is ASCII-case-insensitive by default (a case-insensitive
|
|
6
|
+
* filesystem serves `.SSH` and `.ssh` as the same directory); `caseSensitive: true` opts a single
|
|
7
|
+
* entry out. Unicode normalization (NFC/NFD) and volume-level case semantics are NOT modeled (v1
|
|
8
|
+
* residual, shared with the write guard; the canonical view — realpath returns the on-disk spelling —
|
|
9
|
+
* covers half of it).
|
|
10
|
+
*/
|
|
11
|
+
export type ReadDenyEntry = string | {
|
|
12
|
+
pattern: string;
|
|
13
|
+
caseSensitive?: boolean;
|
|
14
|
+
};
|
|
15
|
+
/** The normalized (validated, defaulted) form of one entry. */
|
|
16
|
+
export interface NormalizedReadDenyEntry {
|
|
17
|
+
pattern: string;
|
|
18
|
+
caseSensitive: boolean;
|
|
19
|
+
}
|
|
20
|
+
/** A deny verdict: which entry's pattern matched. */
|
|
21
|
+
export interface ReadDenyHit {
|
|
22
|
+
pattern: string;
|
|
23
|
+
/** The view string the pattern actually matched (set by {@link CompiledReadDeny.matchTarget}: the
|
|
24
|
+
* canonical key, or the lexical view when only the SPELLING matched). Disclosure must cite the
|
|
25
|
+
* matched view — under a symlink/case alias the two strings differ, and naming the other one
|
|
26
|
+
* sends the reader chasing a path the pattern does not match. */
|
|
27
|
+
matchedView?: string;
|
|
28
|
+
}
|
|
29
|
+
/** One compiled ripgrep glob flag for the traversal legs (`--iglob` = case-insensitive entry). */
|
|
30
|
+
export interface ReadDenyRgGlob {
|
|
31
|
+
flag: "--glob" | "--iglob";
|
|
32
|
+
glob: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The compiled deny judge — ONE compilation, consumed by every judgment point (target judgment in
|
|
36
|
+
* resolveKey, the JS walker, the rg exclusion/probe legs, the classify operand probe, the ENOENT
|
|
37
|
+
* sibling filter, the attachment reader), so the legs cannot drift apart.
|
|
38
|
+
*/
|
|
39
|
+
export interface ReadDenyMatcher {
|
|
40
|
+
/** Normalized entries (built-ins first, then additions; exact duplicates folded). The disclosure /
|
|
41
|
+
* persistence face. */
|
|
42
|
+
readonly entries: readonly NormalizedReadDenyEntry[];
|
|
43
|
+
/** Judge ONE path view (segment-window semantics). Returns the first matching entry, or null. */
|
|
44
|
+
matchPath(path: string): ReadDenyHit | null;
|
|
45
|
+
/** §3.3 target judgment: canonical view + lexical view; EITHER hit decides. */
|
|
46
|
+
matchTarget(canonicalKey: string, lexicalView?: string): ReadDenyHit | null;
|
|
47
|
+
/** Traversal exclusions for the rg legs: `!`-polarity glob pairs — `!**\/<p>` plus `!**\/<p>/…`
|
|
48
|
+
* (the entry itself and its descendants). */
|
|
49
|
+
readonly rgExclusionGlobs: readonly ReadDenyRgGlob[];
|
|
50
|
+
/** Positive-polarity twins of the exclusions, for the bounded existence probe (first hit = entries
|
|
51
|
+
* matching the deny list exist under the scope and were excluded). */
|
|
52
|
+
readonly rgProbeGlobs: readonly ReadDenyRgGlob[];
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* READ_FACE_DEFAULT_DENY_ENTRIES — the built-in table (D-4 ruling: ONE tier, credential-class path
|
|
56
|
+
* families; the design's `outside_workspace` tier was ruled OUT, and the dotenv family is deliberately
|
|
57
|
+
* NOT here — workspace `.env` files are working material for the tasks this engine runs).
|
|
58
|
+
*
|
|
59
|
+
* Each row states its tradeoff. Rows are matched as segment runs anywhere in the path, so a repo
|
|
60
|
+
* fixture spelled `fixtures/.ssh/id_rsa` is refused too — deliberate: the refusal names the pattern,
|
|
61
|
+
* and a false refusal on a fixture is the cheap direction (deny errs strict).
|
|
62
|
+
*
|
|
63
|
+
* NOT listed, deliberately: `.env`/`.env.*` (ruled out — workspace material; the WRITE guard still
|
|
64
|
+
* covers them); `*.pem`/`*.key`/`*.p12`/`*.pfx` (repo certificates/test keys are routinely READ —
|
|
65
|
+
* refusing them breaks ordinary work, the write guard covers the mutation direction); `.git/hooks`/
|
|
66
|
+
* `.git/config` (reads are harmless; writes are the escalation and stay guarded); `.claude*`/
|
|
67
|
+
* `.mcp.json` (agent config is routinely read for debugging); the engine's own data root
|
|
68
|
+
* (transcript integrity policy owns it with an ask on the write face).
|
|
69
|
+
*/
|
|
70
|
+
export declare const READ_FACE_DEFAULT_DENY_ENTRIES: readonly ReadDenyEntry[];
|
|
71
|
+
/** A compiled segment-run pattern (shared engine — the write guard delegates here too). */
|
|
72
|
+
export interface CompiledSegmentPattern {
|
|
73
|
+
raw: string;
|
|
74
|
+
segments: RegExp[];
|
|
75
|
+
/** Fold BOTH sides through {@link asciiLower} before testing (compile lowered the pattern side). */
|
|
76
|
+
foldAscii: boolean;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Compile one segment-glob pattern (shared engine). `fold`:
|
|
80
|
+
* · "none" — byte-exact segments;
|
|
81
|
+
* · "ascii" — ASCII-case-insensitive (deny-set contract);
|
|
82
|
+
* · "unicode" — RegExp `i` flag (the write guard's historical host-keyed behavior — kept for it
|
|
83
|
+
* byte-identically; NOT used by the deny set).
|
|
84
|
+
* Returns null when the pattern reduces to zero segments — the CALLER owns its loud path (#123: the
|
|
85
|
+
* two consumers refuse with their own surface-specific texts).
|
|
86
|
+
*/
|
|
87
|
+
export declare function compileSegmentPattern(raw: string, fold: "none" | "ascii" | "unicode"): CompiledSegmentPattern | null;
|
|
88
|
+
/**
|
|
89
|
+
* Does the path contain a compiled pattern as a CONTIGUOUS run of full path segments? (Shared engine:
|
|
90
|
+
* `.git/hooks` matches `/repo/.git/hooks/pre-commit`; `.ssh` matches any `.ssh` segment.) Splits on
|
|
91
|
+
* BOTH separator families — win32 canonical keys are backslash-form. Returns the matched raw pattern
|
|
92
|
+
* or null.
|
|
93
|
+
*/
|
|
94
|
+
export declare function matchSegmentPatterns(path: string, compiled: readonly CompiledSegmentPattern[]): string | null;
|
|
95
|
+
/**
|
|
96
|
+
* codex r3 — the PURE validation/canonicality predicate over a PERSISTED deny entry (checkpoint v9
|
|
97
|
+
* face section), for the resume pre-CAS ladder: it must refuse BEFORE the approval CAS everything
|
|
98
|
+
* {@link compileReadDeny} would throw on AFTER it (a post-CAS compile throw consumes the human's
|
|
99
|
+
* approval and strands the row), plus non-canonical spellings (normalizeEntry canonicalizes at every
|
|
100
|
+
* mint, so a persisted non-canonical pattern was not minted by any release = damaged). Returns the
|
|
101
|
+
* refusal reason, or null when the entry is valid.
|
|
102
|
+
*/
|
|
103
|
+
export declare function persistedReadDenyEntryProblem(e: unknown): string | null;
|
|
104
|
+
/**
|
|
105
|
+
* Compile the read-face deny judge: built-in table ∪ additions (add-only at every layer — D-4
|
|
106
|
+
* zero-shrink; `[]` additions ≡ absent, deliberately a no-op rather than a refusal: an array API
|
|
107
|
+
* cannot observe a "replace" intent, and union-with-empty is the identity, not a silent fallback).
|
|
108
|
+
* Bad entry shapes / zero-segment patterns throw loudly, naming the layer (#123).
|
|
109
|
+
*/
|
|
110
|
+
export declare function compileReadDeny(additions?: readonly ReadDenyEntry[], layer?: string): ReadDenyMatcher;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
export const READ_FACE_DEFAULT_DENY_ENTRIES = [
|
|
2
|
+
".ssh",
|
|
3
|
+
"id_rsa*",
|
|
4
|
+
"id_ed25519*",
|
|
5
|
+
"id_ecdsa*",
|
|
6
|
+
".gnupg",
|
|
7
|
+
".aws",
|
|
8
|
+
".config/gcloud",
|
|
9
|
+
".azure",
|
|
10
|
+
".kube",
|
|
11
|
+
".netrc",
|
|
12
|
+
"_netrc",
|
|
13
|
+
".git-credentials",
|
|
14
|
+
".docker/config.json",
|
|
15
|
+
".config/gh",
|
|
16
|
+
".npmrc",
|
|
17
|
+
".pypirc",
|
|
18
|
+
".local/share/keyrings",
|
|
19
|
+
"Library/Keychains",
|
|
20
|
+
".bash_history",
|
|
21
|
+
".zsh_history",
|
|
22
|
+
"Library/Application Support/Google/Chrome",
|
|
23
|
+
"Library/Application Support/Firefox",
|
|
24
|
+
"Library/Safari",
|
|
25
|
+
".config/google-chrome",
|
|
26
|
+
".config/chromium",
|
|
27
|
+
".mozilla/firefox",
|
|
28
|
+
"AppData/Local/Google/Chrome/User Data",
|
|
29
|
+
"AppData/Local/Microsoft/Edge/User Data",
|
|
30
|
+
"AppData/Roaming/Mozilla/Firefox",
|
|
31
|
+
".bitcoin",
|
|
32
|
+
".ethereum",
|
|
33
|
+
".electrum",
|
|
34
|
+
"Library/Application Support/Exodus",
|
|
35
|
+
"Library/Application Support/Ledger Live",
|
|
36
|
+
"wallet.dat",
|
|
37
|
+
];
|
|
38
|
+
function asciiLower(s) {
|
|
39
|
+
let out = "";
|
|
40
|
+
for (let i = 0; i < s.length; i++) {
|
|
41
|
+
const c = s.charCodeAt(i);
|
|
42
|
+
out += c >= 0x41 && c <= 0x5a ? String.fromCharCode(c + 32) : s[i];
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
export function compileSegmentPattern(raw, fold) {
|
|
47
|
+
const source = fold === "ascii" ? asciiLower(raw) : raw;
|
|
48
|
+
const parts = source.split("/").filter(Boolean);
|
|
49
|
+
if (parts.length === 0)
|
|
50
|
+
return null;
|
|
51
|
+
const flags = fold === "unicode" ? "i" : "";
|
|
52
|
+
const segments = parts.map((segment) => {
|
|
53
|
+
const escaped = segment.replace(/[.+^${}()|[\]\\?]/g, "\\$&").replace(/\*/g, "[^/]*");
|
|
54
|
+
return new RegExp(`^${escaped}$`, flags);
|
|
55
|
+
});
|
|
56
|
+
return { raw, segments, foldAscii: fold === "ascii" };
|
|
57
|
+
}
|
|
58
|
+
export function matchSegmentPatterns(path, compiled) {
|
|
59
|
+
const rawSegs = path.split(/[\\/]/).filter(Boolean);
|
|
60
|
+
let loweredSegs;
|
|
61
|
+
for (const pat of compiled) {
|
|
62
|
+
const segs = pat.foldAscii ? (loweredSegs ??= rawSegs.map(asciiLower)) : rawSegs;
|
|
63
|
+
const n = pat.segments.length;
|
|
64
|
+
for (let i = 0; i + n <= segs.length; i++) {
|
|
65
|
+
if (pat.segments.every((re, j) => re.test(segs[i + j] ?? "")))
|
|
66
|
+
return pat.raw;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
function rgGlobEscape(pattern) {
|
|
72
|
+
return pattern.replace(/[?[\]{}]/g, (c) => `[${c}]`);
|
|
73
|
+
}
|
|
74
|
+
function normalizeEntry(entry, layer) {
|
|
75
|
+
const shape = typeof entry === "string" ? { pattern: entry, caseSensitive: false } : entry;
|
|
76
|
+
if (typeof shape !== "object" || shape === null || typeof shape.pattern !== "string") {
|
|
77
|
+
throw new Error(`readDenyPatterns (${layer}): entry ${JSON.stringify(entry)} is not a pattern string or { pattern, caseSensitive? } object.`);
|
|
78
|
+
}
|
|
79
|
+
if (shape.caseSensitive !== undefined && typeof shape.caseSensitive !== "boolean") {
|
|
80
|
+
throw new Error(`readDenyPatterns (${layer}): entry ${JSON.stringify(entry)} has a non-boolean caseSensitive.`);
|
|
81
|
+
}
|
|
82
|
+
if (shape.pattern.includes("\\")) {
|
|
83
|
+
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 "/".`);
|
|
84
|
+
}
|
|
85
|
+
const segments = shape.pattern.split("/").filter(Boolean);
|
|
86
|
+
if (segments.length === 0) {
|
|
87
|
+
throw new Error(`readDenyPatterns (${layer}): pattern ${JSON.stringify(shape.pattern)} contains no path segments and would deny nothing. ` +
|
|
88
|
+
`Patterns are "/"-separated runs of path SEGMENTS (e.g. ".ssh", ".config/gcloud", "id_rsa*"); remove the entry or spell the segments.`);
|
|
89
|
+
}
|
|
90
|
+
return { pattern: segments.join("/"), caseSensitive: shape.caseSensitive === true };
|
|
91
|
+
}
|
|
92
|
+
export function persistedReadDenyEntryProblem(e) {
|
|
93
|
+
if (typeof e !== "object" || e === null)
|
|
94
|
+
return "entry is not an object";
|
|
95
|
+
const pattern = e.pattern;
|
|
96
|
+
const caseSensitive = e.caseSensitive;
|
|
97
|
+
if (typeof pattern !== "string")
|
|
98
|
+
return "pattern is not a string";
|
|
99
|
+
if (typeof caseSensitive !== "boolean")
|
|
100
|
+
return "caseSensitive is not a boolean";
|
|
101
|
+
if (pattern.includes("\\"))
|
|
102
|
+
return "pattern contains a backslash";
|
|
103
|
+
const segments = pattern.split("/").filter(Boolean);
|
|
104
|
+
if (segments.length === 0)
|
|
105
|
+
return "pattern has no path segments";
|
|
106
|
+
if (segments.join("/") !== pattern)
|
|
107
|
+
return "pattern is not in canonical a/b form";
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
export function compileReadDeny(additions = [], layer = "additions") {
|
|
111
|
+
const normalized = [];
|
|
112
|
+
const seen = new Set();
|
|
113
|
+
const push = (e) => {
|
|
114
|
+
const key = `${e.caseSensitive ? "s" : "i"}:${e.pattern}`;
|
|
115
|
+
if (seen.has(key))
|
|
116
|
+
return;
|
|
117
|
+
seen.add(key);
|
|
118
|
+
normalized.push(e);
|
|
119
|
+
};
|
|
120
|
+
for (const entry of READ_FACE_DEFAULT_DENY_ENTRIES)
|
|
121
|
+
push(normalizeEntry(entry, "built-in"));
|
|
122
|
+
for (const entry of additions)
|
|
123
|
+
push(normalizeEntry(entry, layer));
|
|
124
|
+
const compiled = normalized.map((e) => {
|
|
125
|
+
const c = compileSegmentPattern(e.pattern, e.caseSensitive ? "none" : "ascii");
|
|
126
|
+
if (c === null)
|
|
127
|
+
throw new Error(`readDenyPatterns: pattern ${JSON.stringify(e.pattern)} compiled to zero segments.`);
|
|
128
|
+
return c;
|
|
129
|
+
});
|
|
130
|
+
const rgExclusionGlobs = [];
|
|
131
|
+
const rgProbeGlobs = [];
|
|
132
|
+
for (const e of normalized) {
|
|
133
|
+
const flag = e.caseSensitive ? "--glob" : "--iglob";
|
|
134
|
+
const g = rgGlobEscape(e.pattern);
|
|
135
|
+
rgExclusionGlobs.push({ flag, glob: `!**/${g}` }, { flag, glob: `!**/${g}/**` });
|
|
136
|
+
rgProbeGlobs.push({ flag, glob: `**/${g}` }, { flag, glob: `**/${g}/**` });
|
|
137
|
+
}
|
|
138
|
+
const matchPath = (path) => {
|
|
139
|
+
const hit = matchSegmentPatterns(path, compiled);
|
|
140
|
+
return hit === null ? null : { pattern: hit };
|
|
141
|
+
};
|
|
142
|
+
return {
|
|
143
|
+
entries: normalized,
|
|
144
|
+
matchPath,
|
|
145
|
+
matchTarget(canonicalKey, lexicalView) {
|
|
146
|
+
const canonical = matchPath(canonicalKey);
|
|
147
|
+
if (canonical !== null)
|
|
148
|
+
return { ...canonical, matchedView: canonicalKey };
|
|
149
|
+
if (lexicalView !== undefined) {
|
|
150
|
+
const lexical = matchPath(lexicalView);
|
|
151
|
+
if (lexical !== null)
|
|
152
|
+
return { ...lexical, matchedView: lexicalView };
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
},
|
|
156
|
+
rgExclusionGlobs,
|
|
157
|
+
rgProbeGlobs,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** The read-face containment state. `"roots"` = the historical containment judgment, unchanged.
|
|
2
|
+
* `"open"` = the containment step is skipped (canonicalization, deny set, UNC out-of-set refusal
|
|
3
|
+
* and the type gates all still run). */
|
|
4
|
+
export type ReadFace = "open" | "roots";
|
|
5
|
+
/** Loud value gate (#123): a `readFace` seat carries exactly "open" | "roots" — anything else
|
|
6
|
+
* (case variants, truthy garbage) refuses at wiring time, never folds to a default. Exported
|
|
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 — the resolver only runs beside a mount, and a
|
|
9
|
+
* garbage value must not become silently legal on the hands-less path (5.30 re-review). */
|
|
10
|
+
export declare function assertReadFaceValue(v: unknown, seat: string): ReadFace | undefined;
|
|
11
|
+
/** Inputs to {@link resolveReadFace} — all structural/declaration facts, never permission modes
|
|
12
|
+
* (a deliberate axis separation: this shape carries facts, not verdicts). */
|
|
13
|
+
export interface ReadFaceInputs {
|
|
14
|
+
/** TaskSpec.readFace (raw — validated here). The TASK layer: may only tighten under governance. */
|
|
15
|
+
specReadFace?: unknown;
|
|
16
|
+
/** RunnerDeps.readFace / HandsToolkitOptions.readFace (raw) — the DEPLOYMENT's own declaration. */
|
|
17
|
+
depsReadFace?: unknown;
|
|
18
|
+
/** The verifier read-only mount (`handsReadOnly` / toolkit `readOnly`): its containment is a
|
|
19
|
+
* LOAD-BEARING wall (bash_readonly is genuinely confined by it) — never openable. */
|
|
20
|
+
readOnlyMount: boolean;
|
|
21
|
+
/** Org governance declared (RunnerDeps.permissionRuleOrg in place): default roots; the task layer
|
|
22
|
+
* may not open (D-2 — tighten-only under governance; the deps layer, being the deployment's own
|
|
23
|
+
* declaration, still may). */
|
|
24
|
+
orgGoverned: boolean;
|
|
25
|
+
/** D-6: is the FULL shell (`core.bash@1`) structurally reachable on this mount? Computed BEFORE
|
|
26
|
+
* tool-band assembly from spec-time facts (env kind / handsReadOnly / Bash exclusion — recon §3:
|
|
27
|
+
* every exclusion source is spec-time-frozen, so this is always computable). When false, the
|
|
28
|
+
* roots fence is the ONLY read boundary and the default stays roots even under a deps-layer
|
|
29
|
+
* "open"… no — see the resolution order: an EXPLICIT open still wins (row 4 is a default, not a
|
|
30
|
+
* clamp); what it changes is that NOTHING implicit opens a bash-less mount. */
|
|
31
|
+
fullShellReachable: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The ONE resolution order (§2.2), first hit wins. prepare-task AND createHandsToolkit both call
|
|
35
|
+
* this — a library-direct mount gets identical validation and identical defaults.
|
|
36
|
+
*
|
|
37
|
+
* 1. read-only (verifier) mount → ROOTS; an explicit "open" on the TASK seat is a genuine
|
|
38
|
+
* per-call CONTRADICTION and refuses loudly (#123 — never silently pick a side). An "open"
|
|
39
|
+
* on the DEPLOYMENT seat alone is a deployment-wide default, not a per-task assertion — it
|
|
40
|
+
* silently CLAMPS to roots (stricter-wins), same seat distinction row 2 already draws for
|
|
41
|
+
* org governance below.
|
|
42
|
+
* 2. org-governed: TaskSpec "open" refuses (task layer only tightens, D-2); deps "open" wins;
|
|
43
|
+
* otherwise ROOTS.
|
|
44
|
+
* 3. explicit seat: spec ?? deps.
|
|
45
|
+
* 4. bash-less mount (D-6): ROOTS (the fence is the only read boundary there — "the fence guards
|
|
46
|
+
* nothing bash reaches anyway" does not hold, so no implicit default may open it).
|
|
47
|
+
* 5. default: ROOTS (D-1b — the engine never opens implicitly).
|
|
48
|
+
*/
|
|
49
|
+
export declare function resolveReadFace(i: ReadFaceInputs): ReadFace;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export function assertReadFaceValue(v, seat) {
|
|
2
|
+
if (v === undefined)
|
|
3
|
+
return undefined;
|
|
4
|
+
if (v === "open" || v === "roots")
|
|
5
|
+
return v;
|
|
6
|
+
const e = new Error(`${seat}: invalid readFace value ${JSON.stringify(v)} — expected "open" or "roots" (bad values refuse loudly; nothing falls back to a default).`);
|
|
7
|
+
e.code = "config.read_face_invalid";
|
|
8
|
+
throw e;
|
|
9
|
+
}
|
|
10
|
+
export function resolveReadFace(i) {
|
|
11
|
+
const spec = assertReadFaceValue(i.specReadFace, "TaskSpec.readFace");
|
|
12
|
+
const deps = assertReadFaceValue(i.depsReadFace, "readFace (deployment seat)");
|
|
13
|
+
if (i.readOnlyMount) {
|
|
14
|
+
if (spec === "open") {
|
|
15
|
+
const e = new Error(`readFace: "open" contradicts the read-only (verifier) mount — that mount's containment is load-bearing (its read-only shell face is confined by it) and is never openable. Drop the readFace declaration or the readOnly mount.`);
|
|
16
|
+
e.code = "config.read_face_readonly_conflict";
|
|
17
|
+
throw e;
|
|
18
|
+
}
|
|
19
|
+
return "roots";
|
|
20
|
+
}
|
|
21
|
+
if (i.orgGoverned) {
|
|
22
|
+
if (spec === "open") {
|
|
23
|
+
const e = new Error(`TaskSpec.readFace: "open" is refused under organization governance — the task layer may only tighten (set RunnerDeps.readFace: "open" if the DEPLOYMENT declares the open read face).`);
|
|
24
|
+
e.code = "config.read_face_org_task_escalation";
|
|
25
|
+
throw e;
|
|
26
|
+
}
|
|
27
|
+
if (spec === "roots")
|
|
28
|
+
return "roots";
|
|
29
|
+
return deps ?? "roots";
|
|
30
|
+
}
|
|
31
|
+
if (spec !== undefined)
|
|
32
|
+
return spec;
|
|
33
|
+
if (deps !== undefined)
|
|
34
|
+
return deps;
|
|
35
|
+
if (!i.fullShellReachable)
|
|
36
|
+
return "roots";
|
|
37
|
+
return "roots";
|
|
38
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { AgentTool } from "../../internal/harness-types.js";
|
|
2
2
|
import type { ExecutionEnv } from "../../internal/harness-types.js";
|
|
3
|
+
import type { ReadDenyMatcher } from "./read-deny.js";
|
|
4
|
+
import type { ReadFace } from "./read-face.js";
|
|
3
5
|
/** Extract top-level symbol names (deduped, in first-seen order) from source text. A lightweight
|
|
4
6
|
* comment/docstring state machine (council #4) skips column-0 declaration-like lines that are really
|
|
5
7
|
* inside a `/* … */` block comment or a Python `"""`/`'''` docstring — otherwise e.g. a module
|
|
@@ -11,4 +13,4 @@ import type { ExecutionEnv } from "../../internal/harness-types.js";
|
|
|
11
13
|
* auto-promote tripwire (src/core/auto-promote.ts) consumes it in ESCALATE-ONLY mode — a clean scan
|
|
12
14
|
* contributes nothing; it can only ever raise an escalation, never clear one. */
|
|
13
15
|
export declare function extractSymbols(text: string): string[];
|
|
14
|
-
export declare function createRepoMapTool(env: ExecutionEnv, rootCanonical: string, additionalRoots?: readonly string[]): AgentTool;
|
|
16
|
+
export declare function createRepoMapTool(env: ExecutionEnv, rootCanonical: string, additionalRoots?: readonly string[], readDeny?: ReadDenyMatcher, readFace?: ReadFace): AgentTool;
|