@sema-agent/core 7.33.7 → 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 +357 -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/secret-env.d.ts +16 -2
- package/dist/core/secret-env.js +25 -1
- package/dist/core/sensitive-path-policy.d.ts +4 -2
- package/dist/core/sensitive-path-policy.js +39 -30
- package/dist/core/spec-contract.d.ts +15 -0
- package/dist/core/spec-contract.js +19 -10
- 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/kill-tree.js +4 -1
- package/dist/engine/execution-env/node-execution-env.d.ts +3 -0
- package/dist/engine/execution-env/node-execution-env.js +34 -35
- 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/run-spec.d.ts +4 -0
- package/dist/orchestration/run-spec.js +4 -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/stores/file/fs-atomic.js +2 -1
- 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 +3 -1
- package/test/export-surface.snapshot.json +103 -11
- package/wire-changes/core@7.33.8.json +165 -0
- package/wire-changes/core@7.34.0.json +2621 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { effectivePathSpellingOf } from "../../core/effective-path-target.js";
|
|
2
|
+
import { pathTargetOf } from "../../core/tool-registry.js";
|
|
3
|
+
import { fitMiddle, READ_DENY_SENTENCE_PART_MAX } from "./read-deny.js";
|
|
4
|
+
import { resolveKey, violationDetails } from "./safety.js";
|
|
5
|
+
import { grantOfferFor, parentDirLexically } from "./shell-read-judge.js";
|
|
6
|
+
const ADMISSION_MAX = 1024;
|
|
7
|
+
const VOUCH = { disposition: "vouch", finding: "none" };
|
|
8
|
+
export function createFileReadStation(opts) {
|
|
9
|
+
const admissions = new Map();
|
|
10
|
+
const remember = (toolCallId, key) => {
|
|
11
|
+
if (admissions.size >= ADMISSION_MAX) {
|
|
12
|
+
const oldest = admissions.keys().next().value;
|
|
13
|
+
if (oldest !== undefined)
|
|
14
|
+
admissions.delete(oldest);
|
|
15
|
+
}
|
|
16
|
+
admissions.set(toolCallId, key);
|
|
17
|
+
};
|
|
18
|
+
const check = {
|
|
19
|
+
judge(toolName, args, cwd, signal, callOpts) {
|
|
20
|
+
if (!opts.tools.has(toolName))
|
|
21
|
+
return null;
|
|
22
|
+
const selection = effectivePathSpellingOf({ toolName, args }, pathTargetOf(toolName), { root: opts.rootCanonical, cwd });
|
|
23
|
+
if (selection === undefined || "refused" in selection || selection.spelled === undefined)
|
|
24
|
+
return null;
|
|
25
|
+
const spelled = selection.spelled;
|
|
26
|
+
return (async () => {
|
|
27
|
+
const extra = opts.roots();
|
|
28
|
+
const r = await resolveKey(opts.env, opts.rootCanonical, spelled, signal, selection.base, extra, undefined, opts.readDeny, opts.readFace);
|
|
29
|
+
if (r.ok || r.violation.code !== "path_not_in_root" || r.violation.target === undefined || opts.excluded?.(r.violation.target) === true || opts.exempt?.(r.violation.target) === true)
|
|
30
|
+
return VOUCH;
|
|
31
|
+
const target = r.violation.target;
|
|
32
|
+
const toolCallId = callOpts.toolCallId;
|
|
33
|
+
const roots = [opts.rootCanonical, ...extra];
|
|
34
|
+
const details = violationDetails(r.violation);
|
|
35
|
+
const allowed = fitMiddle(roots.join(", "), READ_DENY_SENTENCE_PART_MAX);
|
|
36
|
+
if (callOpts.seat === "rejudge") {
|
|
37
|
+
return { disposition: "refuse", finding: "outside_roots", sentence: `Error (${toolName}): the approved edit reads "${fitMiddle(target, READ_DENY_SENTENCE_PART_MAX)}", outside the directories allowed for this session (${allowed}) — re-submit it as a fresh call and answer its card.`, details };
|
|
38
|
+
}
|
|
39
|
+
const info = pathTargetOf(toolName)?.absent === "base" ? await opts.env.fileInfo(target, signal) : undefined;
|
|
40
|
+
const readsDirectory = info?.ok === true && info.value.kind === "directory";
|
|
41
|
+
const offer = grantOfferFor(readsDirectory ? target : parentDirLexically(target), target, { roots, ...(opts.homeDir !== undefined ? { homeDir: opts.homeDir } : {}) });
|
|
42
|
+
return {
|
|
43
|
+
disposition: "ordinary",
|
|
44
|
+
finding: "outside_roots",
|
|
45
|
+
sentence: `reads outside the allowed directories for this session: "${fitMiddle(target, READ_DENY_SENTENCE_PART_MAX)}" is not under ${allowed}`,
|
|
46
|
+
details,
|
|
47
|
+
...(offer !== undefined ? { readRootCandidate: { dir: offer.dir, ...(offer.covers !== undefined ? { covers: offer.covers } : {}), clearsThisAsk: true } } : {}),
|
|
48
|
+
...(toolCallId !== undefined ? { admit: () => remember(toolCallId, target) } : {}),
|
|
49
|
+
};
|
|
50
|
+
})();
|
|
51
|
+
},
|
|
52
|
+
unanswered: () => VOUCH,
|
|
53
|
+
};
|
|
54
|
+
const admitted = (toolCallId, key) => {
|
|
55
|
+
if (toolCallId === undefined || admissions.get(toolCallId) !== key)
|
|
56
|
+
return false;
|
|
57
|
+
admissions.delete(toolCallId);
|
|
58
|
+
return true;
|
|
59
|
+
};
|
|
60
|
+
return { check, tools: opts.tools, admitted };
|
|
61
|
+
}
|
|
62
|
+
export function composeReadChecks(stations) {
|
|
63
|
+
if (stations.length === 0)
|
|
64
|
+
return undefined;
|
|
65
|
+
if (stations.length === 1)
|
|
66
|
+
return stations[0].check;
|
|
67
|
+
return {
|
|
68
|
+
judge: (toolName, args, cwd, signal, callOpts) => stations.find((s) => s.tools.has(toolName))?.check.judge(toolName, args, cwd, signal, callOpts) ?? null,
|
|
69
|
+
unanswered: (toolName, cause) => (stations.find((s) => s.tools.has(toolName)) ?? stations[0]).check.unanswered(toolName, cause),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -4,8 +4,8 @@ import { type CwdRef } from "./fs-shared.js";
|
|
|
4
4
|
import type { ReversibilityVerdict } from "../../core/types.js";
|
|
5
5
|
import type { ShellReadAdmission } from "./shell-read-check.js";
|
|
6
6
|
/**
|
|
7
|
-
* The
|
|
8
|
-
* (the "maybe" tier's probe
|
|
7
|
+
* The full shell face's SHAPE classifier for the `bash` tool, exposed as a `ToolSpec.reversibilityProbe`
|
|
8
|
+
* (the "maybe" tier's probe, installed on every full shell). A command is reversible ONLY if every connected
|
|
9
9
|
* segment is a bare command whose program is on the reversible allowlist (default
|
|
10
10
|
* {@link bashClassifyDefaultAllow} — curated to commands with NO write/mutation mode under ANY args) and whose
|
|
11
11
|
* SHAPE the compound classifier accepts (no redirects, substitutions, subshells, escapes or backgrounding; no
|
|
@@ -17,7 +17,7 @@ import type { ShellReadAdmission } from "./shell-read-check.js";
|
|
|
17
17
|
*
|
|
18
18
|
* WHERE a command reads is not this probe's question. The read boundary — deny rows, containment, symlinks,
|
|
19
19
|
* patterns, traversals, the working directory a `cd` moves — is the gate's shell READ STATION
|
|
20
|
-
* (`shell-read-check.ts`), judged on every shell call under every
|
|
20
|
+
* (`shell-read-check.ts`), judged on every shell call under every mode, ahead of this probe; a call the
|
|
21
21
|
* station refused never reaches it, and a call the station asks about is asked about whatever this probe
|
|
22
22
|
* says. `boundary` supplies only the environment's declared home, so the `$HOME`/`~` spellings fold here the
|
|
23
23
|
* way the station folds them; nothing else of it is read. A caller-supplied `allow` is honored verbatim, so a
|
|
@@ -6,4 +6,5 @@ import { type ReminderDisclosureCounts } from "../../core/reminder-disclosure.js
|
|
|
6
6
|
export declare function createReadFileTool(env: ExecutionEnv, state: ReadFileState, rootCanonical: string, cwdRef?: CwdRef, additionalRoots?: RootsSeat, imageDownsampler?: ReadImageDownsamplerOption, pdfCapabilities?: PdfModelCapabilities, bgOutputReadExemption?: (canonicalKey: string, ctx: {
|
|
7
7
|
taskId?: string;
|
|
8
8
|
principal?: string;
|
|
9
|
+
toolCallId?: string;
|
|
9
10
|
}) => boolean, readCyberReminder?: boolean, readDeny?: import("./read-deny.js").ReadDenyMatcher, readFace?: import("./containment-face.js").ContainmentFace, reminderMark?: string, reminderDisclosureCounts?: ReminderDisclosureCounts): AgentTool;
|
|
@@ -12,5 +12,5 @@ import type { CwdRef } from "./fs-shared.js";
|
|
|
12
12
|
* honesty boundary.
|
|
13
13
|
*/
|
|
14
14
|
export declare function grepDetailFields(text: string, mode: "files_with_matches" | "content" | "count", offset?: number, structuredRows?: readonly import("./search.js").GrepRow[]): Record<string, unknown>;
|
|
15
|
-
export declare function createGrepTool(env: ExecutionEnv, rootCanonical: string, additionalRoots?: RootsSeat, readDeny?: ReadDenyMatcher, readFace?: ContainmentFace, cwdRef?: CwdRef): AgentTool;
|
|
16
|
-
export declare function createGlobTool(env: ExecutionEnv, rootCanonical: string, additionalRoots?: RootsSeat, readDeny?: ReadDenyMatcher, readFace?: ContainmentFace, cwdRef?: CwdRef): AgentTool;
|
|
15
|
+
export declare function createGrepTool(env: ExecutionEnv, rootCanonical: string, additionalRoots?: RootsSeat, readDeny?: ReadDenyMatcher, readFace?: ContainmentFace, cwdRef?: CwdRef, readAdmission?: (toolCallId: string | undefined, canonicalKey: string) => boolean): AgentTool;
|
|
16
|
+
export declare function createGlobTool(env: ExecutionEnv, rootCanonical: string, additionalRoots?: RootsSeat, readDeny?: ReadDenyMatcher, readFace?: ContainmentFace, cwdRef?: CwdRef, readAdmission?: (toolCallId: string | undefined, canonicalKey: string) => boolean): AgentTool;
|
|
@@ -74,7 +74,7 @@ export function grepDetailFields(text, mode, offset, structuredRows) {
|
|
|
74
74
|
}
|
|
75
75
|
return detailFields;
|
|
76
76
|
}
|
|
77
|
-
export function createGrepTool(env, rootCanonical, additionalRoots, readDeny, readFace, cwdRef) {
|
|
77
|
+
export function createGrepTool(env, rootCanonical, additionalRoots, readDeny, readFace, cwdRef, readAdmission) {
|
|
78
78
|
const declaredTarget = toolFace("grep").pathTarget;
|
|
79
79
|
return defineTool({
|
|
80
80
|
name: "Grep",
|
|
@@ -143,7 +143,7 @@ export function createGrepTool(env, rootCanonical, additionalRoots, readDeny, re
|
|
|
143
143
|
const scopeSpelling = declaredTarget === undefined ? undefined : pathTargetValue(a, declaredTarget);
|
|
144
144
|
let scoped = scopeSpelling;
|
|
145
145
|
if (scopeSpelling !== undefined) {
|
|
146
|
-
const r = await resolveKey(env, rootCanonical, scopeSpelling, ctx.signal, baseCwd, additionalRoots,
|
|
146
|
+
const r = await resolveKey(env, rootCanonical, scopeSpelling, ctx.signal, baseCwd, additionalRoots, readAdmission && ((key) => readAdmission(ctx.toolCallId, key)), readDeny, readFace);
|
|
147
147
|
if (!r.ok)
|
|
148
148
|
return errorResult(violationText("Grep", r.violation), violationDetails(r.violation));
|
|
149
149
|
scoped = r.key;
|
|
@@ -177,7 +177,7 @@ export function createGrepTool(env, rootCanonical, additionalRoots, readDeny, re
|
|
|
177
177
|
},
|
|
178
178
|
});
|
|
179
179
|
}
|
|
180
|
-
export function createGlobTool(env, rootCanonical, additionalRoots, readDeny, readFace, cwdRef) {
|
|
180
|
+
export function createGlobTool(env, rootCanonical, additionalRoots, readDeny, readFace, cwdRef, readAdmission) {
|
|
181
181
|
const declaredTarget = toolFace("glob").pathTarget;
|
|
182
182
|
return defineTool({
|
|
183
183
|
name: "Glob",
|
|
@@ -217,7 +217,7 @@ export function createGlobTool(env, rootCanonical, additionalRoots, readDeny, re
|
|
|
217
217
|
const baseCwd = pathTargetBaseOf(declaredTarget, { root: rootCanonical, cwd: cwdRef?.current });
|
|
218
218
|
let scoped = path;
|
|
219
219
|
if (path !== undefined) {
|
|
220
|
-
const r = await resolveKey(env, rootCanonical, path, ctx.signal, baseCwd, additionalRoots,
|
|
220
|
+
const r = await resolveKey(env, rootCanonical, path, ctx.signal, baseCwd, additionalRoots, readAdmission && ((key) => readAdmission(ctx.toolCallId, key)), readDeny, readFace);
|
|
221
221
|
if (!r.ok)
|
|
222
222
|
return errorResult(violationText("Glob", r.violation), violationDetails(r.violation));
|
|
223
223
|
scoped = r.key;
|
|
@@ -4,6 +4,7 @@ import { normalizeAbsPathLexically, sha256, similarNameSuggestion, OVERSIZE_READ
|
|
|
4
4
|
import { decodeTextBytes, normalizeFileText } from "./encoding.js";
|
|
5
5
|
import { shellQuote } from "./search.js";
|
|
6
6
|
import { isNotebookPath } from "./notebook.js";
|
|
7
|
+
import { homeViewsOf } from "./read-deny-home.js";
|
|
7
8
|
import { sharpImageDownsampler } from "../../core/mcp.js";
|
|
8
9
|
import { deliverEngineNotice } from "../../core/types.js";
|
|
9
10
|
import { resolveEnvBudget } from "../../core/env-budget.js";
|
|
@@ -344,24 +345,37 @@ export function applyContextShrinkToReadFileState(state, shrink) {
|
|
|
344
345
|
export async function enoentMessage(env, key, cwd, signal, readDeny) {
|
|
345
346
|
let msg = `File does not exist. Note: your current working directory is ${cwd}.`;
|
|
346
347
|
const sep = Math.max(key.lastIndexOf("/"), key.lastIndexOf("\\"));
|
|
348
|
+
const views = readDeny !== undefined ? homeViewsOf(env, signal) : undefined;
|
|
349
|
+
const denied = async (path, leaf) => {
|
|
350
|
+
if (readDeny === undefined || views === undefined)
|
|
351
|
+
return false;
|
|
352
|
+
if (readDeny.matchPath(path) != null)
|
|
353
|
+
return true;
|
|
354
|
+
const view = await views.of(path, leaf !== undefined ? { leaf } : undefined);
|
|
355
|
+
return view !== undefined && readDeny.matchPath(view) != null;
|
|
356
|
+
};
|
|
347
357
|
if (sep > 0) {
|
|
348
358
|
const parent = key.slice(0, sep);
|
|
349
359
|
const name = key.slice(sep + 1);
|
|
350
|
-
if (
|
|
360
|
+
if (await denied(parent))
|
|
351
361
|
return msg;
|
|
352
362
|
const cwdSepped = cwd.replace(/[\\/]+$/, "");
|
|
353
363
|
const cwdCandidate = `${cwdSepped}${cwd.includes("\\") ? "\\" : "/"}${name}`;
|
|
354
|
-
if (cwdCandidate !== key &&
|
|
364
|
+
if (cwdCandidate !== key && !(await denied(cwdCandidate))) {
|
|
355
365
|
const hit = await env.exists(cwdCandidate, signal).catch(() => undefined);
|
|
356
366
|
if (hit?.ok && hit.value)
|
|
357
367
|
return `${msg} Did you mean ${cwdCandidate}?`;
|
|
358
368
|
}
|
|
359
369
|
const listing = await env.listDir(parent, signal).catch(() => undefined);
|
|
360
370
|
if (listing?.ok) {
|
|
361
|
-
const names =
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
371
|
+
const names = [];
|
|
372
|
+
for (const e of listing.value) {
|
|
373
|
+
if (e.kind === "directory")
|
|
374
|
+
continue;
|
|
375
|
+
const n2 = e.path.slice(Math.max(e.path.lastIndexOf("/"), e.path.lastIndexOf("\\")) + 1);
|
|
376
|
+
if (!(await denied(`${parent}/${n2}`, "file")))
|
|
377
|
+
names.push(n2);
|
|
378
|
+
}
|
|
365
379
|
const suggestion = similarNameSuggestion(names, name);
|
|
366
380
|
if (suggestion)
|
|
367
381
|
msg += ` Did you mean ${suggestion}?`;
|
|
@@ -2,6 +2,7 @@ import type { AgentTool, ExecutionEnv } from "../../internal/harness-types.js";
|
|
|
2
2
|
import type { BeforeWriteHook, FileEditedHook, TrackFileEditHook } from "../../core/types.js";
|
|
3
3
|
import { type ReadFileState } from "./safety.js";
|
|
4
4
|
import type { ContainmentFace } from "./containment-face.js";
|
|
5
|
+
import type { WriteDenyMatcher } from "./read-deny.js";
|
|
5
6
|
import { type CwdRef } from "./fs-shared.js";
|
|
6
7
|
/**
|
|
7
8
|
* The `beforeWrite` CONTENT hook, mounted here rather than in ToolPolicy because only this band sees
|
|
@@ -13,8 +14,8 @@ import { type CwdRef } from "./fs-shared.js";
|
|
|
13
14
|
* Absent hook ⇒ byte-identical behavior.
|
|
14
15
|
*/
|
|
15
16
|
export type { BeforeWriteRequest, BeforeWriteResult, BeforeWriteHook, TrackEditRequest, TrackEditResult, TrackFileEditHook } from "../../core/types.js";
|
|
16
|
-
export declare function createEditFileTool(env: ExecutionEnv, state: ReadFileState, rootCanonical: string, cwdRef?: CwdRef, additionalRoots?: readonly string[], beforeWrite?: BeforeWriteHook, trackEdit?: TrackFileEditHook, onEdited?: FileEditedHook, writeFace?: ContainmentFace): AgentTool;
|
|
17
|
-
export declare function createWriteFileTool(env: ExecutionEnv, state: ReadFileState, rootCanonical: string, cwdRef?: CwdRef, additionalRoots?: readonly string[], beforeWrite?: BeforeWriteHook, trackEdit?: TrackFileEditHook, onEdited?: FileEditedHook, writeFace?: ContainmentFace): AgentTool;
|
|
17
|
+
export declare function createEditFileTool(env: ExecutionEnv, state: ReadFileState, rootCanonical: string, cwdRef?: CwdRef, additionalRoots?: readonly string[], beforeWrite?: BeforeWriteHook, trackEdit?: TrackFileEditHook, onEdited?: FileEditedHook, writeFace?: () => ContainmentFace, writeDeny?: WriteDenyMatcher): AgentTool;
|
|
18
|
+
export declare function createWriteFileTool(env: ExecutionEnv, state: ReadFileState, rootCanonical: string, cwdRef?: CwdRef, additionalRoots?: readonly string[], beforeWrite?: BeforeWriteHook, trackEdit?: TrackFileEditHook, onEdited?: FileEditedHook, writeFace?: () => ContainmentFace, writeDeny?: WriteDenyMatcher): AgentTool;
|
|
18
19
|
/**
|
|
19
20
|
* design v1.163 — NotebookEdit: replace/insert/delete a single cell in a .ipynb. CC-parity tool over the SAME hand-band
|
|
20
21
|
* safety skeleton as Edit/Write (resolveKey containment → requireRead read-before-edit → checkStale content-hash
|
|
@@ -22,4 +23,4 @@ export declare function createWriteFileTool(env: ExecutionEnv, state: ReadFileSt
|
|
|
22
23
|
* .ipynb as the cell projection but records read state (hash/totalLines) in the RAW notebook-text
|
|
23
24
|
* coordinate — that raw-coordinate read record is what this tool's freshness check depends on.
|
|
24
25
|
*/
|
|
25
|
-
export declare function createNotebookEditTool(env: ExecutionEnv, state: ReadFileState, rootCanonical: string, cwdRef?: CwdRef, additionalRoots?: readonly string[], beforeWrite?: BeforeWriteHook, trackEdit?: TrackFileEditHook, onEdited?: FileEditedHook, writeFace?: ContainmentFace): AgentTool;
|
|
26
|
+
export declare function createNotebookEditTool(env: ExecutionEnv, state: ReadFileState, rootCanonical: string, cwdRef?: CwdRef, additionalRoots?: readonly string[], beforeWrite?: BeforeWriteHook, trackEdit?: TrackFileEditHook, onEdited?: FileEditedHook, writeFace?: () => ContainmentFace, writeDeny?: WriteDenyMatcher): AgentTool;
|
|
@@ -87,7 +87,7 @@ async function gateToolWrite(hook, tool, path, key, content) {
|
|
|
87
87
|
return `Error (${tool}): write rejected by the write gate (${res.code}): ${clipHookText(res.reason)}`;
|
|
88
88
|
return undefined;
|
|
89
89
|
}
|
|
90
|
-
export function createEditFileTool(env, state, rootCanonical, cwdRef, additionalRoots, beforeWrite, trackEdit, onEdited, writeFace) {
|
|
90
|
+
export function createEditFileTool(env, state, rootCanonical, cwdRef, additionalRoots, beforeWrite, trackEdit, onEdited, writeFace, writeDeny) {
|
|
91
91
|
return defineTool({
|
|
92
92
|
name: "Edit",
|
|
93
93
|
...toolFace("edit"),
|
|
@@ -112,7 +112,7 @@ export function createEditFileTool(env, state, rootCanonical, cwdRef, additional
|
|
|
112
112
|
const path = fileArgPath(args);
|
|
113
113
|
if (path === undefined)
|
|
114
114
|
return { ok: true };
|
|
115
|
-
const r = await resolveKey(env, rootCanonical, path, ctx.signal, cwdRef?.current, additionalRoots, undefined,
|
|
115
|
+
const r = await resolveKey(env, rootCanonical, path, ctx.signal, cwdRef?.current, additionalRoots, undefined, writeDeny, writeFace?.());
|
|
116
116
|
if (!r.ok)
|
|
117
117
|
return { ok: true };
|
|
118
118
|
if (a.old_string === a.new_string)
|
|
@@ -150,7 +150,7 @@ export function createEditFileTool(env, state, rootCanonical, cwdRef, additional
|
|
|
150
150
|
const path = fileArgPath(args);
|
|
151
151
|
if (path === undefined)
|
|
152
152
|
return errorResult(`Error (Edit): file_path is required.`);
|
|
153
|
-
const r = await resolveKey(env, rootCanonical, path, ctx.signal, cwdRef?.current, additionalRoots, undefined,
|
|
153
|
+
const r = await resolveKey(env, rootCanonical, path, ctx.signal, cwdRef?.current, additionalRoots, undefined, writeDeny, writeFace?.());
|
|
154
154
|
if (!r.ok)
|
|
155
155
|
return errorResult(violationText("Edit", r.violation), violationDetails(r.violation));
|
|
156
156
|
if (!batch && a.old_string === a.new_string) {
|
|
@@ -325,7 +325,7 @@ export function createEditFileTool(env, state, rootCanonical, cwdRef, additional
|
|
|
325
325
|
},
|
|
326
326
|
});
|
|
327
327
|
}
|
|
328
|
-
export function createWriteFileTool(env, state, rootCanonical, cwdRef, additionalRoots, beforeWrite, trackEdit, onEdited, writeFace) {
|
|
328
|
+
export function createWriteFileTool(env, state, rootCanonical, cwdRef, additionalRoots, beforeWrite, trackEdit, onEdited, writeFace, writeDeny) {
|
|
329
329
|
return defineTool({
|
|
330
330
|
name: "Write",
|
|
331
331
|
...toolFace("write"),
|
|
@@ -344,7 +344,7 @@ export function createWriteFileTool(env, state, rootCanonical, cwdRef, additiona
|
|
|
344
344
|
return { ok: true };
|
|
345
345
|
if (ipynbRedirect("Write", path))
|
|
346
346
|
return { ok: true };
|
|
347
|
-
const r = await resolveKey(env, rootCanonical, path, ctx.signal, cwdRef?.current, additionalRoots, undefined,
|
|
347
|
+
const r = await resolveKey(env, rootCanonical, path, ctx.signal, cwdRef?.current, additionalRoots, undefined, writeDeny, writeFace?.());
|
|
348
348
|
if (!r.ok)
|
|
349
349
|
return { ok: true };
|
|
350
350
|
const exists = await env.exists(r.key, ctx.signal);
|
|
@@ -363,7 +363,7 @@ export function createWriteFileTool(env, state, rootCanonical, cwdRef, additiona
|
|
|
363
363
|
const ipynb = ipynbRedirect("Write", path);
|
|
364
364
|
if (ipynb)
|
|
365
365
|
return errorResult(ipynb);
|
|
366
|
-
const r = await resolveKey(env, rootCanonical, path, ctx.signal, cwdRef?.current, additionalRoots, undefined,
|
|
366
|
+
const r = await resolveKey(env, rootCanonical, path, ctx.signal, cwdRef?.current, additionalRoots, undefined, writeDeny, writeFace?.());
|
|
367
367
|
if (!r.ok)
|
|
368
368
|
return errorResult(violationText("Write", r.violation), violationDetails(r.violation));
|
|
369
369
|
const exists = await env.exists(r.key, ctx.signal);
|
|
@@ -424,7 +424,7 @@ export function createWriteFileTool(env, state, rootCanonical, cwdRef, additiona
|
|
|
424
424
|
const notebookNotIpynbMessage = (notebookPath) => `Error (NotebookEdit): "${notebookPath}" is not a .ipynb file; use Edit for other file types.`;
|
|
425
425
|
const NOTEBOOK_CELL_TYPE_REQUIRED_MESSAGE = `Error (NotebookEdit): Cell type is required when using edit_mode=insert.`;
|
|
426
426
|
const NOTEBOOK_CELL_ID_REQUIRED_MESSAGE = `Error (NotebookEdit): cell_id is required for replace/delete.`;
|
|
427
|
-
export function createNotebookEditTool(env, state, rootCanonical, cwdRef, additionalRoots, beforeWrite, trackEdit, onEdited, writeFace) {
|
|
427
|
+
export function createNotebookEditTool(env, state, rootCanonical, cwdRef, additionalRoots, beforeWrite, trackEdit, onEdited, writeFace, writeDeny) {
|
|
428
428
|
return defineTool({
|
|
429
429
|
name: "NotebookEdit",
|
|
430
430
|
...toolFace("notebook-edit"),
|
|
@@ -458,7 +458,7 @@ export function createNotebookEditTool(env, state, rootCanonical, cwdRef, additi
|
|
|
458
458
|
return { ok: false, code: "invalid", message: NOTEBOOK_CELL_TYPE_REQUIRED_MESSAGE };
|
|
459
459
|
if (mode !== "insert" && !a.cell_id)
|
|
460
460
|
return { ok: false, code: "invalid", message: NOTEBOOK_CELL_ID_REQUIRED_MESSAGE };
|
|
461
|
-
const r = await resolveKey(env, rootCanonical, a.notebook_path, ctx.signal, cwdRef?.current, additionalRoots, undefined,
|
|
461
|
+
const r = await resolveKey(env, rootCanonical, a.notebook_path, ctx.signal, cwdRef?.current, additionalRoots, undefined, writeDeny, writeFace?.());
|
|
462
462
|
if (!r.ok)
|
|
463
463
|
return { ok: true };
|
|
464
464
|
const notRead = requireRead(state, r.key);
|
|
@@ -478,7 +478,7 @@ export function createNotebookEditTool(env, state, rootCanonical, cwdRef, additi
|
|
|
478
478
|
return errorResult(NOTEBOOK_CELL_TYPE_REQUIRED_MESSAGE);
|
|
479
479
|
if (mode !== "insert" && !cell_id)
|
|
480
480
|
return errorResult(NOTEBOOK_CELL_ID_REQUIRED_MESSAGE);
|
|
481
|
-
const r = await resolveKey(env, rootCanonical, notebook_path, ctx.signal, cwdRef?.current, additionalRoots, undefined,
|
|
481
|
+
const r = await resolveKey(env, rootCanonical, notebook_path, ctx.signal, cwdRef?.current, additionalRoots, undefined, writeDeny, writeFace?.());
|
|
482
482
|
if (!r.ok)
|
|
483
483
|
return errorResult(violationText("NotebookEdit", r.violation), violationDetails(r.violation));
|
|
484
484
|
const notRead = requireRead(state, r.key);
|
package/dist/tools/fs/index.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export * from "./fs-search-tools.js";
|
|
|
11
11
|
export * from "./bash-readonly-classifier.js";
|
|
12
12
|
export * from "./fs-bash.js";
|
|
13
13
|
export * from "./read-deny.js";
|
|
14
|
+
export { readDenyHomesOf } from "./read-deny-home.js";
|
|
14
15
|
import type { ShellReadAdmission } from "./shell-read-check.js";
|
|
15
16
|
export { BASH_CLASSIFY_EXTRA_ALLOW, bashClassifyDefaultAllow } from "./bash-readonly-classifier.js";
|
|
16
17
|
import { type ReadDenyEntry } from "./read-deny.js";
|
|
@@ -55,6 +56,10 @@ export interface HandsToolkitOptions {
|
|
|
55
56
|
* consumer mounting the read-only band outside the Runner must run `runToolGate` with a station of its own
|
|
56
57
|
* (`createShellReadStation`). */
|
|
57
58
|
shellReadAdmission?: ShellReadAdmission;
|
|
59
|
+
/** The gate's file read station's ADMISSION (`FileReadStation.admitted`, file-read-check.ts), handed to Read / Grep /
|
|
60
|
+
* Glob: an out-of-root target the gate put to a question and then let through is read once, on the canonical key the
|
|
61
|
+
* station judged. ABSENT ⇒ no station (a library-direct toolkit): the tools refuse every out-of-root target. */
|
|
62
|
+
readAdmission?: (toolCallId: string | undefined, canonicalKey: string) => boolean;
|
|
58
63
|
/** Commit-message attribution trailer for the `bash` git protocol. Default NONE — attribution is a
|
|
59
64
|
* deployment identity asset, so the branded deployment sets it; pass `"Name <email>"` to advertise
|
|
60
65
|
* one, `false` (≡ unset) for no trailer section. */
|
|
@@ -167,11 +172,10 @@ export interface HandsToolkitOptions {
|
|
|
167
172
|
* time. The write faces are untouched (their guard is createSensitivePathPolicy). */
|
|
168
173
|
readDenyPatterns?: readonly ReadDenyEntry[];
|
|
169
174
|
/** The deployment's built-in-table TIER selection ({@link import("./read-deny.js").ReadDenyBuiltinConfig}
|
|
170
|
-
* `tiers` key) — the
|
|
175
|
+
* `tiers` key) — the switch for the built-in table:
|
|
171
176
|
* EXACTLY the listed tiers of {@link import("./read-deny.js").READ_FACE_BUILTIN_DENY_TABLE}
|
|
172
177
|
* are active (`[]` = none — explicit and legal); absent = the default selection
|
|
173
|
-
* ({@link import("./read-deny.js").READ_DENY_DEFAULT_TIERS}
|
|
174
|
-
* mount activates no built-in row.
|
|
178
|
+
* ({@link import("./read-deny.js").READ_DENY_DEFAULT_TIERS}: `credentials` and the home-anchored `agent-config`).
|
|
175
179
|
* Composes with {@link readDenyBuiltinExclude} (tier selection first, then row removal) and never
|
|
176
180
|
* touches the additions seats. Unknown tier names throw at wiring time. Deployment seat ONLY —
|
|
177
181
|
* deliberately not a TaskSpec key, so no task can widen below its deployment. */
|
|
@@ -181,6 +185,22 @@ export interface HandsToolkitOptions {
|
|
|
181
185
|
* of an inactive tier is a satisfied intent (no-op); a name matching NO table row throws at wiring
|
|
182
186
|
* time (never silently ignored). Deployment seat ONLY, same clamp argument as the tiers key. */
|
|
183
187
|
readDenyBuiltinExclude?: readonly string[];
|
|
188
|
+
/** The spellings of the execution environment's home a home-anchored deny row (`~/…`) is placed under
|
|
189
|
+
* ({@link import("./read-deny-home.js").readDenyHomesOf}: the declared home and its canonical spelling). Absolute path
|
|
190
|
+
* strings only — any other shape throws at wiring time, naming the seat. The Runner passes the spellings its own judge
|
|
191
|
+
* was compiled with. */
|
|
192
|
+
readDenyHomes?: readonly string[];
|
|
193
|
+
/** Where each home-anchored row's literal head leads beyond the home's spellings
|
|
194
|
+
* ({@link import("./read-deny-home.js").readDenyHomeLinksOf}, keyed by the row's pattern) — the Runner passes the
|
|
195
|
+
* locations its own judge was compiled with, so the band and the gate judge one placement.
|
|
196
|
+
* Unless a mount carries both, the band resolves the placement itself the way the Runner does (the homes it carries
|
|
197
|
+
* are used as given; {@link import("./read-deny-home.js").placeReadDenyAtHome}), and its read tools (Read / Grep /
|
|
198
|
+
* Glob / RepoMap) wait for that placement before they judge anything. A placement that fails leaves the home-anchored
|
|
199
|
+
* rows held anywhere. */
|
|
200
|
+
readDenyHomeLinks?: ReadonlyMap<string, readonly string[]>;
|
|
201
|
+
/** The permission-rule rows of the deny table ({@link import("./read-deny.js").PathRuleDenyRow}, `ToolPolicy.pathDenyRules`):
|
|
202
|
+
* the read-face rows join the structured read faces' judge beside the segment rows. The Runner passes the leg's own. */
|
|
203
|
+
pathDenyRules?: readonly import("./read-deny.js").PathRuleDenyRow[];
|
|
184
204
|
/** The READ-face containment state for the structured read faces
|
|
185
205
|
* ({@link import("./containment-face.js").ContainmentFace}; resolved through the SAME
|
|
186
206
|
* {@link import("./containment-face.js").resolveContainmentFace} order prepare-task uses). Absent ⇒ the
|
|
@@ -202,6 +222,10 @@ export interface HandsToolkitOptions {
|
|
|
202
222
|
* wins the same way the read seat's does (that mount carries no write faces at all), announced as
|
|
203
223
|
* `config.write_face_deployment_clamped`. Never affects the read faces. */
|
|
204
224
|
writeFace?: ContainmentFace;
|
|
225
|
+
/** A runner's LIVE write face — its resolution, which reads the permission mode's face on every call. Present ⇒
|
|
226
|
+
* the write tools read it per call and {@link writeFace} is not consulted (the runner resolved and screened the
|
|
227
|
+
* seats itself); absent ⇒ the toolkit's own resolution of {@link writeFace}, fixed at creation. */
|
|
228
|
+
writeFaceLive?: () => ContainmentFace;
|
|
205
229
|
/** The session's reminder provenance mark: the band's engine-minted
|
|
206
230
|
* `<system-reminder>` open tags (the Read cyber/dedup/offset/empty reminders and the gh
|
|
207
231
|
* rate-limit hint) carry it as `mark="…"`, matching the system-prompt declaration the Runner
|
|
@@ -224,6 +248,18 @@ export interface HandsToolkitOptions {
|
|
|
224
248
|
* predicate {@link createHandsToolkit} branches on and prepare-task plans by (the memory phase must know, before the
|
|
225
249
|
* band exists, whether the instruction-taught write channel will be there). `readOnly` is the verifier boundary. */
|
|
226
250
|
export declare function handsWriteHalfMounts(opts: Pick<HandsToolkitOptions, "readOnly">): boolean;
|
|
251
|
+
/** The engine's background-output read exemption over one toolkit's seats: the exact registered output file of a
|
|
252
|
+
* background command, visible to the caller's own owner / scope / session, on THIS execution env — the file the
|
|
253
|
+
* launch receipt tells the model to Read. Shared by the Read tool's fence and the file read station, so the station
|
|
254
|
+
* puts no question where the fence would admit. */
|
|
255
|
+
export declare function backgroundOutputReadExemption(registry: TaskRegistry | undefined, seat: {
|
|
256
|
+
readonly taskOwner?: string;
|
|
257
|
+
readonly taskScope?: string;
|
|
258
|
+
readonly sessionId?: string;
|
|
259
|
+
}, env: ExecutionEnv): ((canonicalKey: string, c?: {
|
|
260
|
+
taskId?: string;
|
|
261
|
+
principal?: string;
|
|
262
|
+
}) => boolean) | undefined;
|
|
227
263
|
/**
|
|
228
264
|
* Build the per-task hand tool band over an injected env + fresh per-task read state.
|
|
229
265
|
* `rootCanonical` is the already-canonicalized containment root (prepare-task resolves it once). Normal
|
package/dist/tools/fs/index.js
CHANGED
|
@@ -11,9 +11,12 @@ export * from "./fs-search-tools.js";
|
|
|
11
11
|
export * from "./bash-readonly-classifier.js";
|
|
12
12
|
export * from "./fs-bash.js";
|
|
13
13
|
export * from "./read-deny.js";
|
|
14
|
+
export { readDenyHomesOf } from "./read-deny-home.js";
|
|
14
15
|
import { BASH_READONLY_DEFAULT_ALLOW } from "./bash-readonly-classifier.js";
|
|
15
16
|
export { BASH_CLASSIFY_EXTRA_ALLOW, bashClassifyDefaultAllow } from "./bash-readonly-classifier.js";
|
|
16
|
-
import { compileReadDeny } from "./read-deny.js";
|
|
17
|
+
import { checkedReadDenyHomes, compileReadDeny, compileWriteDeny, readDenyBuiltinConfigOf, readDenyPatternRun } from "./read-deny.js";
|
|
18
|
+
import { withGateTimeout } from "../../core/gate-bounded-await.js";
|
|
19
|
+
import { placeReadDenyAtHome, readDenyPlacedLater } from "./read-deny-home.js";
|
|
17
20
|
import { deploymentFaceClampNotice, resolveContainmentFace } from "./containment-face.js";
|
|
18
21
|
export * from "./containment-face.js";
|
|
19
22
|
import {} from "./fs-shared.js";
|
|
@@ -24,22 +27,41 @@ import { createBashTool, createBashReadonlyTool, createEnvTaskOutputTool, create
|
|
|
24
27
|
export function handsWriteHalfMounts(opts) {
|
|
25
28
|
return opts.readOnly !== true;
|
|
26
29
|
}
|
|
30
|
+
export function backgroundOutputReadExemption(registry, seat, env) {
|
|
31
|
+
if (registry === undefined)
|
|
32
|
+
return undefined;
|
|
33
|
+
return (canonicalKey, c) => registry.isBackgroundOutputFile(canonicalKey, { owner: c?.taskId ?? seat.taskOwner, scope: c?.principal ?? seat.taskScope, ...(seat.sessionId !== undefined ? { sessionId: seat.sessionId } : {}) }, env);
|
|
34
|
+
}
|
|
27
35
|
export function createHandsToolkit(env, readFileState, rootCanonical, opts = {}) {
|
|
28
36
|
const { includeShell = false, readOnly = false, bashReadonlyAllow, commitCoAuthor = false, mountBackgroundTaskTools = true, additionalRoots, additionalReadRoots, } = opts;
|
|
29
37
|
const readFaceRoots = additionalReadRoots === undefined ? additionalRoots : () => (additionalReadRoots.length === 0 ? additionalRoots : [...(additionalRoots ?? []), ...additionalReadRoots]);
|
|
30
38
|
const cwdRef = opts.cwdRef ?? { current: rootCanonical };
|
|
31
|
-
const
|
|
32
|
-
const
|
|
39
|
+
const bgOutput = backgroundOutputReadExemption(opts.taskRegistry, opts, env);
|
|
40
|
+
const readAdmission = opts.readAdmission;
|
|
41
|
+
const bgOutputReadExemption = bgOutput === undefined && readAdmission === undefined
|
|
33
42
|
? undefined
|
|
34
|
-
: (canonicalKey, c) =>
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
: (canonicalKey, c) => readAdmission?.(c.toolCallId, canonicalKey) === true || (bgOutput?.(canonicalKey, c) ?? false);
|
|
44
|
+
const readDenyAdditions = (opts.readDenyPatterns ?? []).map((e) => (typeof e === "object" && e !== null ? { ...e } : e));
|
|
45
|
+
const readDenyRules = opts.pathDenyRules === undefined ? undefined : [...opts.pathDenyRules];
|
|
46
|
+
const readDenyBuiltinCfg = readDenyBuiltinConfigOf({ tiers: opts.readDenyBuiltinTiers, exclude: opts.readDenyBuiltinExclude });
|
|
47
|
+
const readDenyHomes = opts.readDenyHomes === undefined ? undefined : [...checkedReadDenyHomes(opts.readDenyHomes)];
|
|
48
|
+
const compileAt = (homes, linked) => compileReadDeny(readDenyAdditions, "HandsToolkitOptions.readDenyPatterns", readDenyBuiltinCfg, readDenyRules, homes, linked);
|
|
49
|
+
const unplaced = compileAt([]);
|
|
50
|
+
const carried = readDenyHomes !== undefined && opts.readDenyHomeLinks !== undefined;
|
|
51
|
+
const placesNothing = !unplaced.entries.some((e) => readDenyPatternRun(e.pattern).homeAnchored);
|
|
52
|
+
const late = carried || placesNothing ? undefined : readDenyPlacedLater(unplaced, () => placeReadDenyAtHome(env, compileAt, readDenyHomes));
|
|
53
|
+
const readDeny = late?.matcher ?? (carried ? compileAt(readDenyHomes, opts.readDenyHomeLinks) : unplaced);
|
|
54
|
+
const afterPlacement = (tool) => late === undefined
|
|
55
|
+
? tool
|
|
56
|
+
: {
|
|
57
|
+
...tool,
|
|
58
|
+
execute: async (...args) => {
|
|
59
|
+
await withGateTimeout(late.ready(), undefined, args[2], "readDenyPlacement").catch(() => undefined);
|
|
60
|
+
return await tool.execute(...args);
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
const writeDenyCompiled = compileWriteDeny(readDenyRules);
|
|
64
|
+
const writeDeny = writeDenyCompiled.ruleRows.length > 0 ? writeDenyCompiled : undefined;
|
|
43
65
|
const readFace = resolveContainmentFace({
|
|
44
66
|
kind: "read",
|
|
45
67
|
depsFace: opts.readFace,
|
|
@@ -56,13 +78,14 @@ export function createHandsToolkit(env, readFileState, rootCanonical, opts = {})
|
|
|
56
78
|
fullShellReachable: includeShell && !readOnly,
|
|
57
79
|
onDeploymentClamp: () => deliverEngineNotice(opts.onNotice, deploymentFaceClampNotice("write")),
|
|
58
80
|
});
|
|
81
|
+
const writeFaceOf = opts.writeFaceLive ?? (() => writeFace);
|
|
59
82
|
const tools = [
|
|
60
|
-
createReadFileTool(env, readFileState, rootCanonical, readOnly ? undefined : cwdRef, readFaceRoots, opts.readImageDownsampler, opts.pdfModelCapabilities, bgOutputReadExemption, opts.readCyberReminder, readDeny, readFace, opts.reminderMark, opts.reminderDisclosureCounts),
|
|
83
|
+
afterPlacement(createReadFileTool(env, readFileState, rootCanonical, readOnly ? undefined : cwdRef, readFaceRoots, opts.readImageDownsampler, opts.pdfModelCapabilities, bgOutputReadExemption, opts.readCyberReminder, readDeny, readFace, opts.reminderMark, opts.reminderDisclosureCounts)),
|
|
61
84
|
];
|
|
62
85
|
if (handsWriteHalfMounts({ readOnly })) {
|
|
63
|
-
tools.push(createEditFileTool(env, readFileState, rootCanonical, cwdRef, additionalRoots, opts.beforeWrite, opts.trackFileEdit, opts.onFileEdited,
|
|
86
|
+
tools.push(createEditFileTool(env, readFileState, rootCanonical, cwdRef, additionalRoots, opts.beforeWrite, opts.trackFileEdit, opts.onFileEdited, writeFaceOf, writeDeny), createWriteFileTool(env, readFileState, rootCanonical, cwdRef, additionalRoots, opts.beforeWrite, opts.trackFileEdit, opts.onFileEdited, writeFaceOf, writeDeny), createNotebookEditTool(env, readFileState, rootCanonical, cwdRef, additionalRoots, opts.beforeWrite, opts.trackFileEdit, opts.onFileEdited, writeFaceOf, writeDeny));
|
|
64
87
|
}
|
|
65
|
-
tools.push(createGrepTool(env, rootCanonical, readFaceRoots, readDeny, readFace, cwdRef), createGlobTool(env, rootCanonical, readFaceRoots, readDeny, readFace, cwdRef), createRepoMapTool(env, rootCanonical, readFaceRoots, readDeny, readFace));
|
|
88
|
+
tools.push(afterPlacement(createGrepTool(env, rootCanonical, readFaceRoots, readDeny, readFace, cwdRef, readAdmission)), afterPlacement(createGlobTool(env, rootCanonical, readFaceRoots, readDeny, readFace, cwdRef, readAdmission)), afterPlacement(createRepoMapTool(env, rootCanonical, readFaceRoots, readDeny, readFace)));
|
|
66
89
|
if (includeShell) {
|
|
67
90
|
tools.push(readOnly
|
|
68
91
|
? createBashReadonlyTool(env, rootCanonical, new Set(bashReadonlyAllow ?? BASH_READONLY_DEFAULT_ALLOW), {
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { ExecutionEnv } from "../../internal/harness-types.js";
|
|
2
|
+
import { type PathFamily } from "./safety.js";
|
|
3
|
+
import { type ReadDenyMatcher } from "./read-deny.js";
|
|
4
|
+
/** The environment's DECLARED home as an anchor list: `[homeDir]` when it declares an absolute one, else `[]` (no
|
|
5
|
+
* home — a home-anchored row then holds its run anywhere, see `compileReadDeny`). */
|
|
6
|
+
export declare function declaredReadDenyHomes(env: Pick<ExecutionEnv, "homeDir">): string[];
|
|
7
|
+
/** The declared home and, where the environment's canonical spelling of it differs (a home reached through a
|
|
8
|
+
* symlink), that spelling too: the faces judge canonical keys beside the spelled views, so a row placed only at
|
|
9
|
+
* the spelled home would never meet a canonical key. A home the environment cannot resolve keeps its spelling. */
|
|
10
|
+
export declare function readDenyHomesOf(env: Pick<ExecutionEnv, "homeDir" | "canonicalPath">, signal?: AbortSignal): Promise<string[]>;
|
|
11
|
+
/**
|
|
12
|
+
* Where the home-anchored rows of a table LEAD beyond the home's own spellings, keyed by the row's pattern — the
|
|
13
|
+
* `linked` argument of `compileReadDeny`. For each row, its LITERAL HEAD (`readDenyRunHead`: the run's segments before
|
|
14
|
+
* the first one carrying `*`) is spelled under each home and resolved the way the read faces mint their keys
|
|
15
|
+
* (`canonicalizeTarget`: the longest existing prefix resolved, the rest rejoined); a location that differs from every
|
|
16
|
+
* home's own spelling of the head is where one of the head's segments is a link, and the row is placed there too.
|
|
17
|
+
* · a head that resolves to its own spelling under a home adds nothing (the row is already placed there);
|
|
18
|
+
* · a head that cannot be resolved (an unanswered or refused probe, a probe that throws) adds nothing, and the answer
|
|
19
|
+
* is not WHOLE (`whole: false`): the row stays placed at the home's spellings until the mount places again;
|
|
20
|
+
* · a row whose head is empty (`~/.sema.*`) has no location to resolve and adds nothing;
|
|
21
|
+
* · the heads are resolved together, so one placement waits at most one probe bound;
|
|
22
|
+
* · read per placement — a link created afterwards is followed from the next placement on.
|
|
23
|
+
*/
|
|
24
|
+
export declare function readDenyHomeLinksOf(env: ExecutionEnv, entries: readonly {
|
|
25
|
+
readonly pattern: string;
|
|
26
|
+
readonly caseSensitive?: boolean;
|
|
27
|
+
}[], homes: readonly string[], signal?: AbortSignal): Promise<{
|
|
28
|
+
readonly linked: Map<string, string[]>;
|
|
29
|
+
readonly whole: boolean;
|
|
30
|
+
}>;
|
|
31
|
+
/** Where an absolute location LEADS in one environment — resolved the way the read and write faces mint their keys
|
|
32
|
+
* (`canonicalizeTarget`: the longest existing prefix resolved, the rest rejoined). `undefined` when the environment
|
|
33
|
+
* does not answer (a refused or failed probe, one that throws). The one resolution a deny row's placement reads: a
|
|
34
|
+
* home-anchored row's head ({@link readDenyHomeLinksOf}) and a rule row's head ({@link whereItsAnsweredPrefixLeads}). */
|
|
35
|
+
export declare function whereItLeads(env: ExecutionEnv, at: string, family?: PathFamily, signal?: AbortSignal): Promise<string | undefined>;
|
|
36
|
+
/** {@link whereItLeads} with a floor: where the environment does not answer for `at` (a probe that fails), the longest
|
|
37
|
+
* prefix it does answer for is resolved and the rest rejoined — a rule row's head below a base that resolves is never
|
|
38
|
+
* placed narrower than that base's own canonical spelling. The whole walk waits at most one bounded-await deadline.
|
|
39
|
+
* `undefined` when no prefix answers in time. */
|
|
40
|
+
export declare function whereItsAnsweredPrefixLeads(env: ExecutionEnv, at: string, family?: PathFamily, signal?: AbortSignal): Promise<string | undefined>;
|
|
41
|
+
/** One mount's read-deny judge with its home-anchored rows placed: at the home's spellings (`homes` when the mount
|
|
42
|
+
* carries them, else {@link readDenyHomesOf}, under the tree's bounded-await deadline — an environment that cannot
|
|
43
|
+
* answer rejects the placement instead of holding it open) and where their heads lead ({@link readDenyHomeLinksOf}).
|
|
44
|
+
* `whole` says whether the home and every head answered. A home the environment did not answer for places nothing:
|
|
45
|
+
* the rows are held anywhere (`compile([])`, the rule for a row that cannot be placed) rather than at a spelling that
|
|
46
|
+
* may miss the home's canonical one. `compile` is the mount's own compile over the two; the rows' patterns are read
|
|
47
|
+
* off its compile at the homes alone. */
|
|
48
|
+
export declare function placeReadDenyAtHome(env: ExecutionEnv, compile: (homes: readonly string[], linked?: ReadonlyMap<string, readonly string[]>) => ReadDenyMatcher, homes?: readonly string[], signal?: AbortSignal): Promise<{
|
|
49
|
+
readonly matcher: ReadDenyMatcher;
|
|
50
|
+
readonly homes: readonly string[];
|
|
51
|
+
readonly linked: ReadonlyMap<string, readonly string[]>;
|
|
52
|
+
readonly whole: boolean;
|
|
53
|
+
}>;
|
|
54
|
+
/**
|
|
55
|
+
* A judge for a mount that is built before its placement settles (the hands band is built synchronously): the table as
|
|
56
|
+
* `unplaced` compiles it — home-anchored rows held anywhere, the rule for a row that cannot be placed — until a
|
|
57
|
+
* placement settles, then the placed table. `place` starts at once; `ready()` settles when the latest placement has
|
|
58
|
+
* (either way) and, while no placement has come back WHOLE (a failed one, or one with a head that did not answer),
|
|
59
|
+
* places again first — an unanswered lookup is never kept as the answer. A failed placement leaves the table it
|
|
60
|
+
* replaced (the rows held anywhere, or an earlier placement), never unplaced-and-narrow. The mount holds its read faces
|
|
61
|
+
* until `ready()`.
|
|
62
|
+
*/
|
|
63
|
+
export declare function readDenyPlacedLater(unplaced: ReadDenyMatcher, place: () => Promise<{
|
|
64
|
+
readonly matcher: ReadDenyMatcher;
|
|
65
|
+
readonly whole: boolean;
|
|
66
|
+
}>): {
|
|
67
|
+
readonly matcher: ReadDenyMatcher;
|
|
68
|
+
readonly ready: () => Promise<void>;
|
|
69
|
+
};
|
|
70
|
+
/** The environment surface a home view reads. */
|
|
71
|
+
export type HomeViewEnv = Pick<ExecutionEnv, "homeDir" | "canonicalPath" | "fileIdentity">;
|
|
72
|
+
/**
|
|
73
|
+
* A path's HOME VIEW — the one reader every face that judges a deny row by location uses (the structured read and
|
|
74
|
+
* write tools' target judgment, the shell station's establishment, the search walker, the ripgrep leg's output check
|
|
75
|
+
* and its scope choice). The view of a path is the path itself when it IS a directory on the home's chain (the
|
|
76
|
+
* canonical home and each of its ancestors, matched by {@link ExecutionEnv.fileIdentity}) — that directory's
|
|
77
|
+
* canonical spelling — and otherwise its parent's view with its own name rejoined. `of` answers the view only where
|
|
78
|
+
* it differs from the path as spelled; `undefined` otherwise, and wherever it cannot be read:
|
|
79
|
+
* · the environment declares no home, no `fileIdentity`, or its home does not resolve ⇒ no view (the spelling form);
|
|
80
|
+
* a home, or a level of its chain, that could not be read is asked again by the next judgment — an unanswered
|
|
81
|
+
* lookup is never kept as the answer;
|
|
82
|
+
* · a level that does not exist (a write's missing tail) has no identity of its own and takes its parent's view;
|
|
83
|
+
* · any other failure at a level stops the view there: the path is judged as spelled, never wider.
|
|
84
|
+
* `leaf: "file"` skips the path's own identity (a file is never a directory on the chain).
|
|
85
|
+
*/
|
|
86
|
+
export interface HomeViews {
|
|
87
|
+
of(path: string, opts?: {
|
|
88
|
+
readonly leaf?: "file";
|
|
89
|
+
}): Promise<string | undefined>;
|
|
90
|
+
}
|
|
91
|
+
/** The home views of one judgment (a tool call, a command's establishment, one search): a view per directory is read
|
|
92
|
+
* once and shared by every path below it. */
|
|
93
|
+
export declare function homeViewsOf(env: HomeViewEnv, signal?: AbortSignal): HomeViews;
|