@sema-agent/core 7.8.0 → 7.9.1
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 +75 -1
- package/dist/agents/subagent.d.ts +1 -1
- package/dist/core/ask-origin.d.ts +16 -1
- package/dist/core/ask-origin.js +5 -1
- package/dist/core/checkpoint-store.d.ts +36 -35
- package/dist/core/effective-path-target.d.ts +43 -0
- package/dist/core/effective-path-target.js +56 -0
- package/dist/core/engine-notice.d.ts +8 -0
- package/dist/core/fs-write-gate-policy.js +2 -1
- package/dist/core/gate-lanes.d.ts +1 -0
- package/dist/core/gate-lanes.js +60 -28
- package/dist/core/gate-outcome.d.ts +8 -4
- package/dist/core/gate-outcome.js +2 -1
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +2 -0
- package/dist/core/hooks.d.ts +66 -32
- package/dist/core/mcp-injection-drop.d.ts +74 -0
- package/dist/core/mcp-injection-drop.js +27 -0
- package/dist/core/permission-rule-consent.d.ts +29 -36
- package/dist/core/permission-rule-consent.js +110 -60
- package/dist/core/permission-rule-model.d.ts +311 -96
- package/dist/core/permission-rule-model.js +309 -50
- package/dist/core/permission-rule-org.d.ts +19 -12
- package/dist/core/permission-rule-org.js +13 -16
- package/dist/core/permission-rule-provider.d.ts +10 -6
- package/dist/core/permission-rule-provider.js +12 -8
- package/dist/core/permission-rule-session.d.ts +7 -6
- package/dist/core/permission-rule-session.js +29 -10
- package/dist/core/permission-rule-store.d.ts +40 -18
- package/dist/core/permission-rule-store.js +68 -42
- package/dist/core/permission-rule-sync.d.ts +9 -3
- package/dist/core/permission-rule-sync.js +29 -25
- package/dist/core/permission-rule-syntax.d.ts +30 -0
- package/dist/core/permission-rule-syntax.js +44 -0
- package/dist/core/permission-rules.d.ts +56 -33
- package/dist/core/permission-rules.js +88 -55
- package/dist/core/persisted-rule-arms.d.ts +61 -0
- package/dist/core/persisted-rule-arms.js +50 -0
- package/dist/core/runner/active-skill-scope.js +2 -1
- package/dist/core/runner/contracts.d.ts +21 -2
- package/dist/core/runner/permission-rule-lanes.d.ts +42 -27
- package/dist/core/runner/permission-rule-lanes.js +25 -21
- package/dist/core/runner/prepare-gate-stations.js +1 -1
- package/dist/core/runner/prepare-safety-scan.js +8 -2
- package/dist/core/runner/prepare-task.js +1 -1
- package/dist/core/runner/resume-admission.d.ts +53 -0
- package/dist/core/runner/resume-admission.js +83 -0
- package/dist/core/runner/resume-apply.d.ts +50 -0
- package/dist/core/runner/resume-apply.js +184 -0
- package/dist/core/runner/resume-checkpoint-screen.d.ts +18 -0
- package/dist/core/runner/resume-checkpoint-screen.js +108 -0
- package/dist/core/runner/resume-claim.d.ts +32 -0
- package/dist/core/runner/resume-claim.js +27 -0
- package/dist/core/runner/resume-internals-and-config.d.ts +33 -0
- package/dist/core/runner/resume-internals-and-config.js +50 -0
- package/dist/core/runner/resume-policy-outcome.d.ts +31 -0
- package/dist/core/runner/resume-policy-outcome.js +127 -0
- package/dist/core/runner/resume-preflight.d.ts +40 -0
- package/dist/core/runner/resume-preflight.js +122 -0
- package/dist/core/runner/resume-review-outcome.d.ts +30 -0
- package/dist/core/runner/resume-review-outcome.js +88 -0
- package/dist/core/runner/run-harness-handlers.js +1 -1
- package/dist/core/runner/runtask.d.ts +20 -0
- package/dist/core/runner/runtask.js +85 -734
- package/dist/core/runner/session-rule-policy.js +2 -1
- package/dist/core/runner/tool-end-body.d.ts +12 -5
- package/dist/core/runner/tool-end-body.js +5 -5
- package/dist/core/runner/tool-face-overlay.js +22 -3
- package/dist/core/runner/tool-output-projection.d.ts +9 -6
- package/dist/core/runner/tool-output-projection.js +1 -18
- package/dist/core/runner/turn-attachments.d.ts +2 -2
- package/dist/core/sensitive-path-policy.js +5 -3
- package/dist/core/shell-lexer.d.ts +47 -0
- package/dist/core/shell-lexer.js +478 -0
- package/dist/core/shell-scan.d.ts +60 -0
- package/dist/core/shell-scan.js +183 -0
- package/dist/core/shell-wrapper-table.d.ts +297 -0
- package/dist/core/shell-wrapper-table.js +58 -0
- package/dist/core/store-contracts/permission-rule-sync-contract.js +29 -10
- package/dist/core/tool-catalog-entries.js +6 -6
- package/dist/core/tool-face.d.ts +80 -4
- package/dist/core/tool-face.js +10 -0
- package/dist/core/tool-policy.d.ts +1 -6
- package/dist/core/tool-registry.d.ts +8 -11
- package/dist/core/tool-registry.js +5 -2
- package/dist/core/tool-roster.d.ts +20 -2
- package/dist/core/tool-roster.js +36 -8
- package/dist/index.d.ts +7 -4
- package/dist/index.js +6 -3
- package/dist/stores/file/checkpoint-store.js +1 -1
- package/dist/stores/file/permission-rule-store.d.ts +28 -23
- package/dist/stores/file/permission-rule-store.js +64 -16
- package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -1
- package/dist/tools/fs/fs-pdf.d.ts +1 -1
- package/dist/tools/fs/fs-search-tools.d.ts +3 -2
- package/dist/tools/fs/fs-search-tools.js +17 -9
- package/dist/tools/fs/index.js +1 -1
- package/dist/tools/fs/notebook.d.ts +1 -1
- package/dist/tools/fs/safety.d.ts +1 -1
- package/dist/tools/fs/search.d.ts +0 -8
- package/dist/tools/fs/search.js +0 -23
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +99 -11
|
@@ -2,6 +2,7 @@ import { canonicalizeTarget, writeTargetPath } from "../../tools/fs/safety.js";
|
|
|
2
2
|
import { isWinFormPath } from "../../tools/fs/safety.js";
|
|
3
3
|
import { createCoarseCommandNamePolicy, namespacedCoveringEntries, namespacedCoveringHit } from "../tool-policy.js";
|
|
4
4
|
import { declaredPathTargetOf, isProtectedWrite, pathConfinableWriteToolNames, skillScopeWriteToolNames } from "../tool-registry.js";
|
|
5
|
+
import { pathTargetBaseOf } from "../effective-path-target.js";
|
|
5
6
|
export const PATH_WRITE_TOOLS = skillScopeWriteToolNames();
|
|
6
7
|
export const PATH_CONFINABLE_WRITE_TOOLS = pathConfinableWriteToolNames();
|
|
7
8
|
export function isWithin(root, p) {
|
|
@@ -65,7 +66,7 @@ export function createSessionRulePolicy(rules, opts) {
|
|
|
65
66
|
if (typeof path !== "string" || path.length === 0) {
|
|
66
67
|
return deny(`write tool "${req.toolName}" denied: session rule confines writes to allowDirs but the call has no resolvable path`);
|
|
67
68
|
}
|
|
68
|
-
const canon = await canonicalizeTarget(env, path, signal, req.cwd
|
|
69
|
+
const canon = await canonicalizeTarget(env, path, signal, pathTargetBaseOf(declared, { root: rootPath, cwd: req.cwd }));
|
|
69
70
|
if (!canon.ok) {
|
|
70
71
|
return deny(`write to "${path}" denied: its real target could not be resolved against the session-rule allowDirs`);
|
|
71
72
|
}
|
|
@@ -7,24 +7,31 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Projections over values the caller already holds, with ONE clock read: `toolResultMsg` stamps the message
|
|
9
9
|
* it mints with `Date.now()` (as it did inside the driver — the transcript's timestamp is minted where the
|
|
10
|
-
* message is).
|
|
10
|
+
* message is). The one callback here is the leg's card projector, handed in by every caller (it keeps the
|
|
11
|
+
* once-per-leg announce state; this module keeps none). Nothing here awaits.
|
|
11
12
|
* Layer-1 machinery, not a driven lane; the prefix rule it follows is written once, on initial-run-state.ts.
|
|
12
13
|
*/
|
|
13
14
|
import { type AgentMessage } from "../../internal/harness.js";
|
|
14
15
|
import type { GateOutcome } from "../gate-outcome.js";
|
|
15
16
|
import { type McpDelivered } from "../mcp-failure.js";
|
|
16
17
|
import type { RecoveredOrphan } from "../session-reconcile.js";
|
|
18
|
+
import { type StructuredProjector } from "./tool-output-projection.js";
|
|
17
19
|
import type { WriteFamily } from "./turn-attachments.js";
|
|
18
20
|
/** design/134 §3.1b — derive the thin `response` digest from a harness tool result: model-facing text
|
|
19
21
|
* content only (never `details` — the H4 thin-projection discipline), capped with a truncation note. */
|
|
20
22
|
export declare function batchResponseDigest(result: unknown): string | undefined;
|
|
21
23
|
/** The `tool_end` body fields projected from a harness tool result — output/truncated/totalChars via
|
|
22
|
-
* {@link toolOutputFrom} and the CC card via
|
|
24
|
+
* {@link toolOutputFrom} and the CC card via the LEG's projector (`Prepared.structuredProjector`: the engine card set
|
|
25
|
+
* ∪ the caller rows' declared `renderHints.resultCards`, an undeclared type dropped and announced once per leg —
|
|
26
|
+
* design/388 L4; handed in because the once-per-leg state lives on the leg). Single construction point for BOTH
|
|
23
27
|
* the live loop's frames and the resumed batch's frames (`resolvePendingCall` + the deferred-sibling
|
|
24
28
|
* close): the resumed frames used to carry only `isError`, so a client rendering tool output from frames
|
|
25
29
|
* showed an empty body for every durable-approved call. Same projection = same source as the transcript.
|
|
26
30
|
* Also the one place the gate outcome reaches a frame — see the parameter. */
|
|
27
31
|
export declare function toolEndBodyFrom(result: unknown, isError: boolean,
|
|
32
|
+
/** The leg's card projector — see the doc above. A parameter, never a module default: a body minted with the engine
|
|
33
|
+
* set alone would silently drop every declared caller card and never announce. */
|
|
34
|
+
structured: StructuredProjector,
|
|
28
35
|
/** The gate's record of the pass that admitted or refused this call, supplied by the CALLER of this
|
|
29
36
|
* projection — the live loop reads it off the gate's per-call sideband, the resumed leg off the decide's
|
|
30
37
|
* minted record. Deliberately a parameter and never derived from `result`: a tool's own `details`
|
|
@@ -58,10 +65,10 @@ gatedCallIdOfRun?: string): {
|
|
|
58
65
|
* disagreed. The projection goes through the same {@link toolEndBodyFrom} every live tool result uses, so
|
|
59
66
|
* `output` = the persisted model-facing text and `errorCode` = the persisted `details.errorKind`
|
|
60
67
|
* (`interrupted_never_started` / `interrupted_outcome_unknown`) — a consumer discriminates on the code
|
|
61
|
-
* instead of prose-matching. No `structured`: the reconcile mints no CC card (no `details.type`), which
|
|
62
|
-
*
|
|
68
|
+
* instead of prose-matching. No `structured`: the reconcile mints no CC card (no `details.type`), which the
|
|
69
|
+
* leg's projector already enforces.
|
|
63
70
|
*/
|
|
64
|
-
export declare function reconciledToolEndBody(orphan: Pick<RecoveredOrphan, "text" | "errorKind"
|
|
71
|
+
export declare function reconciledToolEndBody(orphan: Pick<RecoveredOrphan, "text" | "errorKind">, structured: StructuredProjector): ReturnType<typeof toolEndBodyFrom>;
|
|
65
72
|
/** task #51 F2/F3 — the CANONICAL write-tool → reminder-window mapping (CC qFm :480253-480254
|
|
66
73
|
* `a.name === bD || a.name === hF` = TaskCreate/TaskUpdate; BFm :480217-480219 = TodoWrite). Live
|
|
67
74
|
* deployment-alias resolution happens at the call sites (run-local `writeFamilyOf` over the mounted
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {} from "../../internal/harness.js";
|
|
2
2
|
import { MCP_DELIVERY_VERDICTS } from "../mcp-failure.js";
|
|
3
3
|
import { TOOL_SEARCH_NAME } from "./tool-disclosure.js";
|
|
4
|
-
import {
|
|
4
|
+
import { toolOutputFrom } from "./tool-output-projection.js";
|
|
5
5
|
import { TASK_CREATE_TOOL_NAME, TASK_UPDATE_TOOL_NAME, TODO_WRITE_TOOL_NAME } from "../tool-catalog-entries.js";
|
|
6
6
|
const BATCH_RESPONSE_MAX_CHARS = 500;
|
|
7
7
|
export function batchResponseDigest(result) {
|
|
@@ -32,9 +32,9 @@ export function batchResponseDigest(result) {
|
|
|
32
32
|
? `${text.slice(0, BATCH_RESPONSE_MAX_CHARS)}…[+${text.length - BATCH_RESPONSE_MAX_CHARS} chars truncated]`
|
|
33
33
|
: text;
|
|
34
34
|
}
|
|
35
|
-
export function toolEndBodyFrom(result, isError, gate, gatedCallIdOfRun) {
|
|
35
|
+
export function toolEndBodyFrom(result, isError, structured, gate, gatedCallIdOfRun) {
|
|
36
36
|
const o = toolOutputFrom(result);
|
|
37
|
-
const st =
|
|
37
|
+
const st = structured(result);
|
|
38
38
|
const det = isError ? result?.details : undefined;
|
|
39
39
|
const codeRaw = det?.code;
|
|
40
40
|
const kindRaw = det?.errorKind;
|
|
@@ -51,8 +51,8 @@ export function toolEndBodyFrom(result, isError, gate, gatedCallIdOfRun) {
|
|
|
51
51
|
...(gate !== undefined ? { gate } : {}),
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
|
-
export function reconciledToolEndBody(orphan) {
|
|
55
|
-
return toolEndBodyFrom({ content: orphan.text, details: { code: orphan.errorKind } }, true);
|
|
54
|
+
export function reconciledToolEndBody(orphan, structured) {
|
|
55
|
+
return toolEndBodyFrom({ content: orphan.text, details: { code: orphan.errorKind } }, true, structured);
|
|
56
56
|
}
|
|
57
57
|
export function writeFamilyOfCanonical(name) {
|
|
58
58
|
if (name === TASK_CREATE_TOOL_NAME || name === TASK_UPDATE_TOOL_NAME)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { MCP_NAMESPACE } from "../protocol-table.js";
|
|
2
2
|
import { mintNamespacePrefix } from "../protocol-naming.js";
|
|
3
|
-
import { TOOL_APPROVAL_CARDS, TOOL_FAMILIES, RENDER_HINT_ACTIVITY_MAX_CHARS, RENDER_HINT_MAX_CHARS, RENDER_HINT_MAX_LIST, TOOL_KEY_MAX_CHARS } from "../tool-face.js";
|
|
3
|
+
import { TOOL_APPROVAL_CARDS, TOOL_FAMILIES, TOOL_PATH_ABSENCES, TOOL_PATH_ACCESSES, TOOL_PATH_BASES, RENDER_HINT_ACTIVITY_MAX_CHARS, RENDER_HINT_MAX_CHARS, RENDER_HINT_MAX_LIST, TOOL_KEY_MAX_CHARS, } from "../tool-face.js";
|
|
4
4
|
import { inputKeysOf } from "../tool-roster.js";
|
|
5
5
|
import { deliverEngineNotice } from "../engine-notice.js";
|
|
6
6
|
export const APPROVAL_CARD_REQUIRED_KEYS = Object.freeze({
|
|
@@ -26,9 +26,28 @@ export function toolFaceProblem(face, schema) {
|
|
|
26
26
|
if (face.pathTarget !== undefined) {
|
|
27
27
|
if (typeof face.pathTarget.param !== "string" || !top.has(face.pathTarget.param))
|
|
28
28
|
return `pathTarget.param ${JSON.stringify(face.pathTarget.param)} is not a top-level property of the tool's schema`;
|
|
29
|
-
if (!
|
|
29
|
+
if (!TOOL_PATH_ACCESSES.includes(face.pathTarget.access))
|
|
30
30
|
return `pathTarget.access ${JSON.stringify(face.pathTarget.access)} is outside the closed set`;
|
|
31
|
-
const
|
|
31
|
+
const base = face.pathTarget.base;
|
|
32
|
+
if (base !== undefined && !TOOL_PATH_BASES.includes(base))
|
|
33
|
+
return `pathTarget.base ${JSON.stringify(base)} is outside the closed set`;
|
|
34
|
+
if (base === "root" && face.pathTarget.access !== "read")
|
|
35
|
+
return `pathTarget.base "root" needs pathTarget.access "read" — two write guards (the session-transcript directory guard and the frozen-spec deny) resolve a write target against the call's working directory only, so a write face declaring a root base would be judged on two different files (#638)`;
|
|
36
|
+
const absent = face.pathTarget.absent;
|
|
37
|
+
if (absent !== undefined && !TOOL_PATH_ABSENCES.includes(absent))
|
|
38
|
+
return `pathTarget.absent ${JSON.stringify(absent)} is outside the closed set`;
|
|
39
|
+
if (absent === "base" && face.pathTarget.access !== "read")
|
|
40
|
+
return `pathTarget.absent "base" needs pathTarget.access "read" — it says the call is about the whole base directory when the slot is empty, which is a search SCOPE; a write with no path names no file, and the containment fences judge a write on its own target`;
|
|
41
|
+
const patternParam = face.pathTarget.patternParam;
|
|
42
|
+
if (patternParam !== undefined) {
|
|
43
|
+
if (typeof patternParam !== "string" || !top.has(patternParam))
|
|
44
|
+
return `pathTarget.patternParam ${JSON.stringify(patternParam)} is not a top-level property of the tool's schema`;
|
|
45
|
+
if (absent !== "base")
|
|
46
|
+
return `pathTarget.patternParam ${JSON.stringify(patternParam)} is declared but pathTarget.absent is ${JSON.stringify(absent ?? "none")} — the pattern base is only ever read for an absent slot, so this declaration would be inert`;
|
|
47
|
+
}
|
|
48
|
+
const long = overlong("pathTarget.param", [face.pathTarget.param]) ??
|
|
49
|
+
overlong("pathTarget.aliases", face.pathTarget.aliases ?? []) ??
|
|
50
|
+
overlong("pathTarget.patternParam", patternParam === undefined ? [] : [patternParam]);
|
|
32
51
|
if (long !== undefined)
|
|
33
52
|
return long;
|
|
34
53
|
vocabulary.add(face.pathTarget.param);
|
|
@@ -9,14 +9,17 @@ export declare const toolOutputFrom: (result: unknown) => {
|
|
|
9
9
|
truncated: boolean;
|
|
10
10
|
totalChars?: number;
|
|
11
11
|
} | undefined;
|
|
12
|
+
/** The `tool_end.structured` projection over a harness tool result: the CC card when `details.type` names a card
|
|
13
|
+
* the leg admits, else undefined. Minted per leg by {@link createStructuredProjector}; every body projection takes it
|
|
14
|
+
* as a parameter — there is no module-level card set to fall back to. */
|
|
15
|
+
export type StructuredProjector = (result: unknown) => unknown;
|
|
12
16
|
/**
|
|
13
17
|
* L4's run-time half (design/388 §2.4): a per-leg projector — the engine card set plus the leg's declared
|
|
14
|
-
* caller cards — that DROPS an undeclared `details.type`
|
|
15
|
-
*
|
|
16
|
-
*
|
|
18
|
+
* caller cards — that DROPS an undeclared `details.type` and announces it ONCE per leg
|
|
19
|
+
* (`config.tool_card_undeclared`, audience operator). With no options it is the engine set alone, unannounced —
|
|
20
|
+
* the shape a caller outside a leg (a test, a fixture) uses.
|
|
17
21
|
*/
|
|
18
|
-
export declare function createStructuredProjector(opts
|
|
22
|
+
export declare function createStructuredProjector(opts?: {
|
|
19
23
|
declaredCards?: Iterable<string>;
|
|
20
24
|
onUndeclared?: (type: string) => void;
|
|
21
|
-
}):
|
|
22
|
-
export declare const structuredFrom: (result: unknown) => unknown;
|
|
25
|
+
}): StructuredProjector;
|
|
@@ -58,7 +58,7 @@ export const toolOutputFrom = (result) => {
|
|
|
58
58
|
return { output: raw, truncated: true, totalChars };
|
|
59
59
|
};
|
|
60
60
|
const CC_DETAIL_TYPES = engineCardTypes();
|
|
61
|
-
export function createStructuredProjector(opts) {
|
|
61
|
+
export function createStructuredProjector(opts = {}) {
|
|
62
62
|
const allowed = new Set([...CC_DETAIL_TYPES, ...(opts.declaredCards ?? [])]);
|
|
63
63
|
const announced = new Set();
|
|
64
64
|
return (result) => {
|
|
@@ -89,20 +89,3 @@ function boundedCard(details) {
|
|
|
89
89
|
}
|
|
90
90
|
return details;
|
|
91
91
|
}
|
|
92
|
-
export const structuredFrom = (result) => {
|
|
93
|
-
const details = result !== null && typeof result === "object" ? result.details : undefined;
|
|
94
|
-
if (details === null || typeof details !== "object")
|
|
95
|
-
return undefined;
|
|
96
|
-
const t = details.type;
|
|
97
|
-
if (typeof t !== "string" || !CC_DETAIL_TYPES.has(t))
|
|
98
|
-
return undefined;
|
|
99
|
-
try {
|
|
100
|
-
const serialized = JSON.stringify(details) ?? "";
|
|
101
|
-
if (serialized.length > MAX_TOOL_END_OUTPUT_CHARS * 4)
|
|
102
|
-
return undefined;
|
|
103
|
-
}
|
|
104
|
-
catch {
|
|
105
|
-
return undefined;
|
|
106
|
-
}
|
|
107
|
-
return details;
|
|
108
|
-
};
|
|
@@ -110,7 +110,7 @@ export interface TurnAttachment {
|
|
|
110
110
|
source: AttachmentSource;
|
|
111
111
|
body: string;
|
|
112
112
|
}
|
|
113
|
-
/** H4 — the thin projection of a todo list kept run-local (NEVER the wire-capped
|
|
113
|
+
/** H4 — the thin projection of a todo list kept run-local (NEVER the wire-capped `tool_end.structured` card).
|
|
114
114
|
* Pure CONTENT memory: the write-window anchor lives on {@link AttachmentState.todoLastWriteTurn}
|
|
115
115
|
* (task #51 F3 — the window resets on tool-call INITIATION, which may precede/outlive any ingest). */
|
|
116
116
|
export interface ListProjection {
|
|
@@ -273,7 +273,7 @@ export declare const INSTRUCTIONS_CHANGE_BYTE_CAP = 512;
|
|
|
273
273
|
*/
|
|
274
274
|
export declare function createAttachmentState(): AttachmentState;
|
|
275
275
|
/**
|
|
276
|
-
* H4 — thin-projection reducer over a tool's RAW `result.details` (NOT the wire
|
|
276
|
+
* H4 — thin-projection reducer over a tool's RAW `result.details` (NOT the wire card projector
|
|
277
277
|
* projection and its 4×16K caps: a huge todo list must shrink into a bounded snapshot here, never
|
|
278
278
|
* disarm the reminder by being dropped wholesale). Only the first-party discriminants `todo` /
|
|
279
279
|
* `task` / `task-list` are recognized — every other card type (subagent `agent`/`fork`/`task-output`,
|
|
@@ -2,7 +2,8 @@ import { homedir } from "node:os";
|
|
|
2
2
|
import { isAbsolute, join, resolve } from "node:path";
|
|
3
3
|
import { canonicalizeTarget, expandHomeTilde, isAbsolutePathForm, isWinFormPath, writeTargetPath } from "../tools/fs/safety.js";
|
|
4
4
|
import { compileSegmentPattern, matchSegmentPatterns } from "../tools/fs/read-deny.js";
|
|
5
|
-
import { isProtectedWrite } from "./tool-registry.js";
|
|
5
|
+
import { isProtectedWrite, protectivePathTargetOf } from "./tool-registry.js";
|
|
6
|
+
import { pathTargetBaseOf } from "./effective-path-target.js";
|
|
6
7
|
export const RECOMMENDED_SENSITIVE_PATTERNS = [
|
|
7
8
|
".env",
|
|
8
9
|
".env.*",
|
|
@@ -97,10 +98,11 @@ export function createSensitivePathPolicy(opts) {
|
|
|
97
98
|
return { action: "allow" };
|
|
98
99
|
if (guardedByName !== undefined ? !guardedByName.has(req.toolName) : !isProtectedWrite(req))
|
|
99
100
|
return { action: "allow" };
|
|
100
|
-
const
|
|
101
|
+
const protective = protectivePathTargetOf(req);
|
|
102
|
+
const path = writeTargetPath(req, protective);
|
|
101
103
|
if (typeof path !== "string" || path.length === 0)
|
|
102
104
|
return { action: "allow" };
|
|
103
|
-
const canon = await canonicalizeTarget(opts.env, path, signal,
|
|
105
|
+
const canon = await canonicalizeTarget(opts.env, path, signal, pathTargetBaseOf(protective, { root: opts.rootPath, cwd: req.cwd }));
|
|
104
106
|
if (!canon.ok) {
|
|
105
107
|
if (canon.unresolvedSymlink) {
|
|
106
108
|
return {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** One argv word as the lexer read it. */
|
|
2
|
+
export interface ShellWord {
|
|
3
|
+
/** The word's value with quotes and escapes removed — meaningful only when `expands` is `false`. */
|
|
4
|
+
readonly text: string;
|
|
5
|
+
/** The unquoted source spelling (assignment and wrapper-flag tests read this, never `text`). */
|
|
6
|
+
readonly raw: string;
|
|
7
|
+
/** `false`: a literal word. `"one"`: carries an expansion but yields exactly ONE word (it was double-quoted).
|
|
8
|
+
* `"many"`: an unquoted expansion, glob or brace pattern, or a quoted `"$@"` / array expansion — may
|
|
9
|
+
* yield zero, one or several words. */
|
|
10
|
+
readonly expands: false | "one" | "many";
|
|
11
|
+
}
|
|
12
|
+
/** One program run the lexer found, after keyword stripping. */
|
|
13
|
+
export interface ShellSegment {
|
|
14
|
+
/** The run as SPELLED (a leading keyword removed): the first candidate a rule is compared against. */
|
|
15
|
+
readonly argv: readonly ShellWord[];
|
|
16
|
+
/** The deeper candidates, one per peeled layer (leading assignments, then each wrapper of the table):
|
|
17
|
+
* `sudo -u root rm -r x` ⇒ `[[rm, -r, x]]`. Empty when nothing peeled. */
|
|
18
|
+
readonly peeled: readonly (readonly ShellWord[])[];
|
|
19
|
+
/** Present when this segment's program runs cannot be known from the text at all; `argv` then holds
|
|
20
|
+
* whatever was read before the failure and must not be matched as a run. */
|
|
21
|
+
readonly unreadable?: string;
|
|
22
|
+
/** Present when the segment's program runs are NOT fully known although its candidates could be read:
|
|
23
|
+
* a peel that stopped before the wrapped program could be named (its options carry an expansion, a
|
|
24
|
+
* command string, an option of unknown arity), or a command/process substitution anywhere in the
|
|
25
|
+
* segment (it runs a program the lexer does not read). The candidates stand — a rule they reach is
|
|
26
|
+
* reached — and every other rule reads `unreadable`. */
|
|
27
|
+
readonly peelUnreadable?: string;
|
|
28
|
+
}
|
|
29
|
+
/** A command line as the tightening reader sees it. */
|
|
30
|
+
export interface ShellCommandShape {
|
|
31
|
+
readonly segments: readonly ShellSegment[];
|
|
32
|
+
/** The connector BETWEEN each consecutive pair of segments (`;` `&&` `||` `|` `|&` `&` `\n`). */
|
|
33
|
+
readonly connectors: readonly string[];
|
|
34
|
+
/** A subshell, group, control-structure keyword or dropped empty piece bounded the segments: the
|
|
35
|
+
* connector list is not one flat chain a compound rule could spell. */
|
|
36
|
+
readonly grouped: boolean;
|
|
37
|
+
}
|
|
38
|
+
export { SHELL_WRAPPER_TABLE, type ShellWrapperName } from "./shell-wrapper-table.js";
|
|
39
|
+
/** Longer than this and the command is not read at all (one unreadable segment): every pass is linear,
|
|
40
|
+
* but a substitution nested inside quoting re-enters the quote reader once per nesting level, and the
|
|
41
|
+
* cap is what bounds that on model-supplied text. Upstream's own reader stops at the same size. */
|
|
42
|
+
export declare const MAX_SHELL_READ_CHARS = 10000;
|
|
43
|
+
/** Read `source` as the tightening reader must: every program run the text could perform, or why it cannot be read. */
|
|
44
|
+
export declare function readShellCommand(source: string): ShellCommandShape;
|
|
45
|
+
/** Every program run of `shape` can be NAMED from the text: no segment is unreadable, no peel stopped,
|
|
46
|
+
* and every run candidate's program word is literal. The offer/mint side's "judgeable" question. */
|
|
47
|
+
export declare function isFullyReadable(shape: ShellCommandShape): boolean;
|