@sema-agent/core 7.33.8 → 7.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +339 -0
- package/dist/agents/cross-session-judge.d.ts +16 -7
- package/dist/agents/cross-session-judge.js +18 -15
- package/dist/agents/peer-session-drain.d.ts +4 -0
- package/dist/agents/peer-session-drain.js +5 -2
- package/dist/agents/repair-loop.d.ts +1 -1
- package/dist/agents/subagent.d.ts +3 -0
- package/dist/agents/subagent.js +12 -3
- package/dist/brain/anthropic.js +1 -0
- package/dist/brain/open-responses.js +1 -0
- package/dist/brain/openai.js +1 -0
- package/dist/brain/stream-engine.d.ts +3 -0
- package/dist/brain/stream-engine.js +2 -1
- package/dist/core/a2a.d.ts +3 -1
- package/dist/core/a2a.js +1 -1
- package/dist/core/ask-origin.d.ts +351 -154
- package/dist/core/ask-origin.js +223 -45
- package/dist/core/ask-unresolvable-notice.d.ts +5 -2
- package/dist/core/ask-unresolvable-notice.js +4 -3
- package/dist/core/auto-mode-arming.d.ts +17 -4
- package/dist/core/auto-mode-arming.js +31 -6
- package/dist/core/auto-mode-defaults.d.ts +16 -7
- package/dist/core/auto-mode-defaults.js +2 -1
- package/dist/core/auto-mode-directive.d.ts +18 -30
- package/dist/core/auto-mode-directive.js +9 -18
- package/dist/core/auto-mode-prompt.d.ts +62 -1
- package/dist/core/auto-mode-prompt.js +42 -4
- package/dist/core/auto-mode-rebuild.d.ts +2 -1
- package/dist/core/auto-mode-rebuild.js +1 -0
- package/dist/core/auto-mode.d.ts +32 -3
- package/dist/core/auto-mode.js +80 -41
- package/dist/core/checkpoint-store.d.ts +50 -35
- package/dist/core/checkpoint-store.js +58 -3
- package/dist/core/child-clamp-seats.js +1 -0
- package/dist/core/classifier-unavailable-arm.d.ts +57 -0
- package/dist/core/classifier-unavailable-arm.js +17 -0
- package/dist/core/effective-path-target.d.ts +12 -0
- package/dist/core/effective-path-target.js +18 -7
- package/dist/core/engine-notice.d.ts +53 -2
- package/dist/core/fs-write-gate-policy.d.ts +5 -1
- package/dist/core/fs-write-gate-policy.js +4 -1
- package/dist/core/gate-fold.js +17 -1
- package/dist/core/gate-lanes.js +111 -46
- package/dist/core/gate-outcome.d.ts +40 -7
- package/dist/core/gate-outcome.js +26 -4
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +12 -0
- package/dist/core/hooks.d.ts +50 -29
- package/dist/core/hooks.js +1 -3
- package/dist/core/mcp.js +1 -1
- package/dist/core/mode-policy.d.ts +182 -0
- package/dist/core/mode-policy.js +179 -0
- package/dist/core/oracle-isolation.d.ts +3 -3
- package/dist/core/permission-rule-model.d.ts +16 -3
- package/dist/core/permission-rule-model.js +36 -3
- package/dist/core/permission-rule-provider.d.ts +22 -11
- package/dist/core/permission-rule-provider.js +17 -7
- package/dist/core/permission-rules.d.ts +23 -1
- package/dist/core/permission-rules.js +35 -8
- package/dist/core/persisted-rule-arms.d.ts +10 -9
- package/dist/core/persisted-rule-arms.js +9 -6
- package/dist/core/present-plan-tool.d.ts +1 -1
- package/dist/core/present-plan-tool.js +3 -3
- package/dist/core/read-only-shell.d.ts +19 -19
- package/dist/core/read-only-shell.js +38 -12
- package/dist/core/retired-keys.d.ts +9 -3
- package/dist/core/retired-keys.js +5 -1
- package/dist/core/runner/classifier-ladder.d.ts +43 -1
- package/dist/core/runner/classifier-ladder.js +29 -0
- package/dist/core/runner/contracts.d.ts +37 -35
- package/dist/core/runner/denial-limit-arms.d.ts +30 -7
- package/dist/core/runner/denial-limit-arms.js +15 -9
- package/dist/core/runner/env-capability-census.js +1 -1
- package/dist/core/runner/gate-exit.d.ts +30 -4
- package/dist/core/runner/gate-exit.js +28 -7
- package/dist/core/runner/permission-rule-lanes.d.ts +9 -5
- package/dist/core/runner/permission-rule-lanes.js +44 -40
- package/dist/core/runner/persisted-path-rows.d.ts +64 -0
- package/dist/core/runner/persisted-path-rows.js +89 -0
- package/dist/core/runner/prepare-artifact.d.ts +5 -1
- package/dist/core/runner/prepare-artifact.js +9 -3
- package/dist/core/runner/prepare-ask-lane.d.ts +5 -3
- package/dist/core/runner/prepare-ask-lane.js +1 -1
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +39 -13
- package/dist/core/runner/prepare-caps-and-workflow.js +58 -29
- package/dist/core/runner/prepare-gate-stations.d.ts +33 -6
- package/dist/core/runner/prepare-gate-stations.js +32 -19
- package/dist/core/runner/prepare-hands-readface.d.ts +40 -17
- package/dist/core/runner/prepare-hands-readface.js +64 -53
- package/dist/core/runner/prepare-inherited-gate.d.ts +22 -22
- package/dist/core/runner/prepare-inherited-gate.js +37 -23
- package/dist/core/runner/prepare-park-ask.d.ts +2 -2
- package/dist/core/runner/prepare-park-ask.js +2 -1
- package/dist/core/runner/prepare-policy-chain.d.ts +22 -2
- package/dist/core/runner/prepare-policy-chain.js +39 -12
- package/dist/core/runner/prepare-protocol-tools.d.ts +28 -0
- package/dist/core/runner/prepare-protocol-tools.js +13 -3
- package/dist/core/runner/prepare-run-refs.d.ts +6 -6
- package/dist/core/runner/prepare-run-refs.js +9 -4
- package/dist/core/runner/prepare-safety-scan.d.ts +5 -0
- package/dist/core/runner/prepare-safety-scan.js +19 -3
- package/dist/core/runner/prepare-suspend-saga.d.ts +5 -5
- package/dist/core/runner/prepare-suspend-saga.js +6 -6
- package/dist/core/runner/prepare-task.js +51 -22
- package/dist/core/runner/prepare-wiring-manifest.d.ts +10 -6
- package/dist/core/runner/prepare-wiring-manifest.js +5 -3
- package/dist/core/runner/read-root-grant.d.ts +9 -0
- package/dist/core/runner/read-root-grant.js +12 -0
- package/dist/core/runner/retired-runner-deps.js +1 -0
- package/dist/core/runner/run-identity-wiring.js +1 -1
- package/dist/core/runner/run-leg.js +24 -15
- package/dist/core/runner/run-notification-lane.js +1 -0
- package/dist/core/runner/run-telemetry-and-budget-seats.d.ts +2 -2
- package/dist/core/runner/run-telemetry-and-budget-seats.js +1 -1
- package/dist/core/runner/run-turn-boundary.js +1 -1
- package/dist/core/runner/runtask.js +3 -0
- package/dist/core/runner/stream-mode-verb.d.ts +27 -0
- package/dist/core/runner/stream-mode-verb.js +19 -0
- package/dist/core/runner/turn-attachments.d.ts +3 -3
- package/dist/core/runner/turn-attachments.js +1 -1
- package/dist/core/runner/write-protection-station.d.ts +7 -1
- package/dist/core/runner/write-protection-station.js +6 -3
- package/dist/core/runner-deps.d.ts +31 -34
- package/dist/core/runtime-caps.d.ts +12 -3
- package/dist/core/safety-axis-vocab.d.ts +2 -2
- package/dist/core/safety-axis-vocab.js +1 -1
- package/dist/core/sensitive-path-policy.d.ts +4 -2
- package/dist/core/sensitive-path-policy.js +39 -30
- package/dist/core/task-event.d.ts +1 -1
- package/dist/core/task-result.d.ts +1 -1
- package/dist/core/task-spec.d.ts +41 -89
- package/dist/core/task-stream.d.ts +16 -0
- package/dist/core/terminal-cause.js +1 -0
- package/dist/core/tighten-task-spec.d.ts +2 -2
- package/dist/core/tighten-task-spec.js +12 -14
- package/dist/core/tool-catalog-entries.js +4 -3
- package/dist/core/tool-policy.d.ts +54 -25
- package/dist/core/tool-policy.js +43 -2
- package/dist/core/tool-registry.d.ts +2 -0
- package/dist/core/tool-registry.js +3 -0
- package/dist/core/tool-spec.d.ts +8 -9
- package/dist/core/trace.d.ts +7 -6
- package/dist/core/types.d.ts +2 -1
- package/dist/core/wiring-manifest.d.ts +31 -5
- package/dist/core/wiring-manifest.js +28 -1
- package/dist/core/write-protect.d.ts +11 -1
- package/dist/core/write-protect.js +26 -15
- package/dist/engine/execution-env/node-execution-env.d.ts +3 -0
- package/dist/engine/execution-env/node-execution-env.js +11 -1
- package/dist/engine/harness/types.d.ts +6 -7
- package/dist/engine/llm/types.d.ts +2 -4
- package/dist/engine/loop/types.d.ts +7 -0
- package/dist/index.d.ts +15 -5
- package/dist/index.js +12 -3
- package/dist/orchestration/workflow-governance.js +1 -1
- package/dist/orchestration/workflow-primitives.d.ts +1 -1
- package/dist/orchestration/workflow-size-guideline.js +2 -2
- package/dist/tools/artifact/artifact-tool.d.ts +17 -0
- package/dist/tools/artifact/artifact-tool.js +16 -1
- package/dist/tools/fs/bash-readonly-classifier.d.ts +28 -7
- package/dist/tools/fs/bash-readonly-classifier.js +88 -0
- package/dist/tools/fs/containment-face.d.ts +9 -3
- package/dist/tools/fs/containment-face.js +2 -0
- package/dist/tools/fs/file-read-check.d.ts +49 -0
- package/dist/tools/fs/file-read-check.js +71 -0
- package/dist/tools/fs/fs-bash.d.ts +3 -3
- package/dist/tools/fs/fs-read.d.ts +1 -0
- package/dist/tools/fs/fs-search-tools.d.ts +2 -2
- package/dist/tools/fs/fs-search-tools.js +4 -4
- package/dist/tools/fs/fs-shared.js +20 -6
- package/dist/tools/fs/fs-write.d.ts +4 -3
- package/dist/tools/fs/fs-write.js +9 -9
- package/dist/tools/fs/index.d.ts +39 -3
- package/dist/tools/fs/index.js +38 -15
- package/dist/tools/fs/read-deny-home.d.ts +93 -0
- package/dist/tools/fs/read-deny-home.js +225 -0
- package/dist/tools/fs/read-deny.d.ts +176 -34
- package/dist/tools/fs/read-deny.js +268 -61
- package/dist/tools/fs/safety.d.ts +6 -1
- package/dist/tools/fs/safety.js +18 -5
- package/dist/tools/fs/search.d.ts +3 -0
- package/dist/tools/fs/search.js +47 -9
- package/dist/tools/fs/shell-read-check.d.ts +42 -11
- package/dist/tools/fs/shell-read-check.js +188 -13
- package/dist/tools/fs/shell-read-establish.d.ts +7 -3
- package/dist/tools/fs/shell-read-establish.js +134 -21
- package/dist/tools/fs/shell-read-identity.d.ts +7 -0
- package/dist/tools/fs/shell-read-identity.js +21 -0
- package/dist/tools/fs/shell-read-judge.d.ts +92 -32
- package/dist/tools/fs/shell-read-judge.js +43 -18
- package/dist/tools/fs/shell-read-plan.d.ts +4 -0
- package/dist/tools/fs/shell-read-plan.js +26 -4
- package/dist/tools/fs/shell-read-write-targets.d.ts +73 -0
- package/dist/tools/fs/shell-read-write-targets.js +173 -0
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +103 -11
- package/wire-changes/core@7.34.0.json +2621 -0
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { arbitrateByMode, classifyAsk } from "../../core/ask-origin.js";
|
|
2
|
+
import { deliverEngineNotice } from "../../core/engine-notice.js";
|
|
1
3
|
import { createHash } from "node:crypto";
|
|
2
4
|
import { Type } from "typebox";
|
|
3
5
|
import { ARTIFACT_ACTION_CONCURRENCY_SAFE, ARTIFACT_COMMIT_THROW_OUTCOME, ARTIFACT_FAILURE_RESULT_CODE, ARTIFACT_HOST_ERROR_CODE, ARTIFACT_LIST_SCOPES, ARTIFACT_PLANE_CONTENT_ORIGIN, ARTIFACT_PUBLISH_CONTEXTS, } from "../../core/artifact-host.js";
|
|
@@ -128,7 +130,8 @@ function buildSchema(caps) {
|
|
|
128
130
|
const invalid = (text) => errorResult(`Error (Artifact): ${text}`, { type: "artifact", invalidArguments: true });
|
|
129
131
|
const typed = (code, text, extra = {}) => errorResult(`Error (Artifact): ${text}`, { type: "artifact", code: ARTIFACT_FAILURE_RESULT_CODE[code], ...extra });
|
|
130
132
|
export function createArtifactTool(opts) {
|
|
131
|
-
const { host, capabilities: caps, runId, taskId, sessionId, agentName, readLocalFile, onAsk } = opts;
|
|
133
|
+
const { host, capabilities: caps, runId, taskId, sessionId, agentName, readLocalFile, onAsk, permissionMode, onNotice } = opts;
|
|
134
|
+
let releaseAnnounced = false;
|
|
132
135
|
const versions = new Map();
|
|
133
136
|
const bindings = new Map();
|
|
134
137
|
const occurrences = new Map();
|
|
@@ -316,6 +319,18 @@ export function createArtifactTool(opts) {
|
|
|
316
319
|
const sharedReadConsent = (url, ref, ctx) => {
|
|
317
320
|
if (sharedReadApproved.has(url))
|
|
318
321
|
return Promise.resolve({ ok: true });
|
|
322
|
+
if (permissionMode !== undefined) {
|
|
323
|
+
const verdict = arbitrateByMode(permissionMode(), classifyAsk({ action: "ask", requiresRealApproval: true, guard: "artifact_shared_read" }, { contentQuestion: false, markedUnresolvable: false, org: undefined, tightened: undefined, mandateMarks: {} }));
|
|
324
|
+
if (verdict.kind === "released") {
|
|
325
|
+
if (!releaseAnnounced && onNotice !== undefined) {
|
|
326
|
+
releaseAnnounced = true;
|
|
327
|
+
deliverEngineNotice(onNotice, { code: "permission.ask_released_by_mode", message: `the ${verdict.mode} permission mode released a shared-artifact read (${inlineUntrusted(url, 300)}) without asking anyone; further shared reads of this run under the mode are released the same way`, detail: { mode: verdict.mode, askClass: verdict.askClass, count: 1, firstToolCallId: ctx.toolCallId, ...(sessionId !== undefined ? { sessionId } : {}), runId } });
|
|
328
|
+
}
|
|
329
|
+
return Promise.resolve({ ok: true });
|
|
330
|
+
}
|
|
331
|
+
if (verdict.kind === "denied_by_mode")
|
|
332
|
+
return Promise.resolve({ ok: false, reason: `the ${verdict.mode} permission mode refuses to read another person's artifact without approval` });
|
|
333
|
+
}
|
|
319
334
|
const pending = consentInFlight.get(url);
|
|
320
335
|
if (pending !== undefined)
|
|
321
336
|
return pending;
|
|
@@ -15,7 +15,7 @@ import { type LexedCommand, type LexedSegment, type LexedWord, type ShellLex, ty
|
|
|
15
15
|
* hostname), and `file` (`-C -m` → compiles/writes a magic file) were REMOVED — each is read-only by NAME
|
|
16
16
|
* but state-MUTATING with args, which an argv[0]-only filter cannot tell apart. Leaving them in defeated
|
|
17
17
|
* both the `effect:read` truthfulness here and the classifier's irreversibility promise (a `date -s` would
|
|
18
|
-
* auto-allow an irreversible clock jump
|
|
18
|
+
* auto-allow an irreversible clock jump on the full shell face).
|
|
19
19
|
*
|
|
20
20
|
* Rows 3-4 are the CC-anchored expansion — the members of CC's safe-command
|
|
21
21
|
* set (pretty223.js:420513-420565 `e6s`; identical in the 220 corpus; the 220→245 campaign diff records
|
|
@@ -223,7 +223,7 @@ export declare function splitShellCompoundSegments(source: string, options?: {
|
|
|
223
223
|
reject: string;
|
|
224
224
|
};
|
|
225
225
|
/**
|
|
226
|
-
* Compound read-only classification for the `bash`
|
|
226
|
+
* Compound read-only classification for the `bash` full-shell face (the shape tier).
|
|
227
227
|
* Splits a command on the connector set `;` `&&` `||` `|` and passes every segment through
|
|
228
228
|
* {@link coarseReadonlyCheck} (the single {@link parseLeadingCommandName} parser + bare-name allowlist).
|
|
229
229
|
* All segments green ⇒ read-only. Returns a reason when rejected, else undefined.
|
|
@@ -304,8 +304,11 @@ export interface BashReadonlyRootBoundary {
|
|
|
304
304
|
* EVERY read face wires this one seat, the `bash_readonly` shell included, at BOTH of its judging
|
|
305
305
|
* moments — roots answer WHERE a command may read and the allowlist WHICH verb runs, neither answers
|
|
306
306
|
* WHICH FILES are off the table. What this seat is and is not is stated once, at
|
|
307
|
-
* `shell.read_boundary.friction_floor`.
|
|
308
|
-
|
|
307
|
+
* `shell.read_boundary.friction_floor`. `walked` — the path is a member a traversal enumerated, not a
|
|
308
|
+
* spelling of the command ({@link import("./read-deny.js").ReadDenyMatcher.matchPath}). */
|
|
309
|
+
denyMatch?: (resolvedPath: string, opts?: {
|
|
310
|
+
readonly walked?: boolean;
|
|
311
|
+
}) => string | null;
|
|
309
312
|
/** What the wired {@link denyMatch} judge would apply — the ACTIVE deny patterns. Read for ONE question:
|
|
310
313
|
* is there anything for a traversal to defeat? EMPTY ⇒ the judge refuses nothing, so a walk under it
|
|
311
314
|
* bypasses nothing and {@link CompoundReadonlyVerdict.recursiveReadPaths} is not minted. ABSENT ⇒ the
|
|
@@ -328,7 +331,7 @@ export interface BashReadonlyRootBoundary {
|
|
|
328
331
|
*/
|
|
329
332
|
export interface CompoundReadonlyVerdict {
|
|
330
333
|
/** Demotion reason (the same string {@link classifyCompoundReadonly} returns), or undefined: the
|
|
331
|
-
* command's SHAPE is not one the
|
|
334
|
+
* command's SHAPE is not one the shape tier auto-allows. WHERE the command reads is not this
|
|
332
335
|
* verdict's business — that is the gate's shell read station (`shell-read-check.ts`). */
|
|
333
336
|
reason?: string;
|
|
334
337
|
}
|
|
@@ -350,6 +353,24 @@ export declare function formatOutOfRootReadApprovalOption(directory: string): st
|
|
|
350
353
|
* operand scan below, which is safe for an unknown command because a bare word can only ever name
|
|
351
354
|
* something under the working directory. */
|
|
352
355
|
export declare const NO_PATH_OPERAND_COMMANDS: ReadonlySet<string>;
|
|
356
|
+
/** The no-path-operand programs that EVALUATE a word (`test -v 'a[$(…)]'` runs the substitution in the subscript): a word
|
|
357
|
+
* the shell or that evaluation rewrites is handed over as any program's is; only their settled words name nothing. */
|
|
358
|
+
export declare const NO_PATH_OPERAND_EVALUATORS: ReadonlySet<string>;
|
|
359
|
+
/** The builtins whose `NAME=value` arguments are assignments bash performs (and exports, for `export`), never paths. */
|
|
360
|
+
export declare const DECLARATION_BUILTINS: ReadonlySet<string>;
|
|
361
|
+
/** The shell BUILTINS that open no file, start no program and evaluate nothing: an external program's loader reads its
|
|
362
|
+
* environment (`LD_PRELOAD` for `sleep`), `printf` reads `TZ` for `%(…)T` and assigns through `-v`, `test -v` and an
|
|
363
|
+
* array subscript evaluate arithmetic (which runs a `$(…)` inside it). A declaration builtin counts only bare — no option
|
|
364
|
+
* word (`-i` evaluates the value) — and every inert segment's words carry no `[` `]` `$` or backquote. */
|
|
365
|
+
export declare const INERT_BUILTINS: ReadonlySet<string>;
|
|
366
|
+
/** The indices of the SETTLED words a {@link MESSAGE_SLOTS} row reads as message text under the subcommand's grammar:
|
|
367
|
+
* the value of `-m` (attached, or the next word) or of `--message` (`=`, or the next word; an unambiguous prefix of it
|
|
368
|
+
* counts). A word the shell rewrites is never claimed, and neither is any word another option consumes — those words
|
|
369
|
+
* stay the verb-blind walk's. */
|
|
370
|
+
export declare function messageSlotWords(program: string, words: readonly {
|
|
371
|
+
readonly folded: string;
|
|
372
|
+
readonly expands: unknown;
|
|
373
|
+
}[], programIndex: number): Set<number>;
|
|
353
374
|
/** True for a token that can denote a filesystem location OUTSIDE the working directory. A bare word
|
|
354
375
|
* (`foo`, `a.txt`, a grep pattern, an awk program) cannot: with no separator and no `..` it always
|
|
355
376
|
* resolves under the cwd, which the caller's cwd governance already covers.
|
|
@@ -934,8 +955,8 @@ export declare const POLL_LOOP_MAX_BEATS = 120;
|
|
|
934
955
|
export declare const POLL_LOOP_MAX_SLEEP_SECONDS = 600;
|
|
935
956
|
/**
|
|
936
957
|
* The BOUNDED READ-ONLY POLL LOOP arm of the classify face. The compound classifier above rejects any
|
|
937
|
-
* control structure (`for` is not an allowlisted name), which makes
|
|
938
|
-
* `"always"` for a monitoring consumer whose whole idiom is `for i in $(seq 1 8); do tail -n 5 x.log;
|
|
958
|
+
* control structure (`for` is not an allowlisted name), which makes the shape tier behave like
|
|
959
|
+
* the `"always"` floor for a monitoring consumer whose whole idiom is `for i in $(seq 1 8); do tail -n 5 x.log;
|
|
939
960
|
* sleep 2; done`. This arm accepts EXACTLY that grammar and nothing else. Returns the rejection
|
|
940
961
|
* reason, or undefined when the command matches the grammar (⇒ safe to treat as reversible).
|
|
941
962
|
*
|
|
@@ -162,7 +162,95 @@ export function formatOutOfRootReadApprovalOption(directory) {
|
|
|
162
162
|
export const NO_PATH_OPERAND_COMMANDS = new Set([
|
|
163
163
|
"pwd", "echo", "printf", "whoami", "uname", "which", "tr", "basename", "dirname",
|
|
164
164
|
"cal", "uptime", "id", "groups", "nproc", "locale", "free", "true", "false", "seq", "expr", "type", "sleep",
|
|
165
|
+
"test", "[",
|
|
165
166
|
]);
|
|
167
|
+
export const NO_PATH_OPERAND_EVALUATORS = new Set(["test", "["]);
|
|
168
|
+
export const DECLARATION_BUILTINS = new Set(["export", "declare", "typeset", "local", "readonly"]);
|
|
169
|
+
export const INERT_BUILTINS = new Set(["echo", "true", "false", "pwd", ...DECLARATION_BUILTINS]);
|
|
170
|
+
const MESSAGE_SLOTS = {
|
|
171
|
+
git: {
|
|
172
|
+
globalPlain: ["-p", "--paginate", "-P", "--no-pager", "--bare", "--no-replace-objects", "--literal-pathspecs", "--glob-pathspecs", "--noglob-pathspecs", "--icase-pathspecs", "--no-optional-locks"],
|
|
173
|
+
globalValue: ["-C", "-c", "--git-dir", "--work-tree", "--namespace", "--config-env", "--super-prefix"],
|
|
174
|
+
subcommands: {
|
|
175
|
+
commit: { plain: "apsnvqeioz", optional: "Su", value: "mFCct", longValue: ["message", "file", "template", "pathspec-from-file", "author", "date", "cleanup", "fixup", "squash", "trailer", "reuse-message", "reedit-message"] },
|
|
176
|
+
tag: { plain: "asfdvle", optional: "n", value: "mFu", longValue: ["message", "file", "local-user", "cleanup", "trailer", "sort", "format", "contains", "no-contains", "points-at", "merged", "no-merged"] },
|
|
177
|
+
merge: { plain: "nqve", optional: "S", value: "mFsX", longValue: ["message", "file", "strategy", "strategy-option", "into-name", "cleanup"] },
|
|
178
|
+
stash: { plain: "pkuaqS", optional: "", value: "m", longValue: ["message", "pathspec-from-file"] },
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
export function messageSlotWords(program, words, programIndex) {
|
|
183
|
+
const out = new Set();
|
|
184
|
+
const row = MESSAGE_SLOTS[program];
|
|
185
|
+
if (row === undefined)
|
|
186
|
+
return out;
|
|
187
|
+
let j = programIndex + 1;
|
|
188
|
+
for (; j < words.length; j++) {
|
|
189
|
+
const w = words[j].folded;
|
|
190
|
+
if (!w.startsWith("-"))
|
|
191
|
+
break;
|
|
192
|
+
if (row.globalValue.includes(w))
|
|
193
|
+
j++;
|
|
194
|
+
else if (!row.globalPlain.includes(w) && !row.globalValue.some((o) => o.startsWith("--") && w.startsWith(`${o}=`)))
|
|
195
|
+
return out;
|
|
196
|
+
}
|
|
197
|
+
const sub = row.subcommands[words[j]?.folded ?? ""];
|
|
198
|
+
if (sub === undefined)
|
|
199
|
+
return out;
|
|
200
|
+
const settled = (k) => words[k] !== undefined && words[k].expands === false;
|
|
201
|
+
let pending;
|
|
202
|
+
for (let k = j + 1; k < words.length; k++) {
|
|
203
|
+
const w = words[k].folded;
|
|
204
|
+
if (pending !== undefined) {
|
|
205
|
+
if (pending === "message" && settled(k))
|
|
206
|
+
out.add(k);
|
|
207
|
+
pending = undefined;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (w === "--")
|
|
211
|
+
break;
|
|
212
|
+
if (!settled(k)) {
|
|
213
|
+
pending = "other";
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (!w.startsWith("-") || w === "-")
|
|
217
|
+
continue;
|
|
218
|
+
if (w.startsWith("--")) {
|
|
219
|
+
const eq = w.indexOf("=");
|
|
220
|
+
const name = w.slice(2, eq < 0 ? undefined : eq);
|
|
221
|
+
const takers = sub.longValue.filter((n) => n.startsWith(name));
|
|
222
|
+
const message = name.length > 0 && takers.length > 0 && takers.every((n) => n === "message") || name === "message";
|
|
223
|
+
if (eq >= 0) {
|
|
224
|
+
if (message)
|
|
225
|
+
out.add(k);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (takers.length > 0)
|
|
229
|
+
pending = message ? "message" : "other";
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
const letters = w.slice(1);
|
|
233
|
+
for (let i = 0; i < letters.length; i++) {
|
|
234
|
+
const c = letters[i];
|
|
235
|
+
if (sub.plain.includes(c))
|
|
236
|
+
continue;
|
|
237
|
+
if (sub.optional.includes(c))
|
|
238
|
+
break;
|
|
239
|
+
if (sub.value.includes(c)) {
|
|
240
|
+
if (i < letters.length - 1) {
|
|
241
|
+
if (c === "m")
|
|
242
|
+
out.add(k);
|
|
243
|
+
}
|
|
244
|
+
else
|
|
245
|
+
pending = c === "m" ? "message" : "other";
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
pending = "other";
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return out;
|
|
253
|
+
}
|
|
166
254
|
const XARGS_TARGETS_ACCEPTED_UNJUDGED = NO_PATH_OPERAND_COMMANDS;
|
|
167
255
|
export function isPathShapedToken(tok) {
|
|
168
256
|
return tok.includes("/") || tok.startsWith("~") || tok === "." || tok === "..";
|
|
@@ -18,8 +18,8 @@ export declare function containmentFaceSeatName(kind: ContainmentFaceKind, layer
|
|
|
18
18
|
* screen the seats on runs that never mount hands. The resolver itself also runs on every hands-less
|
|
19
19
|
* leg; this screen still fires first, so a garbage value refuses before any leg-shape branching. */
|
|
20
20
|
export declare function assertContainmentFaceValue(v: unknown, kind: ContainmentFaceKind, layer: ContainmentFaceLayer): ContainmentFace | undefined;
|
|
21
|
-
/** Inputs to {@link resolveContainmentFace} —
|
|
22
|
-
*
|
|
21
|
+
/** Inputs to {@link resolveContainmentFace} — structural/declaration facts; a permission mode enters only as
|
|
22
|
+
* the face its table row states ({@link modeFace}), never as a mode word. */
|
|
23
23
|
export interface ContainmentFaceInputs {
|
|
24
24
|
/** Which face is being resolved. Selects the seat spellings and the mint codes; the ORDER below is
|
|
25
25
|
* one body, identical for both. */
|
|
@@ -41,8 +41,13 @@ export interface ContainmentFaceInputs {
|
|
|
41
41
|
* tool-band assembly from spec-time facts (env kind / handsReadOnly / Bash exclusion — every
|
|
42
42
|
* exclusion source is spec-time-frozen, so this is always computable). When false, the roots fence
|
|
43
43
|
* is the ONLY boundary this face has, so no IMPLICIT default may open it; an EXPLICIT "open" on
|
|
44
|
-
* either seat still wins (row 4 of the order is a default, not a clamp). */
|
|
44
|
+
* either seat or the mode's face still wins (row 4 of the order is a default, not a clamp). */
|
|
45
45
|
fullShellReachable: boolean;
|
|
46
|
+
/** The face the leg's permission MODE states (`ModePolicy.writeFace`: `bypassPermissions` ⇒ "open"; absent ⇒
|
|
47
|
+
* the mode states nothing). Row 3′: read after the explicit seats and before the defaults — a seat's word (the
|
|
48
|
+
* deployment's ceiling, a task's tighten) outranks it, and neither the read-only mount nor org governance
|
|
49
|
+
* consults it. */
|
|
50
|
+
modeFace?: ContainmentFace;
|
|
46
51
|
/** Observation hook for the ONE silent branch of the order: row 1's deployment-seat clamp
|
|
47
52
|
* (readOnly mount + deps "open" + NO task seat). Fired exactly when the deps declaration was the
|
|
48
53
|
* deciding seat and the mount overrode it — not when a task-level "roots" pinned the same value
|
|
@@ -63,6 +68,7 @@ export interface ContainmentFaceInputs {
|
|
|
63
68
|
* 2. org-governed: the task seat's "open" refuses (task layer only tightens); deps "open" wins;
|
|
64
69
|
* otherwise ROOTS.
|
|
65
70
|
* 3. explicit seat: spec ?? deps.
|
|
71
|
+
* 3′. the mode's face ({@link ContainmentFaceInputs.modeFace}).
|
|
66
72
|
* 4. bash-less mount: ROOTS (the fence is the only boundary there — "the fence guards nothing bash
|
|
67
73
|
* reaches anyway" does not hold, so no implicit default may open it).
|
|
68
74
|
* 5. default: ROOTS — the engine never opens implicitly.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The READ station of the file tools (Read / Grep / Glob) — the read face's twin of the shell read station, one gate
|
|
3
|
+
* input shape ({@link ShellReadCheck}) so the gate, the approved-edit re-judge and the resumed leg's belt read both:
|
|
4
|
+
* before approval, where the call's declared path target lands is resolved exactly as the tool will resolve it
|
|
5
|
+
* (`resolveKey` over the same spelling, base, roots, deny table and containment face), and a target OUTSIDE the read
|
|
6
|
+
* roots is an ORDINARY question — a person answers it, a persisted `Read(//dir/**)` row or the auto-mode classifier
|
|
7
|
+
* may, a permission mode arbitrates it — with the directory whose grant clears it on the card. The deny table and
|
|
8
|
+
* every other refusal stay the tool's own: this station judges containment only, after the deny judgment.
|
|
9
|
+
*
|
|
10
|
+
* A call whose out-of-root read the gate put to a question is ADMITTED for that one target when the gate applies the
|
|
11
|
+
* verdict ({@link ShellReadVerdict.admit}): the tool reads it once ({@link FileReadStation.admitted}), on the canonical
|
|
12
|
+
* key the station judged — a target that resolves elsewhere by then is refused by the tool's own fence, and a verdict
|
|
13
|
+
* the gate never received (its wait elapsed, the judgment threw) admits nothing.
|
|
14
|
+
*/
|
|
15
|
+
import type { ExecutionEnv } from "../../internal/harness-types.js";
|
|
16
|
+
import type { ContainmentFace } from "./containment-face.js";
|
|
17
|
+
import { type ReadDenyMatcher } from "./read-deny.js";
|
|
18
|
+
import type { ShellReadCheck } from "./shell-read-check.js";
|
|
19
|
+
export interface FileReadStation {
|
|
20
|
+
readonly check: ShellReadCheck;
|
|
21
|
+
/** The station's membership: the tool names whose declared read target it judges. */
|
|
22
|
+
readonly tools: ReadonlySet<string>;
|
|
23
|
+
/** Did the gate put THIS call's read of `canonicalKey` outside the roots to a question it then let through? Consumed
|
|
24
|
+
* on a match: the admission covers one read of one target. */
|
|
25
|
+
readonly admitted: (toolCallId: string | undefined, canonicalKey: string) => boolean;
|
|
26
|
+
}
|
|
27
|
+
export interface FileReadStationOptions {
|
|
28
|
+
readonly env: ExecutionEnv;
|
|
29
|
+
readonly rootCanonical: string;
|
|
30
|
+
/** The extra read roots, read per call (a grant widens the live set). */
|
|
31
|
+
readonly roots: () => readonly string[];
|
|
32
|
+
readonly readDeny?: ReadDenyMatcher;
|
|
33
|
+
readonly readFace?: ContainmentFace;
|
|
34
|
+
readonly homeDir?: string;
|
|
35
|
+
/** The file tools this leg mounts (their catalog path target is the one judged). */
|
|
36
|
+
readonly tools: ReadonlySet<string>;
|
|
37
|
+
/** A target no answer may admit (the store-isolation rule's judgment, `rootAdmittingArtifactStore`): it is left to
|
|
38
|
+
* the tool's own refusal, no question is raised for it. */
|
|
39
|
+
readonly excluded?: (canonicalKey: string) => boolean;
|
|
40
|
+
/** A target the tools' own fence admits without the roots (the engine's background output file,
|
|
41
|
+
* `backgroundOutputReadExemption`): no question is put for it. */
|
|
42
|
+
readonly exempt?: (canonicalKey: string) => boolean;
|
|
43
|
+
}
|
|
44
|
+
export declare function createFileReadStation(opts: FileReadStationOptions): FileReadStation;
|
|
45
|
+
/** One gate input over the leg's read stations: each call is judged by the station whose membership holds it. */
|
|
46
|
+
export declare function composeReadChecks(stations: ReadonlyArray<{
|
|
47
|
+
readonly check: ShellReadCheck;
|
|
48
|
+
readonly tools: ReadonlySet<string>;
|
|
49
|
+
}>): ShellReadCheck | undefined;
|
|
@@ -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);
|