@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
|
@@ -22,14 +22,14 @@ function formatResourceClampNote(notes) {
|
|
|
22
22
|
const parts = notes.map((n) => `${n.field}: requested ${n.requested === undefined ? "unset" : n.requested} → applied ${n.applied}`);
|
|
23
23
|
return `workflow governance tightened this agent's resource limits (${parts.join("; ")})`;
|
|
24
24
|
}
|
|
25
|
-
export function buildWorkflowPrimitives(ctx, governance, onAgentSpawn, parentThinking, parentPrincipal, parentCheckpointStoreDisabled) {
|
|
25
|
+
export function buildWorkflowPrimitives(ctx, governance, onAgentSpawn, parentThinking, parentPrincipal, parentCheckpointStoreDisabled, parentReadFace, parentReadDenyPatterns) {
|
|
26
26
|
const agent = (spec, opts) => {
|
|
27
27
|
if (typeof spec === "string")
|
|
28
28
|
spec = { objective: spec };
|
|
29
29
|
const agentOpts = safeAgentOptions(opts);
|
|
30
30
|
const effectiveBaseline = (b) => agentOpts.isolation === "worktree" && b.worktreeBase !== undefined ? { ...b, base: { ...b.base, ...b.worktreeBase } } : b;
|
|
31
31
|
const childSpec = governance
|
|
32
|
-
? buildGovernedChildSpec(spec, effectiveBaseline(governance.baseline), governance.models, governance.caps, (notes) => ctx.log(formatResourceClampNote(notes)))
|
|
32
|
+
? buildGovernedChildSpec(spec, effectiveBaseline(governance.baseline), governance.models, governance.caps, (notes) => ctx.log(formatResourceClampNote(notes)), governance.onNotice)
|
|
33
33
|
: { ...spec };
|
|
34
34
|
if (childSpec.thinking === undefined && parentThinking) {
|
|
35
35
|
const inherited = parentThinking();
|
|
@@ -42,6 +42,17 @@ export function buildWorkflowPrimitives(ctx, governance, onAgentSpawn, parentThi
|
|
|
42
42
|
if (parentCheckpointStoreDisabled === true) {
|
|
43
43
|
childSpec.checkpointStore = null;
|
|
44
44
|
}
|
|
45
|
+
if (parentReadFace) {
|
|
46
|
+
const pf = parentReadFace();
|
|
47
|
+
if (pf === "roots" && childSpec.readFace !== "roots")
|
|
48
|
+
childSpec.readFace = "roots";
|
|
49
|
+
}
|
|
50
|
+
if (parentReadDenyPatterns) {
|
|
51
|
+
const pd = parentReadDenyPatterns();
|
|
52
|
+
if (pd !== undefined && pd.length > 0) {
|
|
53
|
+
childSpec.readDenyPatterns = childSpec.readDenyPatterns !== undefined ? [...childSpec.readDenyPatterns, ...pd] : [...pd];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
45
56
|
if (onAgentSpawn) {
|
|
46
57
|
return ctx.agentStream(childSpec, agentOpts).then((handle) => {
|
|
47
58
|
onAgentSpawn(handle);
|
|
@@ -22,6 +22,7 @@ const PROBE_FACTS_OFF = {
|
|
|
22
22
|
isSubagent: false,
|
|
23
23
|
promptProfile: "simple",
|
|
24
24
|
fableMitigations: false,
|
|
25
|
+
readFaceOpen: false,
|
|
25
26
|
};
|
|
26
27
|
const PROBE_FACTS_ON = {
|
|
27
28
|
policyEnabled: true,
|
|
@@ -38,6 +39,7 @@ const PROBE_FACTS_ON = {
|
|
|
38
39
|
isSubagent: true,
|
|
39
40
|
promptProfile: "classic",
|
|
40
41
|
fableMitigations: true,
|
|
42
|
+
readFaceOpen: true,
|
|
41
43
|
};
|
|
42
44
|
const PROBE_VECTORS = [
|
|
43
45
|
PROBE_FACTS_OFF,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CYBER_RISK, EXECUTION_ENVIRONMENT, URL_SAFETY, SUMMARIZE_TOOL_RESULTS, WORKTREE_NOTICE, SUBAGENT_CONSENT_NOTICE, SUBAGENT_DELIVERY_NOTES, harnessHeadLines, } from "../../prompts/default.js";
|
|
1
|
+
import { CYBER_RISK, EXECUTION_ENVIRONMENT, EXECUTION_ENVIRONMENT_OPEN_READS, URL_SAFETY, SUMMARIZE_TOOL_RESULTS, WORKTREE_NOTICE, SUBAGENT_CONSENT_NOTICE, SUBAGENT_DELIVERY_NOTES, harnessHeadLines, } from "../../prompts/default.js";
|
|
2
2
|
import { GOAL_COMPLETION_GUIDANCE, ORCHESTRATION_AWARENESS, ORCHESTRATION_GUIDANCE, ORCHESTRATION_GUIDANCE_DEFERRED, SUPERVISOR_PROMPT, } from "../../prompts/supervisor.js";
|
|
3
3
|
import { TEAMMATE_COMMUNICATION_ADDENDUM } from "../../prompts/coordinator.js";
|
|
4
4
|
import { SIMPLE_ACTION_CAUTION, SIMPLE_ACT_DONT_REDERIVE, SIMPLE_AUTONOMY_FABLE, SIMPLE_COMMUNICATING_FABLE, SIMPLE_CORRECTIONS_FABLE, SIMPLE_DELIVERING_WORK_FABLE, SIMPLE_COMMUNICATING_LEAN, SIMPLE_CONTEXT_MANAGEMENT, SIMPLE_PRONOUNS, SIMPLE_TOOL_PARAM_JSON, SEMA_VERIFY_FRESH, SEMA_EVIDENCE_AUDIT, } from "../../prompts/simple-sections.js";
|
|
@@ -41,7 +41,7 @@ export const SEMA_DEFAULT_PACK = {
|
|
|
41
41
|
rank: 240,
|
|
42
42
|
...CORE,
|
|
43
43
|
admit: (i) => i.facts.policyEnabled || i.facts.isolationEnabled,
|
|
44
|
-
content: () => EXECUTION_ENVIRONMENT,
|
|
44
|
+
content: (i) => (i.facts.readFaceOpen === true ? EXECUTION_ENVIRONMENT_OPEN_READS : EXECUTION_ENVIRONMENT),
|
|
45
45
|
legacyBlockId: "harness.context",
|
|
46
46
|
},
|
|
47
47
|
{ id: "core/simple.communicating", slot: "harness", rank: 250, ...CORE, admit: (i) => i.facts.promptProfile !== "classic", content: (i) => (i.facts.fableMitigations === true ? SIMPLE_COMMUNICATING_FABLE : SIMPLE_COMMUNICATING_LEAN), legacyBlockId: "harness.context" },
|
|
@@ -41,6 +41,10 @@ export interface PromptRuntimeFacts {
|
|
|
41
41
|
policyEnabled: boolean;
|
|
42
42
|
hooksEnabled: boolean;
|
|
43
43
|
isolationEnabled: boolean;
|
|
44
|
+
/** design/199 F8① — the resolved read face is OPEN (renders the execution-environment block's
|
|
45
|
+
* open-reads first bullet). OPTIONAL: absence reads as false (roots wording, byte-identical),
|
|
46
|
+
* so existing fact constructors keep compiling. */
|
|
47
|
+
readFaceOpen?: boolean;
|
|
44
48
|
withinTaskCompactionEnabled: boolean;
|
|
45
49
|
supervisorEnabled: boolean;
|
|
46
50
|
orchestrationEnabled: boolean;
|
|
@@ -161,15 +161,9 @@ export declare const URL_SAFETY = "IMPORTANT: You must NEVER generate or guess U
|
|
|
161
161
|
* runs before EVERY request, which is a stronger reason to warn than CC's.
|
|
162
162
|
*/
|
|
163
163
|
export declare const SUMMARIZE_TOOL_RESULTS = "When working with tool results, write down any important information you might need later in your own response, as the original tool result may be cleared or summarized from the context later.";
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
* we don't run it; this gives the model-visible contract for OUR exec-env (managed container / remote host)
|
|
168
|
-
* + the design/37 policy gate — how to read a restriction (permission/network/policy-deny) vs an ordinary
|
|
169
|
-
* failure, and to not circumvent a genuine restriction. Injected only when isolation OR a policy gate is
|
|
170
|
-
* active (§16.2) — never claim an isolated environment that isn't there.
|
|
171
|
-
*/
|
|
172
|
-
export declare const EXECUTION_ENVIRONMENT = "# Execution environment\nCommands run inside an isolated execution environment (a managed container or remote host), not on the operator's machine. Within it:\n- You can read and write within the project working directory. Writes outside it, or to system paths, may be denied by the environment or the permission policy.\n- Network access may be restricted to an allowlist. A blocked request fails at the network layer \u2014 it does not silently succeed.\n- A permission policy may intercept individual tool calls and deny them. A denied call did not run; do not re-issue the identical call. Follow the denial message's own guidance: a policy denial is something to reason about and adjust to, while a rejection by the user means stop and follow their direction rather than working around it. If you cannot tell why a call was denied, ask the user (via the AskUserQuestion tool, if available) rather than guessing.\n\nWhen a command fails, identify the cause before retrying:\n- Evidence of an environment/permission restriction: \"Operation not permitted\", \"Permission denied\" on an unexpected path, a network timeout/refusal to a host, or an explicit policy-deny message.\n- Ordinary failures (missing file, wrong argument, a non-zero exit from the program itself) are unrelated to isolation \u2014 fix the command rather than treating it as a restriction.\n\nIf a restriction genuinely blocks a necessary action, do NOT attempt to circumvent it (no privilege escalation, no disabling of guards, no destructive workarounds). Adjust your approach, or surface the limitation to the user with the specific evidence you saw.";
|
|
164
|
+
export declare const EXECUTION_ENVIRONMENT: string;
|
|
165
|
+
/** design/199 F8① — the open-read-face variant (first bullet tells the truth about reads). */
|
|
166
|
+
export declare const EXECUTION_ENVIRONMENT_OPEN_READS: string;
|
|
173
167
|
/**
|
|
174
168
|
* design/97 CORE-6 (P1b) — worktree-isolation NOTICE. Composed (via {@link StablePromptContext.worktreeIsolated})
|
|
175
169
|
* only when the task runs in an isolated git worktree, so the model treats inherited paths correctly. Generic
|
|
@@ -291,6 +285,10 @@ export interface EnvironmentFacts {
|
|
|
291
285
|
/** Ruled 2026-08-05 (read-boundary whitelist): extra directories the READ faces may access
|
|
292
286
|
* (canonical) — reads auto-classify inside them, writes are refused exactly as before. */
|
|
293
287
|
additionalReadDirectories?: readonly string[];
|
|
288
|
+
/** design/199 件A — the RESOLVED read-face containment state ("open" | "roots"; additive
|
|
289
|
+
* structured key). Rendered only when "open" (the roots posture is the historical default and
|
|
290
|
+
* renders nothing new — byte-compat). */
|
|
291
|
+
readFace?: "open" | "roots";
|
|
294
292
|
/** OS name (uname -s) — remote = the container's, not the host's. */
|
|
295
293
|
platform?: string;
|
|
296
294
|
/** OS version (uname -r). */
|
|
@@ -444,6 +442,13 @@ export interface StablePromptContext {
|
|
|
444
442
|
* line in {@link harnessContext}. Don't claim a deny mechanism the task doesn't have (§6.3).
|
|
445
443
|
*/
|
|
446
444
|
policyEnabled?: boolean;
|
|
445
|
+
/**
|
|
446
|
+
* design/199 F8① — the resolved read face is OPEN: the `# Execution environment` block's first
|
|
447
|
+
* bullet renders its open-reads variant ({@link EXECUTION_ENVIRONMENT_OPEN_READS}) so the prompt
|
|
448
|
+
* never claims a read fence that is not there. Absent/false ⇒ the historical (roots) wording,
|
|
449
|
+
* byte-identical.
|
|
450
|
+
*/
|
|
451
|
+
readFaceOpen?: boolean;
|
|
447
452
|
/**
|
|
448
453
|
* Whether hooks are wired (design/37) — drives the "hook output is user feedback" line in
|
|
449
454
|
* {@link harnessContext}. Omitted/false → that line is left out (§6.3).
|
package/dist/prompts/default.js
CHANGED
|
@@ -108,9 +108,13 @@ You have no persistent memory store: what is said in this conversation is not au
|
|
|
108
108
|
export const CYBER_RISK = `IMPORTANT: Assist with authorized security testing, defensive security, CTF challenges, and educational contexts. Refuse requests for destructive techniques, DoS attacks, mass targeting, supply chain compromise, or detection evasion for malicious purposes. Dual-use security tools (C2 frameworks, credential testing, exploit development) require clear authorization context: pentesting engagements, CTF competitions, security research, or defensive use cases.`;
|
|
109
109
|
export const URL_SAFETY = `IMPORTANT: You must NEVER generate or guess URLs for the user unless you are confident that the URLs are for helping the user with programming. You may use URLs provided by the user in their messages or local files.`;
|
|
110
110
|
export const SUMMARIZE_TOOL_RESULTS = `When working with tool results, write down any important information you might need later in your own response, as the original tool result may be cleared or summarized from the context later.`;
|
|
111
|
-
|
|
111
|
+
function executionEnvironmentBlock(openReads) {
|
|
112
|
+
const readWriteLine = openReads
|
|
113
|
+
? "- You can read files anywhere this environment exposes (a small sensitive-path deny list applies), and write within the project working directory. Writes outside it, or to system paths, may be denied by the environment or the permission policy."
|
|
114
|
+
: "- You can read and write within the project working directory. Writes outside it, or to system paths, may be denied by the environment or the permission policy.";
|
|
115
|
+
return `# Execution environment
|
|
112
116
|
Commands run inside an isolated execution environment (a managed container or remote host), not on the operator's machine. Within it:
|
|
113
|
-
|
|
117
|
+
${readWriteLine}
|
|
114
118
|
- Network access may be restricted to an allowlist. A blocked request fails at the network layer — it does not silently succeed.
|
|
115
119
|
- A permission policy may intercept individual tool calls and deny them. A denied call did not run; do not re-issue the identical call. Follow the denial message's own guidance: a policy denial is something to reason about and adjust to, while a rejection by the user means stop and follow their direction rather than working around it. If you cannot tell why a call was denied, ask the user (via the AskUserQuestion tool, if available) rather than guessing.
|
|
116
120
|
|
|
@@ -119,6 +123,9 @@ When a command fails, identify the cause before retrying:
|
|
|
119
123
|
- Ordinary failures (missing file, wrong argument, a non-zero exit from the program itself) are unrelated to isolation — fix the command rather than treating it as a restriction.
|
|
120
124
|
|
|
121
125
|
If a restriction genuinely blocks a necessary action, do NOT attempt to circumvent it (no privilege escalation, no disabling of guards, no destructive workarounds). Adjust your approach, or surface the limitation to the user with the specific evidence you saw.`;
|
|
126
|
+
}
|
|
127
|
+
export const EXECUTION_ENVIRONMENT = executionEnvironmentBlock(false);
|
|
128
|
+
export const EXECUTION_ENVIRONMENT_OPEN_READS = executionEnvironmentBlock(true);
|
|
122
129
|
export const WORKTREE_NOTICE = `# Isolated worktree
|
|
123
130
|
This task runs in its own isolated git worktree — a separate working copy whose root is the working directory shown in # Environment, NOT the repository's main checkout. Any absolute path you were given that points at the main checkout (or another worktree) refers to a DIFFERENT copy; translate it to the same relative path under this worktree's root before reading or writing, and operate only within this worktree. A file's content here may differ from the main checkout, so re-read a file in this worktree before editing it rather than assuming an earlier or external view is current.`;
|
|
124
131
|
export const WORKTREE_STASH_WARNING = "The git stash stack is shared with the main checkout and all other worktrees, and other agent sessions may push or pop it concurrently. " +
|
|
@@ -151,7 +158,7 @@ export function harnessHeadLines(ctx) {
|
|
|
151
158
|
export function harnessContext(ctx) {
|
|
152
159
|
const blocks = [harnessHeadLines(ctx), CYBER_RISK, URL_SAFETY, SUMMARIZE_TOOL_RESULTS];
|
|
153
160
|
if (ctx.policyEnabled || ctx.isolationEnabled)
|
|
154
|
-
blocks.push(EXECUTION_ENVIRONMENT);
|
|
161
|
+
blocks.push(ctx.readFaceOpen === true ? EXECUTION_ENVIRONMENT_OPEN_READS : EXECUTION_ENVIRONMENT);
|
|
155
162
|
if (ctx.promptProfile !== "classic") {
|
|
156
163
|
blocks.push(ctx.fableMitigations === true ? SIMPLE_COMMUNICATING_FABLE : SIMPLE_COMMUNICATING_LEAN);
|
|
157
164
|
blocks.push(SIMPLE_PRONOUNS);
|
|
@@ -243,6 +250,9 @@ export function buildEnvironmentContext(facts) {
|
|
|
243
250
|
if (facts.additionalReadDirectories && facts.additionalReadDirectories.length > 0) {
|
|
244
251
|
lines.push(`Additional read-only directories: ${facts.additionalReadDirectories.map((d) => inlineUntrusted(d)).join(", ")}`);
|
|
245
252
|
}
|
|
253
|
+
if (facts.readFace === "open") {
|
|
254
|
+
lines.push("File reads are not confined to the workspace root. A small sensitive-path deny list applies (see refusals for the exact pattern).");
|
|
255
|
+
}
|
|
246
256
|
if (facts.isGitRepo !== undefined)
|
|
247
257
|
lines.push(`Is a git repository: ${facts.isGitRepo ? "yes" : "no"}`);
|
|
248
258
|
if (facts.gitBranch)
|
|
@@ -91,6 +91,27 @@ export interface BashReadonlyRootBoundary {
|
|
|
91
91
|
* out-of-root signal — the classifier does not know where it points, which is a different verdict
|
|
92
92
|
* from knowing it points outside). */
|
|
93
93
|
homeDir?: string;
|
|
94
|
+
/** design/199 件B — the sensitive-path read deny judge over LEXICALLY RESOLVED operands: a hit
|
|
95
|
+
* demotes the command (ask, never auto-allow), independently of the roots — in-root operands are
|
|
96
|
+
* judged too. Returns the matched pattern, or null. TWO named residuals, both inherited from this
|
|
97
|
+
* classifier's declared purity (synchronous, zero I/O — RB-448/RB-451 state the same scope for the
|
|
98
|
+
* containment half): ① operand TARGET matching only — no ancestor intersection, so `grep -r x ~/`
|
|
99
|
+
* whose operand is `~` itself does not demote here (§3.4; the recursive reach residual belongs to
|
|
100
|
+
* the full-bash lane's honest scope note); ② LEXICAL only — an in-root symlink whose target is a
|
|
101
|
+
* guarded path reads as its innocent spelling here, exactly as it does for the containment half
|
|
102
|
+
* (the enforcing/canonicalizing recheck is the bash_readonly leg's job via checkedPaths; the
|
|
103
|
+
* classify auto-allow lane has no I/O seat by contract). The structured read faces judge BOTH
|
|
104
|
+
* views (canonical + lexical) — this seat is the shell lane's honest-friction floor, not its
|
|
105
|
+
* security boundary (§3.0). The `bash_readonly` face deliberately does NOT wire this seat (its
|
|
106
|
+
* roots containment + command allowlist double gate is the deployment's own read-safety
|
|
107
|
+
* declaration — v1 ruling). */
|
|
108
|
+
denyMatch?: (resolvedPath: string) => string | null;
|
|
109
|
+
/** design/199 件A — the resolved read-face containment state. Under "open" the CONTAINMENT half of
|
|
110
|
+
* this boundary is structurally satisfied (an out-of-roots operand is not a demotion; it reports
|
|
111
|
+
* as a checked candidate instead), while the deny half above keeps judging in both faces (§2.0).
|
|
112
|
+
* Absent ⇒ "roots" (byte-compat). The `bash_readonly` face never passes this seat — its
|
|
113
|
+
* containment is load-bearing and never opens. */
|
|
114
|
+
face?: "open" | "roots";
|
|
94
115
|
}
|
|
95
116
|
/**
|
|
96
117
|
* RB-412 — the structured verdict of {@link classifyCompoundReadonlyDetailed}. `reason === undefined`
|
|
@@ -151,8 +172,32 @@ export interface CompoundReadonlyVerdict {
|
|
|
151
172
|
*
|
|
152
173
|
* `cd` is the one exception, and it fails closed: a glob there cannot be expanded into the single
|
|
153
174
|
* directory the compound face must track as the new working directory, so it demotes (see {@link reason}).
|
|
175
|
+
*
|
|
176
|
+
* ALSO carries the {@link recursiveReadPaths} entries (a subset): both families are operands whose
|
|
177
|
+
* real read set is not in the command text, and the consumer contract above ("non-empty ⇒ ask, or
|
|
178
|
+
* resolve yourself") is stated over this one field so an auto-allow gate cannot honour one family
|
|
179
|
+
* and miss the other.
|
|
154
180
|
*/
|
|
155
181
|
undecidedPaths?: readonly string[];
|
|
182
|
+
/**
|
|
183
|
+
* Operands of a RECURSIVE/EXPANDING read form (`grep -r`, `ls -R`, `du`, … — see
|
|
184
|
+
* {@link RECURSIVE_READ_FORMS}) judged with a {@link BashReadonlyRootBoundary.denyMatch} seat wired.
|
|
185
|
+
* The deny judge sees only the operand's own resolved spelling, but a recursive verb reads the
|
|
186
|
+
* operand's whole SUBTREE — `grep -r x /home/user` touches `/home/user/.ssh/*` while the judged
|
|
187
|
+
* spelling `/home/user` matches no deny pattern. The traversal's reach set is therefore not covered
|
|
188
|
+
* by the lexical check at all, and with zero I/O "provably not a directory" does not exist — so
|
|
189
|
+
* every such operand is UNDECIDED (no directory guessing, no bounded pre-check: both would be a
|
|
190
|
+
* false "resolved, inside" of exactly the kind {@link undecidedPaths} exists to prevent).
|
|
191
|
+
*
|
|
192
|
+
* Subset of {@link undecidedPaths} (same consumer contract: ask, never auto-allow); carried
|
|
193
|
+
* separately so a consumer minting prose can name the recursive-reach cause rather than the glob
|
|
194
|
+
* one. Minted ONLY when `denyMatch` is wired: without a deny judge there is nothing the traversal
|
|
195
|
+
* bypasses — the containment half already judges the operand itself, and its lexical residuals are
|
|
196
|
+
* recorded on {@link checkedPaths}. The `bash_readonly` face never wires `denyMatch` (v1 ruling,
|
|
197
|
+
* see that seat's note), so this field never appears there and its expand-and-verify execution
|
|
198
|
+
* path is unchanged.
|
|
199
|
+
*/
|
|
200
|
+
recursiveReadPaths?: readonly string[];
|
|
156
201
|
}
|
|
157
202
|
/**
|
|
158
203
|
* RB-412 — the single minting point for the out-of-root-read approval option text, so a gate rendering
|
|
@@ -203,10 +248,14 @@ export declare function classifyCompoundReadonlyDetailed(command: string, allow:
|
|
|
203
248
|
export declare function classifySimpleCommandReadBoundary(command: string, boundary: BashReadonlyRootBoundary): CompoundReadonlyVerdict;
|
|
204
249
|
/**
|
|
205
250
|
* design/154 — compound read-only classification, reason-only face. Returns the demotion reason, or
|
|
206
|
-
* undefined when the command classifies read-only.
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
251
|
+
* undefined when the command classifies read-only. ⚠️ `undefined` is NOT "safe to auto-execute":
|
|
252
|
+
* the detailed verdict may still carry `undecidedPaths` (operands whose unexpanded spelling — a
|
|
253
|
+
* glob — is what got checked), and this face discards that field. An auto-allow decision must read
|
|
254
|
+
* {@link classifyCompoundReadonlyDetailed} and treat a non-empty `undecidedPaths` as ask — the
|
|
255
|
+
* engine's own probe does exactly that (fs-bash.ts). RB-412 added the optional `boundary`: with it,
|
|
256
|
+
* an allowlisted reader whose path arguments leave the allowed directories is demoted too (use the
|
|
257
|
+
* detailed face when the caller wants to know WHY, e.g. to offer the narrow "allow reading from
|
|
258
|
+
* <dir>" approval); without it the verdict is exactly what it always was.
|
|
210
259
|
*/
|
|
211
260
|
export declare function classifyCompoundReadonly(command: string, allow: ReadonlySet<string>, boundary?: BashReadonlyRootBoundary): string | undefined;
|
|
212
261
|
/**
|
|
@@ -284,6 +284,90 @@ function takesSeparatedValue(name, tok) {
|
|
|
284
284
|
}
|
|
285
285
|
return false;
|
|
286
286
|
}
|
|
287
|
+
const RECURSIVE_READ_FORMS = {
|
|
288
|
+
grep: {
|
|
289
|
+
shortLetters: "rR",
|
|
290
|
+
valueOwners: "efmABCD",
|
|
291
|
+
longNames: ["recursive", "dereference-recursive"],
|
|
292
|
+
enumOptions: [{ shortLetter: "d", longName: "directories", recursiveValue: "recurse" }],
|
|
293
|
+
dashIsStdin: true,
|
|
294
|
+
},
|
|
295
|
+
ls: { shortLetters: "R", longNames: ["recursive"] },
|
|
296
|
+
du: { always: true },
|
|
297
|
+
find: { always: true },
|
|
298
|
+
rg: { always: true, dashIsStdin: true },
|
|
299
|
+
tree: { always: true },
|
|
300
|
+
ag: { always: true, dashIsStdin: true },
|
|
301
|
+
ack: { always: true, dashIsStdin: true },
|
|
302
|
+
tar: { shortLetters: "cru", valueOwners: "fCTXbg", longNames: ["create", "append", "update"], bundledModeLetters: "cru", dashIsStdin: true },
|
|
303
|
+
diff: { shortLetters: "r", valueOwners: "UCWISFXx", longNames: ["recursive"], dashIsStdin: true },
|
|
304
|
+
};
|
|
305
|
+
function segmentSelectsRecursiveRead(name, args) {
|
|
306
|
+
const model = RECURSIVE_READ_FORMS[name];
|
|
307
|
+
if (model === undefined)
|
|
308
|
+
return false;
|
|
309
|
+
if (model.always === true)
|
|
310
|
+
return true;
|
|
311
|
+
if (model.bundledModeLetters !== undefined) {
|
|
312
|
+
const first = args.find((t) => t.length > 0);
|
|
313
|
+
if (first !== undefined && !first.startsWith("-") && /^[A-Za-z]+$/.test(first) && [...first].some((ch) => model.bundledModeLetters.includes(ch))) {
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
let endOfOptions = false;
|
|
318
|
+
for (let k = 0; k < args.length; k++) {
|
|
319
|
+
const t = args[k];
|
|
320
|
+
if (endOfOptions)
|
|
321
|
+
continue;
|
|
322
|
+
if (t === "--") {
|
|
323
|
+
const prev = k > 0 ? args[k - 1] : undefined;
|
|
324
|
+
const prevMayOwnValue = prev !== undefined && prev.startsWith("--") && prev.length > 2 && !prev.includes("=");
|
|
325
|
+
if (!prevMayOwnValue)
|
|
326
|
+
endOfOptions = true;
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (t.startsWith("--")) {
|
|
330
|
+
const long = longOptionNameOf(t);
|
|
331
|
+
if (long === undefined)
|
|
332
|
+
continue;
|
|
333
|
+
if (model.longNames?.some((full) => isLongOptionAbbrevOf(long, full)) === true)
|
|
334
|
+
return true;
|
|
335
|
+
for (const en of model.enumOptions ?? []) {
|
|
336
|
+
if (en.longName === undefined || !isLongOptionAbbrevOf(long, en.longName))
|
|
337
|
+
continue;
|
|
338
|
+
const eq = t.indexOf("=");
|
|
339
|
+
const v = eq >= 0 ? t.slice(eq + 1) : args[k + 1];
|
|
340
|
+
if (v !== undefined && v.length > 0 && en.recursiveValue.startsWith(v.toLowerCase()))
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
if (!t.startsWith("-") || t === "-")
|
|
346
|
+
continue;
|
|
347
|
+
for (let i = 1; i < t.length; i++) {
|
|
348
|
+
const ch = t[i];
|
|
349
|
+
if (model.shortLetters?.includes(ch) === true)
|
|
350
|
+
return true;
|
|
351
|
+
const en = (model.enumOptions ?? []).find((e) => e.shortLetter === ch);
|
|
352
|
+
if (en !== undefined) {
|
|
353
|
+
const v = i === t.length - 1 ? args[k + 1] : t.slice(i + 1);
|
|
354
|
+
if (v !== undefined && v.length > 0 && en.recursiveValue.startsWith(v.toLowerCase()))
|
|
355
|
+
return true;
|
|
356
|
+
if (i === t.length - 1)
|
|
357
|
+
k++;
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
if (model.valueOwners?.includes(ch) === true) {
|
|
361
|
+
if (i === t.length - 1)
|
|
362
|
+
k++;
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
if (!/[A-Za-z0-9]/.test(ch))
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
287
371
|
function isGrepFileStdinLongOption(tok) {
|
|
288
372
|
const eq = tok.indexOf("=");
|
|
289
373
|
if (eq < 0 || tok.slice(eq + 1) !== "-")
|
|
@@ -327,6 +411,7 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
|
|
|
327
411
|
const findings = [];
|
|
328
412
|
const candidates = [];
|
|
329
413
|
const bareWordOperands = [];
|
|
414
|
+
const positionalOperands = [];
|
|
330
415
|
const argGlobs = (k) => {
|
|
331
416
|
const raw = tokens.raw[k + 1];
|
|
332
417
|
return raw !== undefined && hasUnquotedGlobMetachar(raw);
|
|
@@ -356,31 +441,47 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
|
|
|
356
441
|
candidates.push({ text: target, globbed: false });
|
|
357
442
|
}
|
|
358
443
|
else {
|
|
359
|
-
const
|
|
444
|
+
const eoo = args.indexOf("--");
|
|
445
|
+
const patternSuppliedByFlag = name === "grep" && (eoo === -1 ? args : args.slice(0, eoo)).some(isGrepPatternFlagToken);
|
|
360
446
|
let sawOperand = false;
|
|
447
|
+
let endOfOptions = false;
|
|
361
448
|
for (let k = 0; k < args.length; k++) {
|
|
362
449
|
const t = args[k];
|
|
363
|
-
if (
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
450
|
+
if (!endOfOptions) {
|
|
451
|
+
if (t === "--") {
|
|
452
|
+
endOfOptions = true;
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
if (name === "cut" && (t === "-d" || t === "--delimiter" || t === "--output-delimiter")) {
|
|
456
|
+
k++;
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
if (name === "cut" && (/^-d./.test(t) || isCutDelimiterPayloadLongOption(t)))
|
|
460
|
+
continue;
|
|
461
|
+
if (name === "grep" && (grepClusterValueOwner(t) === "e" || isGrepPatternPayloadLongOption(t)))
|
|
462
|
+
continue;
|
|
463
|
+
if (t.startsWith("-") && t !== "-") {
|
|
464
|
+
for (const payload of attachedOptionPayloads(t)) {
|
|
465
|
+
if (isAbsolutePathForm(payload) || isPathShapedToken(payload))
|
|
466
|
+
candidates.push({ text: payload, globbed: argGlobs(k) });
|
|
467
|
+
}
|
|
468
|
+
continue;
|
|
375
469
|
}
|
|
376
|
-
continue;
|
|
377
470
|
}
|
|
378
471
|
const isGrepPatternSlot = name === "grep" && !patternSuppliedByFlag && !sawOperand;
|
|
379
472
|
sawOperand = true;
|
|
380
|
-
if (t === "-")
|
|
473
|
+
if (t === "-") {
|
|
474
|
+
if (isGrepPatternSlot)
|
|
475
|
+
continue;
|
|
476
|
+
const m = RECURSIVE_READ_FORMS[name];
|
|
477
|
+
if (m === undefined || m.dashIsStdin === true)
|
|
478
|
+
continue;
|
|
479
|
+
positionalOperands.push(t);
|
|
381
480
|
continue;
|
|
481
|
+
}
|
|
382
482
|
if (isGrepPatternSlot)
|
|
383
483
|
continue;
|
|
484
|
+
positionalOperands.push(t);
|
|
384
485
|
if (isPathShapedToken(t))
|
|
385
486
|
candidates.push({ text: t, globbed: argGlobs(k) });
|
|
386
487
|
else
|
|
@@ -416,6 +517,13 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
|
|
|
416
517
|
if (!globbed && withinAnyRoot(boundary.roots, resolved))
|
|
417
518
|
findings.push({ kind: "inside", path: resolved });
|
|
418
519
|
}
|
|
520
|
+
if (boundary.denyMatch !== undefined && segmentSelectsRecursiveRead(name, args)) {
|
|
521
|
+
const roots = positionalOperands.length > 0 ? positionalOperands : ["."];
|
|
522
|
+
for (const operand of roots) {
|
|
523
|
+
const resolved = resolveOperandLexically(boundary.cwd ?? boundary.roots[0], operand, boundary.homeDir);
|
|
524
|
+
findings.push({ kind: "recursive", path: resolved ?? operand });
|
|
525
|
+
}
|
|
526
|
+
}
|
|
419
527
|
return findings;
|
|
420
528
|
}
|
|
421
529
|
export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
|
|
@@ -584,10 +692,17 @@ function evaluateReadBoundary(foldedSegments, boundary) {
|
|
|
584
692
|
const outside = [];
|
|
585
693
|
const inside = [];
|
|
586
694
|
const undecided = [];
|
|
695
|
+
const recursive = [];
|
|
587
696
|
for (const toks of foldedSegments) {
|
|
588
697
|
for (const finding of collectSegmentBoundaryFindings(toks, boundary)) {
|
|
589
698
|
if (finding.kind === "unresolvable")
|
|
590
699
|
return { reason: finding.reason };
|
|
700
|
+
const denied = boundary.denyMatch?.(finding.path);
|
|
701
|
+
if (denied != null) {
|
|
702
|
+
return {
|
|
703
|
+
reason: `a command operand resolves to "${finding.path}", which matches the sensitive-path read deny list (pattern "${denied}") ${NOT_AUTO_ALLOWED}`,
|
|
704
|
+
};
|
|
705
|
+
}
|
|
591
706
|
if (finding.kind === "inside") {
|
|
592
707
|
if (!inside.includes(finding.path))
|
|
593
708
|
inside.push(finding.path);
|
|
@@ -598,11 +713,25 @@ function evaluateReadBoundary(foldedSegments, boundary) {
|
|
|
598
713
|
undecided.push(finding.path);
|
|
599
714
|
continue;
|
|
600
715
|
}
|
|
716
|
+
if (finding.kind === "recursive") {
|
|
717
|
+
if (!recursive.includes(finding.path))
|
|
718
|
+
recursive.push(finding.path);
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
if (boundary.face === "open") {
|
|
722
|
+
if (!inside.includes(finding.path))
|
|
723
|
+
inside.push(finding.path);
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
601
726
|
if (!outside.some((o) => o.path === finding.path))
|
|
602
727
|
outside.push(finding);
|
|
603
728
|
}
|
|
604
729
|
}
|
|
605
|
-
const
|
|
730
|
+
const undecidedAll = [...undecided, ...recursive.filter((p) => !undecided.includes(p))];
|
|
731
|
+
const undecidedField = {
|
|
732
|
+
...(undecidedAll.length > 0 ? { undecidedPaths: undecidedAll } : {}),
|
|
733
|
+
...(recursive.length > 0 ? { recursiveReadPaths: recursive } : {}),
|
|
734
|
+
};
|
|
606
735
|
if (outside.length === 0)
|
|
607
736
|
return inside.length > 0 ? { checkedPaths: inside, ...undecidedField } : { ...undecidedField };
|
|
608
737
|
const paths = outside.map((o) => `"${o.path}"`).join(", ");
|
|
@@ -713,6 +842,9 @@ export function classifyBoundedReadonlyPollLoop(command, allow, boundary) {
|
|
|
713
842
|
const verdict = classifyCompoundReadonlyDetailed(readSegments.join("; "), allow, boundary);
|
|
714
843
|
if (verdict.reason !== undefined)
|
|
715
844
|
return verdict.reason;
|
|
845
|
+
if (verdict.recursiveReadPaths !== undefined) {
|
|
846
|
+
return `the loop body reads recursively from ${verdict.recursiveReadPaths.join(", ")} — the traversal's reach is not covered by this lexical check, so it is not auto-allowed`;
|
|
847
|
+
}
|
|
716
848
|
if (verdict.undecidedPaths !== undefined) {
|
|
717
849
|
return `the loop body carries an unexpanded glob (${verdict.undecidedPaths.join(", ")}) — what a REPEATED read touches is decided at run time, so it is not auto-allowed`;
|
|
718
850
|
}
|
|
@@ -40,6 +40,13 @@ import { type BashReadonlyRootBoundary } from "./bash-readonly-classifier.js";
|
|
|
40
40
|
export declare function bashReversibilityProbe(allow?: readonly string[], boundary?: BashReadonlyRootBoundary | (() => BashReadonlyRootBoundary | undefined)): (args: unknown) => {
|
|
41
41
|
reversible: boolean;
|
|
42
42
|
};
|
|
43
|
+
/**
|
|
44
|
+
* design/199 D-6 — the FULL shell's contract id, single-sourced: both shell faces share the wire
|
|
45
|
+
* name "Bash", so this id is the ONE structural discriminator between the write-capable shell and
|
|
46
|
+
* the read-only allowlist face (`core.bash_readonly@1`). Consumed by prepare-task's
|
|
47
|
+
* fullShellReachable roster assertion (its first load-bearing consumer).
|
|
48
|
+
*/
|
|
49
|
+
export declare const FULL_SHELL_CONTRACT_ID = "core.bash@1";
|
|
43
50
|
/** The CC-verbatim exit-1 interpretation for `command`, or undefined when exit 1 means a real error.
|
|
44
51
|
* Conservative parse: last `;`/`&&`/`||`/newline statement → last `|` pipeline segment → leading
|
|
45
52
|
* command name (env-assignments skipped, path prefix stripped); `git grep`/`git diff` special-cased
|
package/dist/tools/fs/fs-bash.js
CHANGED
|
@@ -10,7 +10,7 @@ import { imageMagicMatches, withinAnyRoot } from "./safety.js";
|
|
|
10
10
|
import { isRemoteExecutionEnv, hasDestroy, isIsolated } from "../../core/remote-env.js";
|
|
11
11
|
import { ghRateLimitHint } from "./gh-rate-limit.js";
|
|
12
12
|
import { resolveBashTimeoutCaps, bashTimeoutCapsSec, bashTimeoutArgRefusal, bashTimeoutParamDescription, envErrorDetail, bashMaxOutputChars, clipShellOutput, writeShellOverflowFile, createShellOverflowSpoolFence, shellRecoveryHint, CWD_SENTINEL, BASH_READONLY_CONFINEMENT_NOTE, } from "./fs-shared.js";
|
|
13
|
-
import { BASH_READONLY_DEFAULT_ALLOW, coarseReadonlyCheck, classifyBoundedReadonlyPollLoop,
|
|
13
|
+
import { BASH_READONLY_DEFAULT_ALLOW, coarseReadonlyCheck, classifyBoundedReadonlyPollLoop, classifyCompoundReadonlyDetailed, classifySimpleCommandReadBoundary, NOT_AUTO_ALLOWED, } from "./bash-readonly-classifier.js";
|
|
14
14
|
export function bashReversibilityProbe(allow, boundary) {
|
|
15
15
|
const allowSet = new Set(allow ?? BASH_READONLY_DEFAULT_ALLOW);
|
|
16
16
|
return (args) => {
|
|
@@ -21,11 +21,16 @@ export function bashReversibilityProbe(allow, boundary) {
|
|
|
21
21
|
if (a?.run_in_background === true)
|
|
22
22
|
return { reversible: false };
|
|
23
23
|
const resolved = typeof boundary === "function" ? boundary() : boundary;
|
|
24
|
-
|
|
24
|
+
const detailed = classifyCompoundReadonlyDetailed(command, allowSet, resolved);
|
|
25
|
+
if (detailed.reason === undefined) {
|
|
26
|
+
if (detailed.undecidedPaths !== undefined && detailed.undecidedPaths.length > 0)
|
|
27
|
+
return { reversible: false };
|
|
25
28
|
return { reversible: true };
|
|
29
|
+
}
|
|
26
30
|
return { reversible: classifyBoundedReadonlyPollLoop(command, allowSet, resolved) === undefined };
|
|
27
31
|
};
|
|
28
32
|
}
|
|
33
|
+
export const FULL_SHELL_CONTRACT_ID = "core.bash@1";
|
|
29
34
|
const EXIT1_INTERPRETATION = {
|
|
30
35
|
grep: "No matches found",
|
|
31
36
|
rg: "No matches found",
|
|
@@ -497,7 +502,7 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
|
|
|
497
502
|
const bgEnvIsolatedOwned = hasDestroy(env) && isIsolated(env);
|
|
498
503
|
return defineTool({
|
|
499
504
|
name: "Bash",
|
|
500
|
-
contract: { contractId:
|
|
505
|
+
contract: { contractId: FULL_SHELL_CONTRACT_ID, implementationRevision: "1" },
|
|
501
506
|
description: bashDescription(coAuthor, timeoutCaps, bgNotifies, bgRetained, bgSessionScoped, bgEnvIsolatedOwned),
|
|
502
507
|
parameters: Type.Object({
|
|
503
508
|
command: Type.String({ description: "The command to execute" }),
|
|
@@ -33,7 +33,7 @@ type ReadPdfReturn = string | {
|
|
|
33
33
|
* Every degraded return carries `details.fallback = { level, reason }` (telemetry on the structured frame).
|
|
34
34
|
* `caps` absent ⇒ fully capable (byte-compat: native document block; the brain placeholder still guards).
|
|
35
35
|
*/
|
|
36
|
-
export declare function readPdfFile(env: ExecutionEnv, path: string, key: string, pages: string | undefined, signal: AbortSignal | undefined, downsamplerOpt: ReadImageDownsamplerOption, cwd: string, preRead?: Uint8Array, caps?: PdfModelCapabilities): Promise<ReadPdfReturn>;
|
|
36
|
+
export declare function readPdfFile(env: ExecutionEnv, path: string, key: string, pages: string | undefined, signal: AbortSignal | undefined, downsamplerOpt: ReadImageDownsamplerOption, cwd: string, preRead?: Uint8Array, caps?: PdfModelCapabilities, readDeny?: import("./read-deny.js").ReadDenyMatcher): Promise<ReadPdfReturn>;
|
|
37
37
|
/** E1: readPdfFile's own return type stays `ReadPdfReturn` (its INTERNAL string-means-error dispatch
|
|
38
38
|
* contract, shared with pdfPagesToImageBlocks) — the isError flag is applied once, here, at the tool's
|
|
39
39
|
* actual execute() boundary, not inside the helper. */
|
package/dist/tools/fs/fs-pdf.js
CHANGED
|
@@ -30,13 +30,13 @@ function boundPdfExtractedText(text) {
|
|
|
30
30
|
return { body: text, truncated: false };
|
|
31
31
|
return { body: text.slice(0, MAX_READ_BYTES), truncated: true };
|
|
32
32
|
}
|
|
33
|
-
export async function readPdfFile(env, path, key, pages, signal, downsamplerOpt, cwd, preRead, caps) {
|
|
33
|
+
export async function readPdfFile(env, path, key, pages, signal, downsamplerOpt, cwd, preRead, caps, readDeny) {
|
|
34
34
|
const cap = caps ?? { document: true, vision: true };
|
|
35
35
|
const meta = await env.fileInfo(key, signal);
|
|
36
36
|
if (!meta.ok) {
|
|
37
37
|
const ex = await env.exists(key, signal);
|
|
38
38
|
if (ex.ok && !ex.value)
|
|
39
|
-
return `Error (Read): ${await enoentMessage(env, key, cwd, signal)}`;
|
|
39
|
+
return `Error (Read): ${await enoentMessage(env, key, cwd, signal, readDeny)}`;
|
|
40
40
|
return `Error (Read): cannot stat PDF "${path}" to verify its size before reading: ${meta.error.message}`;
|
|
41
41
|
}
|
|
42
42
|
if (meta.value.kind === "directory") {
|
|
@@ -5,4 +5,4 @@ import { type ReadImageDownsamplerOption, type CwdRef } from "./fs-shared.js";
|
|
|
5
5
|
export declare function createReadFileTool(env: ExecutionEnv, state: ReadFileState, rootCanonical: string, cwdRef?: CwdRef, additionalRoots?: readonly string[], imageDownsampler?: ReadImageDownsamplerOption, pdfCapabilities?: PdfModelCapabilities, bgOutputReadExemption?: (canonicalKey: string, ctx: {
|
|
6
6
|
taskId?: string;
|
|
7
7
|
principal?: string;
|
|
8
|
-
}) => boolean, readCyberReminder?: boolean): AgentTool;
|
|
8
|
+
}) => boolean, readCyberReminder?: boolean, readDeny?: import("./read-deny.js").ReadDenyMatcher, readFace?: import("./read-face.js").ReadFace): AgentTool;
|