@sema-agent/core 7.43.0 → 7.45.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 +245 -0
- package/dist/agents/subagent.d.ts +2 -1
- package/dist/agents/subagent.js +33 -14
- package/dist/agents/tool-filter.d.ts +65 -4
- package/dist/agents/tool-filter.js +127 -4
- package/dist/brain/errors.d.ts +23 -2
- package/dist/brain/errors.js +26 -2
- package/dist/brain/stream-engine.d.ts +2 -1
- package/dist/brain/stream-engine.js +10 -9
- package/dist/core/agent-definition.d.ts +2 -2
- package/dist/core/allow-entry.d.ts +25 -1
- package/dist/core/allow-entry.js +29 -4
- package/dist/core/ask-origin.d.ts +23 -4
- package/dist/core/ask-origin.js +10 -4
- package/dist/core/checkpoint-store.d.ts +28 -15
- package/dist/core/checkpoint-store.js +4 -13
- package/dist/core/decision-receipt.js +1 -1
- package/dist/core/effective-path-target.d.ts +7 -6
- package/dist/core/effective-path-target.js +3 -7
- package/dist/core/engine-notice.d.ts +33 -14
- package/dist/core/engine-notice.js +14 -7
- package/dist/core/gate-lanes.js +2 -0
- package/dist/core/gate-outcome.d.ts +5 -0
- package/dist/core/gate-outcome.js +2 -0
- package/dist/core/governance-codes.d.ts +4 -3
- package/dist/core/governance-codes.js +1 -0
- package/dist/core/hooks.d.ts +2 -0
- package/dist/core/memory-engine/content-origin.d.ts +0 -34
- package/dist/core/memory-engine/content-origin.js +9 -4
- package/dist/core/memory-engine/engine.d.ts +2 -2
- package/dist/core/memory-engine/engine.js +7 -14
- package/dist/core/memory-engine/file-backend.d.ts +2 -2
- package/dist/core/memory-engine/file-backend.js +5 -7
- package/dist/core/memory-engine/layout.d.ts +41 -4
- package/dist/core/memory-engine/layout.js +35 -5
- package/dist/core/permission-rules.d.ts +4 -0
- package/dist/core/permission-rules.js +5 -2
- package/dist/core/real-approval.d.ts +9 -6
- package/dist/core/real-approval.js +5 -5
- package/dist/core/runner/child-tree.d.ts +13 -0
- package/dist/core/runner/child-tree.js +3 -0
- package/dist/core/runner/contracts.d.ts +9 -18
- package/dist/core/runner/prepare-ask-lane.d.ts +2 -0
- package/dist/core/runner/prepare-ask-lane.js +2 -2
- package/dist/core/runner/prepare-config-doors.js +3 -1
- package/dist/core/runner/prepare-execution-env.js +2 -1
- package/dist/core/runner/prepare-gate-stations.d.ts +3 -0
- package/dist/core/runner/prepare-gate-stations.js +2 -1
- package/dist/core/runner/prepare-listings.d.ts +4 -4
- package/dist/core/runner/prepare-listings.js +2 -22
- package/dist/core/runner/prepare-park-ask.d.ts +2 -0
- package/dist/core/runner/prepare-park-ask.js +2 -2
- package/dist/core/runner/prepare-policy-chain.d.ts +2 -0
- package/dist/core/runner/prepare-policy-chain.js +4 -2
- package/dist/core/runner/prepare-task.js +10 -6
- package/dist/core/runner/resume-policy-outcome.js +2 -1
- package/dist/core/runner/run-leg.js +1 -1
- package/dist/core/runner/run-notification-lane.js +2 -2
- package/dist/core/runner/runtask.d.ts +2 -0
- package/dist/core/runner/runtask.js +3 -5
- package/dist/core/runner/steer-admission.d.ts +10 -11
- package/dist/core/runner/steer-admission.js +6 -8
- package/dist/core/runner/stream-steer-verb.d.ts +5 -4
- package/dist/core/runner/stream-steer-verb.js +8 -10
- package/dist/core/runner/sub-agent-deny-lists.d.ts +32 -0
- package/dist/core/runner/sub-agent-deny-lists.js +121 -0
- package/dist/core/runner/synthetic-tools.d.ts +1 -1
- package/dist/core/runner/synthetic-tools.js +3 -2
- package/dist/core/session-policy-store.d.ts +6 -2
- package/dist/core/session-policy-store.js +2 -1
- package/dist/core/skill-grant.d.ts +10 -2
- package/dist/core/skill-grant.js +20 -4
- package/dist/core/steer-replay.d.ts +46 -0
- package/dist/core/steer-replay.js +22 -0
- package/dist/core/task-event.d.ts +3 -2
- package/dist/core/task-notification.d.ts +12 -1
- package/dist/core/task-notification.js +13 -1
- package/dist/core/task-spec.d.ts +5 -3
- package/dist/core/task-stream.d.ts +12 -6
- package/dist/core/tool-errors.d.ts +2 -2
- package/dist/core/tool-policy.d.ts +15 -4
- package/dist/core/tool-policy.js +9 -13
- package/dist/core/tool-spec.d.ts +20 -2
- package/dist/core/types.d.ts +2 -2
- package/dist/core/types.js +1 -1
- package/dist/index.d.ts +7 -4
- package/dist/index.js +7 -4
- package/dist/orchestration/workflow-types.d.ts +5 -0
- package/dist/orchestration/workflow.d.ts +3 -2
- package/dist/orchestration/workflow.js +16 -7
- package/dist/tools/fs/removal-targets.d.ts +82 -0
- package/dist/tools/fs/removal-targets.js +236 -0
- package/dist/tools/fs/shell-read-write-targets.d.ts +27 -0
- package/dist/tools/fs/shell-read-write-targets.js +1 -1
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +43 -5
- package/wire-changes/core@7.44.0.json +469 -0
- package/wire-changes/core@7.45.0.json +1084 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ToolSpec } from "../core/types.js";
|
|
1
|
+
import type { AgentDefinition, ToolSpec } from "../core/types.js";
|
|
2
2
|
/**
|
|
3
3
|
* Per-agent tool allow/deny (design/38 1A). Statically narrows a tool POOL to the subset a delegated
|
|
4
4
|
* sub-agent / team member may use — assembly-time filtering (the tool simply never appears in the
|
|
@@ -16,13 +16,74 @@ import type { ToolSpec } from "../core/types.js";
|
|
|
16
16
|
* Anchored (dual-leg verified): this spawn-time item-level filter IS the upstream shape for allow
|
|
17
17
|
* entries — CC's resolveAgentTools partitions unknown names into an `invalidTools` bucket nobody
|
|
18
18
|
* consumes at runtime, and a live 2.1.207 probe shows the agent stays listed/delegable with the
|
|
19
|
-
* unknown item silently dropped, zero warnings.
|
|
20
|
-
* `
|
|
21
|
-
*
|
|
19
|
+
* unknown item silently dropped, zero warnings. A DENY entry is read as the reference reads it
|
|
20
|
+
* ({@link readDeniedEntry}: `Tool(content)` names `Tool`, the content is not read, the whole tool is hidden). The
|
|
21
|
+
* list is judged once before the run, from every place a deployment writes one (prepare's
|
|
22
|
+
* `judgeDeniedToolNames`): a spelling that can name no tool is refused, the rest is told to the host.
|
|
22
23
|
*
|
|
23
24
|
* Note: `"*"` in `allowTools` is the "allow all" sentinel, so a tool literally NAMED `"*"` cannot be
|
|
24
25
|
* the sole allowed tool via this path (tool names are conventionally identifiers, so this is moot).
|
|
25
26
|
*/
|
|
27
|
+
/**
|
|
28
|
+
* How ONE denied-tools entry is read — the reference reads each as a rule string, without trimming: `Tool(content)` names
|
|
29
|
+
* `Tool` (the content is dropped, so the whole tool is hidden), `Tool()` and `Tool(*)` are the bare name, and anything else
|
|
30
|
+
* — a malformed paren form, an entry with surrounding whitespace — is the whole string, which names no tool. A value that is
|
|
31
|
+
* not a string (a list a host read from a file can hold anything) names no tool either: it reads as `not_a_string` with a
|
|
32
|
+
* fixed label of its TYPE, and nothing about the value is read, converted or printed — `typeof` and the array test only.
|
|
33
|
+
*/
|
|
34
|
+
export type DeniedEntryReading = {
|
|
35
|
+
readonly kind: "name";
|
|
36
|
+
readonly name: string;
|
|
37
|
+
readonly content: boolean;
|
|
38
|
+
} | {
|
|
39
|
+
readonly kind: "not_a_string";
|
|
40
|
+
readonly type: string;
|
|
41
|
+
};
|
|
42
|
+
export declare function readDeniedEntry(entry: unknown): DeniedEntryReading;
|
|
43
|
+
/** A fixed label of a host value's TYPE for a sentence that must not print or convert the value (`typeof` and the array test only). */
|
|
44
|
+
export declare function valueTypeLabel(value: unknown): string;
|
|
45
|
+
/**
|
|
46
|
+
* How a tool-name list's VALUE (an agent definition's allowed-tools or denied-tools list) reads as a list. A host that reads its
|
|
47
|
+
* definitions from a file can hand over anything, and a list can be hostile as a container (an accessor or a proxy trap that
|
|
48
|
+
* throws, an overridden iterator, holes): it is read BY INDEX over its length, once, into a dense copy — no iterator of the host
|
|
49
|
+
* runs, a hole reads as `undefined` (an entry that is not a string) — and a value that is not an array, or an array whose read
|
|
50
|
+
* throws, is reported as such and never printed or converted.
|
|
51
|
+
*/
|
|
52
|
+
export type ToolNameListReading = {
|
|
53
|
+
readonly kind: "entries";
|
|
54
|
+
readonly entries: readonly unknown[];
|
|
55
|
+
} | {
|
|
56
|
+
readonly kind: "not_a_list";
|
|
57
|
+
readonly type: string;
|
|
58
|
+
} | {
|
|
59
|
+
readonly kind: "unreadable";
|
|
60
|
+
};
|
|
61
|
+
export declare function readToolNameList(value: unknown): ToolNameListReading;
|
|
62
|
+
/** A tool-name list as the mount keeps it: the dense copy when it reads as a list, the failure marker when it cannot be read, else
|
|
63
|
+
* the value itself (not a list: the judge refuses it, and its type never changes). */
|
|
64
|
+
export declare function snapshotToolNameList(value: unknown): unknown;
|
|
65
|
+
/** The two tool-name lists of a definition, and the words a sentence calls each. */
|
|
66
|
+
export type ToolNameListSide = "allowed" | "denied";
|
|
67
|
+
/** The sentence for an entry that is not a string: it can be no tool name; the sentence names its place and its type, never its value. */
|
|
68
|
+
export declare function notAStringSentence(where: string, side: ToolNameListSide, index: number, type: string): string;
|
|
69
|
+
/** The sentence for a value that is not a list, or a list that cannot be read: it names the type, never the value. */
|
|
70
|
+
export declare function notAListSentence(where: string, side: ToolNameListSide, list: Exclude<ToolNameListReading, {
|
|
71
|
+
kind: "entries";
|
|
72
|
+
}>): string;
|
|
73
|
+
/**
|
|
74
|
+
* Why a host-written tool-name list cannot be one, as a sentence — or `undefined` when it is absent (`undefined` / `null`) or a list
|
|
75
|
+
* of strings. The one reading the judgment before the run and the spawn-time check of a tool that never went through prepare share.
|
|
76
|
+
*/
|
|
77
|
+
export declare function toolNameListFlaw(where: string, side: ToolNameListSide, value: unknown): string | undefined;
|
|
78
|
+
/** A definition with its two tool-name lists replaced by their one copy — the copy every later reader (the judgment before the
|
|
79
|
+
* run, the roster, the filter at each spawn) takes instead of the host's container. */
|
|
80
|
+
export declare function snapshotDefinitionLists(def: AgentDefinition): AgentDefinition;
|
|
81
|
+
/**
|
|
82
|
+
* The names an allowed-tools value admits, or `undefined` when it names no allowlist: absent, `null`, or a list holding the
|
|
83
|
+
* wildcard. A list admits its string entries (an entry that is not a string admits nothing); a value that is not a list admits
|
|
84
|
+
* NOTHING — the judge before the run refuses it, and a reader that meets it first fails closed.
|
|
85
|
+
*/
|
|
86
|
+
export declare function allowedToolNames(allowTools: unknown): ReadonlySet<string> | undefined;
|
|
26
87
|
/**
|
|
27
88
|
* Membership check for ONE tool name under the same allow/deny semantics as {@link resolveToolSubset}
|
|
28
89
|
* (canonicalized both sides; `"*"` allow sentinel; deny wins). Used by the delegation tool to decide
|
|
@@ -1,12 +1,135 @@
|
|
|
1
|
+
import { parsePermissionRule } from "../core/permission-rule-syntax.js";
|
|
2
|
+
export function readDeniedEntry(entry) {
|
|
3
|
+
if (typeof entry !== "string")
|
|
4
|
+
return { kind: "not_a_string", type: valueTypeLabel(entry) };
|
|
5
|
+
if (entry !== entry.trim())
|
|
6
|
+
return { kind: "name", name: entry, content: false };
|
|
7
|
+
const parsed = parsePermissionRule(entry);
|
|
8
|
+
return { kind: "name", name: parsed.toolName, content: parsed.ruleContent !== undefined };
|
|
9
|
+
}
|
|
10
|
+
export function valueTypeLabel(value) {
|
|
11
|
+
if (value === null)
|
|
12
|
+
return "null";
|
|
13
|
+
switch (typeof value) {
|
|
14
|
+
case "string":
|
|
15
|
+
return "a string";
|
|
16
|
+
case "undefined":
|
|
17
|
+
return "undefined";
|
|
18
|
+
case "number":
|
|
19
|
+
return "a number";
|
|
20
|
+
case "boolean":
|
|
21
|
+
return "a boolean";
|
|
22
|
+
case "bigint":
|
|
23
|
+
return "a bigint";
|
|
24
|
+
case "symbol":
|
|
25
|
+
return "a symbol";
|
|
26
|
+
case "function":
|
|
27
|
+
return "a function";
|
|
28
|
+
default:
|
|
29
|
+
try {
|
|
30
|
+
return Array.isArray(value) ? "an array" : "an object";
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return "an object";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const isList = (value) => Array.isArray(value);
|
|
38
|
+
const UNREADABLE_LIST = Object.freeze({});
|
|
39
|
+
export function readToolNameList(value) {
|
|
40
|
+
if (value === UNREADABLE_LIST)
|
|
41
|
+
return { kind: "unreadable" };
|
|
42
|
+
let list;
|
|
43
|
+
try {
|
|
44
|
+
list = isList(value) ? value : undefined;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return { kind: "not_a_list", type: "an object" };
|
|
48
|
+
}
|
|
49
|
+
if (list === undefined)
|
|
50
|
+
return { kind: "not_a_list", type: valueTypeLabel(value) };
|
|
51
|
+
try {
|
|
52
|
+
const entries = [];
|
|
53
|
+
for (let i = 0; i < list.length; i++)
|
|
54
|
+
entries.push(list[i]);
|
|
55
|
+
return { kind: "entries", entries };
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return { kind: "unreadable" };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export function snapshotToolNameList(value) {
|
|
62
|
+
const read = readToolNameList(value);
|
|
63
|
+
return read.kind === "entries" ? read.entries : read.kind === "unreadable" ? UNREADABLE_LIST : value;
|
|
64
|
+
}
|
|
65
|
+
const LIST_NOUN = { allowed: "allowed tools", denied: "denied tools" };
|
|
66
|
+
const LIST_NAME = { allowed: "an allowed-tools list", denied: "a denied-tools list" };
|
|
67
|
+
export function notAStringSentence(where, side, index, type) {
|
|
68
|
+
return `${where} declares in its ${LIST_NOUN[side]}, as entry ${index + 1}, a value that is ${type}, not a tool name — ${LIST_NAME[side]} holds tool names, each a string. Fix the entry.`;
|
|
69
|
+
}
|
|
70
|
+
export function notAListSentence(where, side, list) {
|
|
71
|
+
return list.kind === "not_a_list"
|
|
72
|
+
? `${where} declares in its ${LIST_NOUN[side]} a value that is ${list.type}, not a list of tool names. Fix the list.`
|
|
73
|
+
: `${where} declares in its ${LIST_NOUN[side]} a list whose entries cannot be read (an accessor or a proxy trap threw). Fix the list.`;
|
|
74
|
+
}
|
|
75
|
+
export function toolNameListFlaw(where, side, value) {
|
|
76
|
+
if (value === undefined || value === null)
|
|
77
|
+
return undefined;
|
|
78
|
+
const list = readToolNameList(value);
|
|
79
|
+
if (list.kind !== "entries")
|
|
80
|
+
return notAListSentence(where, side, list);
|
|
81
|
+
for (let i = 0; i < list.entries.length; i++) {
|
|
82
|
+
const entry = list.entries[i];
|
|
83
|
+
if (typeof entry !== "string")
|
|
84
|
+
return notAStringSentence(where, side, i, valueTypeLabel(entry));
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
export function snapshotDefinitionLists(def) {
|
|
89
|
+
const copy = { ...def };
|
|
90
|
+
if (copy.allowTools !== undefined)
|
|
91
|
+
copy.allowTools = snapshotToolNameList(copy.allowTools);
|
|
92
|
+
if (copy.denyTools !== undefined)
|
|
93
|
+
copy.denyTools = snapshotToolNameList(copy.denyTools);
|
|
94
|
+
return copy;
|
|
95
|
+
}
|
|
96
|
+
export function allowedToolNames(allowTools) {
|
|
97
|
+
if (allowTools === undefined || allowTools === null)
|
|
98
|
+
return undefined;
|
|
99
|
+
const list = readToolNameList(allowTools);
|
|
100
|
+
if (list.kind !== "entries")
|
|
101
|
+
return new Set();
|
|
102
|
+
if (list.entries.includes("*"))
|
|
103
|
+
return undefined;
|
|
104
|
+
const names = new Set();
|
|
105
|
+
for (const entry of list.entries)
|
|
106
|
+
if (typeof entry === "string")
|
|
107
|
+
names.add(entry);
|
|
108
|
+
return names;
|
|
109
|
+
}
|
|
110
|
+
function deniedNames(denyTools) {
|
|
111
|
+
const names = new Set();
|
|
112
|
+
const list = readToolNameList(denyTools);
|
|
113
|
+
if (list.kind !== "entries")
|
|
114
|
+
return names;
|
|
115
|
+
for (const entry of list.entries) {
|
|
116
|
+
const read = readDeniedEntry(entry);
|
|
117
|
+
if (read.kind === "name")
|
|
118
|
+
names.add(read.name);
|
|
119
|
+
}
|
|
120
|
+
return names;
|
|
121
|
+
}
|
|
1
122
|
export function toolNameAllowed(name, allowTools, denyTools) {
|
|
2
|
-
const allow =
|
|
3
|
-
const
|
|
123
|
+
const allow = allowedToolNames(allowTools);
|
|
124
|
+
const denied = deniedNames(denyTools);
|
|
125
|
+
const deny = denied.size > 0 ? denied : undefined;
|
|
4
126
|
const n = name;
|
|
5
127
|
return (!allow || allow.has(n)) && (!deny || !deny.has(n));
|
|
6
128
|
}
|
|
7
129
|
export function resolveToolSubset(pool, allowTools, denyTools) {
|
|
8
|
-
const allow =
|
|
9
|
-
const
|
|
130
|
+
const allow = allowedToolNames(allowTools);
|
|
131
|
+
const denied = deniedNames(denyTools);
|
|
132
|
+
const deny = denied.size > 0 ? denied : undefined;
|
|
10
133
|
if (!allow && !deny)
|
|
11
134
|
return [...pool];
|
|
12
135
|
return pool.filter((t) => {
|
package/dist/brain/errors.d.ts
CHANGED
|
@@ -147,6 +147,14 @@ export declare function extractErrorCode(errorMessage: string | undefined): Brai
|
|
|
147
147
|
* human text. Pairs with {@link extractErrorCode} so a caller can lift the code and clean the message
|
|
148
148
|
* without re-hardcoding the code list (the Runner does both when assembling `TaskResult`). */
|
|
149
149
|
export declare function stripErrorCodePrefix(errorMessage: string): string;
|
|
150
|
+
/**
|
|
151
|
+
* The code a deployment's outbound dispatcher stamps on the error of a proxy that answered the tunnel request
|
|
152
|
+
* (`CONNECT`) with anything but 200, with the HTTP status the proxy gave in `statusCode` on the same node. Not a
|
|
153
|
+
* runtime code — undici spells that refusal with prose and no stable code — so it is defined here, once, for the
|
|
154
|
+
* deployment to mint and for {@link DETERMINISTIC_CONNECT_CODES} to read. The proxy ANSWERED: re-sending the same
|
|
155
|
+
* request through the same proxy only changes that answer if someone changes the proxy's rules.
|
|
156
|
+
*/
|
|
157
|
+
export declare const PROXY_TUNNEL_REFUSED = "ERR_PROXY_TUNNEL_REFUSED";
|
|
150
158
|
/**
|
|
151
159
|
* Whether a transport-level connect failure is DETERMINISTIC (see {@link DETERMINISTIC_CONNECT_CODES})
|
|
152
160
|
* or gives no such verdict. Classification is by `code` along the error graph — never by message text,
|
|
@@ -165,7 +173,18 @@ export declare function stripErrorCodePrefix(errorMessage: string): string;
|
|
|
165
173
|
* pre-existing behavior.
|
|
166
174
|
*/
|
|
167
175
|
export type ConnectFailureClass = "deterministic" | "indeterminate";
|
|
176
|
+
/** What one walk of a transport error's graph found: the verdict, and — only with a `deterministic` one — the proxy
|
|
177
|
+
* refusal in it, if any (`status` absent when the node states none readable). One walk answers both, so the verdict
|
|
178
|
+
* and the sentence it licenses cannot disagree about which node spoke. */
|
|
179
|
+
export interface ConnectFailureReading {
|
|
180
|
+
verdict: ConnectFailureClass;
|
|
181
|
+
proxyRefusal?: {
|
|
182
|
+
status: number | undefined;
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
export declare function readConnectFailure(e: unknown): ConnectFailureReading;
|
|
168
186
|
export declare function classifyConnectFailure(e: unknown): ConnectFailureClass;
|
|
187
|
+
export declare function proxyTunnelRefusedSentence(status: number | undefined): string;
|
|
169
188
|
/** What a throw out of `fetch` was about: the NETWORK (retryable — the historic reading of every throw
|
|
170
189
|
* here), or the REQUEST ITSELF, which is deterministic: identical arguments reproduce it every time. */
|
|
171
190
|
export type FetchThrowClass = "transport" | "request_unbuildable";
|
|
@@ -174,8 +193,10 @@ export type FetchThrowClass = "transport" | "request_unbuildable";
|
|
|
174
193
|
* throws a `TypeError` with no `code` and no `cause`, so the connect classifier answered indeterminate and
|
|
175
194
|
* the call spent its whole backoff budget on a request the runtime never built).
|
|
176
195
|
*
|
|
177
|
-
* ONE question — is there TRANSPORT EVIDENCE? — in its
|
|
178
|
-
* `cause` that is a transport fact rather than a parse verdict
|
|
196
|
+
* ONE question — is there TRANSPORT EVIDENCE? — in its three forms: the runtime's own shell sentence, a
|
|
197
|
+
* `cause` that is a transport fact rather than a parse verdict, or a graph the connect judge reads as a
|
|
198
|
+
* definite answer ({@link classifyConnectFailure}: the same table, the same unanimity). A `TypeError` with none
|
|
199
|
+
* was never built.
|
|
179
200
|
* Measured over ten real shapes; non-`TypeError` stays on the transport arm and an unreadable value answers
|
|
180
201
|
* `transport`, the pre-existing behavior. Stated cost: KL-933a. */
|
|
181
202
|
export declare function classifyFetchThrow(e: unknown): FetchThrowClass;
|
package/dist/brain/errors.js
CHANGED
|
@@ -138,8 +138,13 @@ function hasStaleConnectionCode(e) {
|
|
|
138
138
|
}
|
|
139
139
|
return false;
|
|
140
140
|
}
|
|
141
|
-
const
|
|
141
|
+
export const PROXY_TUNNEL_REFUSED = "ERR_PROXY_TUNNEL_REFUSED";
|
|
142
|
+
const DETERMINISTIC_CONNECT_CODES = new Set(["ECONNREFUSED", "ENOTFOUND", PROXY_TUNNEL_REFUSED]);
|
|
142
143
|
const CONNECT_GRAPH_NODE_BUDGET = 16;
|
|
144
|
+
function proxyStatusOf(node) {
|
|
145
|
+
const status = readMember(node, "statusCode");
|
|
146
|
+
return typeof status === "number" && Number.isInteger(status) && status < 600 && namesTheFailure(status) ? status : undefined;
|
|
147
|
+
}
|
|
143
148
|
function walkConnectFailure(node, budget) {
|
|
144
149
|
if (budget.left <= 0)
|
|
145
150
|
return "indeterminate";
|
|
@@ -151,6 +156,8 @@ function walkConnectFailure(node, budget) {
|
|
|
151
156
|
if (typeof code === "string") {
|
|
152
157
|
if (!DETERMINISTIC_CONNECT_CODES.has(code))
|
|
153
158
|
return "indeterminate";
|
|
159
|
+
if (code === PROXY_TUNNEL_REFUSED && budget.proxy === undefined)
|
|
160
|
+
budget.proxy = { status: proxyStatusOf(node) };
|
|
154
161
|
verdict = "deterministic";
|
|
155
162
|
}
|
|
156
163
|
if (!isErrorLike(node))
|
|
@@ -176,8 +183,23 @@ function walkConnectFailure(node, budget) {
|
|
|
176
183
|
}
|
|
177
184
|
return verdict;
|
|
178
185
|
}
|
|
186
|
+
export function readConnectFailure(e) {
|
|
187
|
+
const walk = { left: CONNECT_GRAPH_NODE_BUDGET };
|
|
188
|
+
if (walkConnectFailure(e, walk) !== "deterministic")
|
|
189
|
+
return { verdict: "indeterminate" };
|
|
190
|
+
return walk.proxy === undefined ? { verdict: "deterministic" } : { verdict: "deterministic", proxyRefusal: walk.proxy };
|
|
191
|
+
}
|
|
179
192
|
export function classifyConnectFailure(e) {
|
|
180
|
-
return
|
|
193
|
+
return readConnectFailure(e).verdict;
|
|
194
|
+
}
|
|
195
|
+
const PROXY_TUNNEL_REFUSED_SENTENCES = new Map([
|
|
196
|
+
[407, "proxy authentication required (HTTP 407)"],
|
|
197
|
+
[403, "proxy forbids this destination (HTTP 403)"],
|
|
198
|
+
]);
|
|
199
|
+
export function proxyTunnelRefusedSentence(status) {
|
|
200
|
+
if (status === undefined)
|
|
201
|
+
return "proxy refused the tunnel";
|
|
202
|
+
return PROXY_TUNNEL_REFUSED_SENTENCES.get(status) ?? `proxy refused the tunnel (HTTP ${status})`;
|
|
181
203
|
}
|
|
182
204
|
const FETCH_TRANSPORT_SHELLS = new Set(["fetch failed", "terminated"]);
|
|
183
205
|
const REQUEST_ARGUMENT_CODES = new Set(["ERR_INVALID_URL"]);
|
|
@@ -187,6 +209,8 @@ export function classifyFetchThrow(e) {
|
|
|
187
209
|
const message = readMember(e, "message");
|
|
188
210
|
if (typeof message === "string" && FETCH_TRANSPORT_SHELLS.has(message))
|
|
189
211
|
return "transport";
|
|
212
|
+
if (classifyConnectFailure(e) === "deterministic")
|
|
213
|
+
return "transport";
|
|
190
214
|
const cause = readMember(e, "cause");
|
|
191
215
|
if (cause === undefined || cause === null)
|
|
192
216
|
return "request_unbuildable";
|
|
@@ -27,7 +27,8 @@ export interface StreamEngineConfig extends BrainTimeoutConfig {
|
|
|
27
27
|
retryDelayMs?: number;
|
|
28
28
|
/**
|
|
29
29
|
* Retries allowed for a DETERMINISTIC connect failure — one whose error codes say the target itself
|
|
30
|
-
* is not there (`ECONNREFUSED`, or a name with no address
|
|
30
|
+
* is not there (`ECONNREFUSED`, or a name with no address) or that the proxy on the way refused the tunnel
|
|
31
|
+
* (`PROXY_TUNNEL_REFUSED`); see `errors.ts` `readConnectFailure`.
|
|
31
32
|
* Absent ⇒ {@link DEFAULT_DETERMINISTIC_CONNECT_RETRIES}.
|
|
32
33
|
*
|
|
33
34
|
* These retries exist for one case only: a peer that is mid-restart and about to accept. Waiting
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createAssistantMessageEventStream, } from "../internal/llm.js";
|
|
2
2
|
import { STREAM_TORN_WARNING } from "./stream-shared.js";
|
|
3
3
|
import { FLOOR_OUTPUT_TOKENS, parseContextOverflow, planOutputCapAdjustment } from "./context-overflow.js";
|
|
4
|
-
import { abortLikeName, BrainError, brainErrorCodeOf,
|
|
4
|
+
import { abortLikeName, BrainError, brainErrorCodeOf, classifyFetchThrow, classifyHttp, describeNetworkError, isProviderEchoShape, namesTheFailure, proxyTunnelRefusedSentence, readConnectFailure, readProviderErrorCode, readProviderRequestId, refusedTheRequest } from "./errors.js";
|
|
5
5
|
import { stripUrlCredentials } from "./route-adjudicator.js";
|
|
6
6
|
import { classifyInputTooLong } from "./input-too-long.js";
|
|
7
7
|
import { FAST_MAX_BACKOFF_MS, providerWaitHint, retryBackoffMs } from "./retry.js";
|
|
@@ -531,7 +531,8 @@ export function runStreamingBrain(args) {
|
|
|
531
531
|
const retryableByStatus = netErr !== undefined ||
|
|
532
532
|
(r !== undefined && (r.status >= 500 || r.status === 429 || r.status === 408 || r.status === 409));
|
|
533
533
|
const retryable = shouldRetryHeaderVerdict(r) ?? retryableByStatus;
|
|
534
|
-
const
|
|
534
|
+
const connectReading = netErr !== undefined ? readConnectFailure(netErr) : undefined;
|
|
535
|
+
const deterministicConnect = connectReading?.verdict === "deterministic";
|
|
535
536
|
const fastLane = deterministicConnect && deterministicLaneActive;
|
|
536
537
|
const laneMaxRetries = fastLane ? deterministicConnectBudget : maxRetries;
|
|
537
538
|
const errClass = netErr !== undefined
|
|
@@ -552,11 +553,8 @@ export function runStreamingBrain(args) {
|
|
|
552
553
|
? ` (the requested wait was longer than the cap; honoring ${Math.ceil(waitHint.ms / 1000)}s instead of ${Math.ceil(waitHint.rawMs / 1000)}s)`
|
|
553
554
|
: "";
|
|
554
555
|
const statusPhase = r?.status === 429 ? "rate_limited" : netErr !== undefined ? "reconnecting" : "retrying";
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
: fastLane
|
|
558
|
-
? "no endpoint answering, retrying briefly"
|
|
559
|
-
: "no endpoint answering, retrying";
|
|
556
|
+
const refusedSubject = connectReading?.proxyRefusal !== undefined ? "proxy refused the tunnel" : "no endpoint answering";
|
|
557
|
+
const reconnectDetail = errClass !== "connect_refused" ? "connection lost, reconnecting" : fastLane ? `${refusedSubject}, retrying briefly` : `${refusedSubject}, retrying`;
|
|
560
558
|
emitBrainTelemetry({ kind: "retry", attempt: attempt + 1, phase: "connect", errClass, nextDelayMs: delayMs });
|
|
561
559
|
announcedRetry = true;
|
|
562
560
|
discardResponseBody(r);
|
|
@@ -580,8 +578,11 @@ export function runStreamingBrain(args) {
|
|
|
580
578
|
attempt++;
|
|
581
579
|
continue;
|
|
582
580
|
}
|
|
583
|
-
if (netErr)
|
|
584
|
-
|
|
581
|
+
if (netErr) {
|
|
582
|
+
const chain = describeNetworkError(netErr);
|
|
583
|
+
const proxyRefusal = connectReading?.proxyRefusal;
|
|
584
|
+
throw new BrainError("network", proxyRefusal === undefined ? chain : `${proxyTunnelRefusedSentence(proxyRefusal.status)} — ${chain}`, undefined, { apiError: true });
|
|
585
|
+
}
|
|
585
586
|
const detail = overflowBody ?? (r ? (await readErrorBody(r, signal)).text : "");
|
|
586
587
|
const status = r?.status ?? 0;
|
|
587
588
|
const itl = classifyInputTooLong(status, detail);
|
|
@@ -33,7 +33,7 @@ export interface AgentDefinition {
|
|
|
33
33
|
* the delegation tool's name cannot re-delegate (CC Explore/Plan `disallowedTools` include Agent,
|
|
34
34
|
* pretty.js:409189/486948). */
|
|
35
35
|
allowTools?: string[];
|
|
36
|
-
/** Tool denylist (a deny always wins)
|
|
36
|
+
/** Tool denylist (a deny always wins), tool NAMES: `Tool(content)` hides the whole `Tool`. See {@link allowTools} for the per-agent delegation-tool note. */
|
|
37
37
|
denyTools?: string[];
|
|
38
38
|
/**
|
|
39
39
|
* F7 (CC 2.1.198 parity, per-agent `skills` — 锚 pretty.js:422126 `e.skills ?? []`, GA/ungated in 198:
|
|
@@ -116,7 +116,7 @@ export interface SkillSpec {
|
|
|
116
116
|
* either list asks once (`Execute skill: <name>`, the card listing what it releases); a skill the person invoked by
|
|
117
117
|
* name ({@link import("./task-spec.js").TaskSpec.userInvokedSkills}) grants without asking. The grant holds for that
|
|
118
118
|
* run (a steer it takes in does not end it) and the next runTask starts without it. An entry this grammar reads as nothing — `*` (it voids an allow list; on a deny list it is inert and the rest of the
|
|
119
|
-
* list still refuses), `mcp__*`, a write path, a domain — grants nothing and is announced at the start of the run (`permission.allowed_tool_not_granted`).
|
|
119
|
+
* list still refuses), `mcp__*`, a write path, a domain — grants nothing and is announced at the start of the run (`permission.allowed_tool_not_granted`). A table whose value is not a list (a string, an object…) reads as one such entry; `null` is no table.
|
|
120
120
|
*/
|
|
121
121
|
allowedTools?: string[];
|
|
122
122
|
/** The tools refused for the rest of the run this skill is activated in — the frontmatter `disallowed-tools` list,
|
|
@@ -3,17 +3,33 @@ import { type ToolCallRequest } from "./tool-policy.js";
|
|
|
3
3
|
import { type AllowRuleMember, type AllowRuleShadow } from "./allow-rule-shadow.js";
|
|
4
4
|
import { type PersistedRule } from "./permission-rule-model.js";
|
|
5
5
|
import { type AskClass, type AssertAllKeysHandled, type PermissionMode, type PersistedRuleMandate } from "./ask-origin.js";
|
|
6
|
+
import { type ToolNameListReading } from "../agents/tool-filter.js";
|
|
6
7
|
import { type EngineNotice } from "./engine-notice.js";
|
|
7
8
|
/** Why an entry grants (or refuses) nothing — the closed set the start-of-run notice names, for every list. `tool_name`: the
|
|
8
9
|
* entry's tool name is one the name judgment reads as stale against the run's roster (`verdict` says which); `near_miss`: the
|
|
9
10
|
* run mounts no tool of that exact name but mounts one that differs only by case (`currentName` is its spelling). */
|
|
10
11
|
export declare const ALLOW_ENTRY_INERT_REASONS: readonly ["wildcard_all", "namespace_wildcard", "path_pattern", "domain", "content_unsupported", "grammar", "tool_name", "near_miss"];
|
|
11
12
|
export type AllowEntryInertReason = (typeof ALLOW_ENTRY_INERT_REASONS)[number];
|
|
12
|
-
/** The facts beside a `tool_name` / `near_miss`
|
|
13
|
+
/** The facts beside a reason: for `tool_name` / `near_miss` the judgment's word and the spelling to write instead when there is
|
|
14
|
+
* one; for a list member that is not a string (`grammar`) the TYPE of the value (`a number`, `null`, `an object`…) — `entry` then
|
|
15
|
+
* holds {@link nonStringMemberLabel}, never the value. */
|
|
13
16
|
export interface AllowEntryInertDetail {
|
|
14
17
|
readonly verdict?: Exclude<RuleToolNameVerdict["kind"], "current">;
|
|
15
18
|
readonly currentName?: string;
|
|
19
|
+
readonly valueType?: string;
|
|
16
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* How a list member that is not a string is named wherever a list's entries are named (the inert record's `entry`, the Skill
|
|
23
|
+
* card's binding): its TYPE, in a spelling no readable entry can have — a readable entry's name holds no whitespace and a
|
|
24
|
+
* name-only entry holds no parenthesis, and this label holds a space and none. It can equal only another entry that reads as
|
|
25
|
+
* nothing, so a binding that holds it never stands for an entry that grants or refuses.
|
|
26
|
+
*/
|
|
27
|
+
export declare function nonStringMemberLabel(value: unknown): string;
|
|
28
|
+
/** How a tool-name list's VALUE that is not a list stands as one entry (the same kind of spelling as {@link nonStringMemberLabel}: it
|
|
29
|
+
* reads as `grammar`): its TYPE, or that its entries could not be read — never the value. */
|
|
30
|
+
export declare function nonListValueLabel(read: Exclude<ToolNameListReading, {
|
|
31
|
+
kind: "entries";
|
|
32
|
+
}>): string;
|
|
17
33
|
/** One entry a list reads as nothing. */
|
|
18
34
|
export type AllowEntryInert = {
|
|
19
35
|
readonly entry: string;
|
|
@@ -189,6 +205,7 @@ declare const SHADOW_REASON: {
|
|
|
189
205
|
readonly content_question: "the call is the engine's own question to a person";
|
|
190
206
|
readonly hook: "a PreToolUse hook asks about this call — the deployment's own screening is answered by a person";
|
|
191
207
|
readonly ancestor: "the task that delegated this one owns this question";
|
|
208
|
+
readonly dangerous_delete: "this removal targets a protected location (the filesystem root, a top-level directory, a home directory, or a workspace directory or one of its ancestors) — it is cleared by confirming this call, never by an allow rule";
|
|
192
209
|
readonly unverifiable_delete: "this removal could not be verified safe — it is cleared by confirming this call, never by an allow rule";
|
|
193
210
|
readonly transcript_write: "this write targets the session transcript — it is cleared by confirming this call, never by an allow rule";
|
|
194
211
|
readonly org_rule: "an organization policy rule requires approval for this call";
|
|
@@ -244,6 +261,13 @@ export type AllowEntryInertNote = ({
|
|
|
244
261
|
} & AllowEntryInert) | ({
|
|
245
262
|
readonly source: AllowEntrySource;
|
|
246
263
|
} & AllowEntryInert);
|
|
264
|
+
/**
|
|
265
|
+
* THE ONE WRITER of a list entry into a sentence (the start-of-run notice, a skill's refusal and its decisions, a sub-agent
|
|
266
|
+
* list's refusals and advisories): the entry as a JSON string — quotes, backslashes and the usual escapes — with every
|
|
267
|
+
* character that prints as nothing, or as a space, written as `\uXXXX` (`\u{…}` beyond the BMP) and a run of spaces kept
|
|
268
|
+
* space by space, so what a person reads is what the engine read. Capped on what is printed.
|
|
269
|
+
*/
|
|
270
|
+
export declare function renderListEntry(entry: string): string;
|
|
247
271
|
/**
|
|
248
272
|
* One entry that reads as nothing, as a person reads it: the entry as written (quoted, so a surrounding space shows) and why
|
|
249
273
|
* — the reason word, with the spelling to write when the reading has one. The one phrasing the start-of-run notice and a
|
package/dist/core/allow-entry.js
CHANGED
|
@@ -9,12 +9,19 @@ import { pathRuleLaneAnswer } from "./runner/permission-rule-lanes.js";
|
|
|
9
9
|
import { splitShellCompoundSegments } from "../tools/fs/bash-readonly-classifier.js";
|
|
10
10
|
import { mandateDisclosureOf } from "./ask-origin.js";
|
|
11
11
|
import { inlineUntrusted } from "./untrusted-text.js";
|
|
12
|
+
import { valueTypeLabel } from "../agents/tool-filter.js";
|
|
12
13
|
import { deliverEngineNotice } from "./engine-notice.js";
|
|
13
14
|
export const ALLOW_ENTRY_INERT_REASONS = Object.freeze(["wildcard_all", "namespace_wildcard", "path_pattern", "domain", "content_unsupported", "grammar", "tool_name", "near_miss"]);
|
|
15
|
+
export function nonStringMemberLabel(value) {
|
|
16
|
+
return `<not a string: ${valueTypeLabel(value)}>`;
|
|
17
|
+
}
|
|
18
|
+
export function nonListValueLabel(read) {
|
|
19
|
+
return read.kind === "not_a_list" ? `<not a list: ${read.type}>` : "<a list whose entries cannot be read>";
|
|
20
|
+
}
|
|
14
21
|
function ruleRowOf(parsed) {
|
|
15
22
|
return { ...parsed, scope: { kind: "global" }, adds: [{ dot: { actor: "list", counter: 1 }, origin: "user", createdAt: "" }] };
|
|
16
23
|
}
|
|
17
|
-
const TOOL_NAME_SPELLING = /^[^\s()]
|
|
24
|
+
const TOOL_NAME_SPELLING = /^[^\s()\p{Cc}\p{Cf}]+$/u;
|
|
18
25
|
function mountedSpellingOf(name, roster) {
|
|
19
26
|
const folded = name.toLowerCase();
|
|
20
27
|
for (const mounted of roster)
|
|
@@ -36,6 +43,8 @@ function toolNameReading(name, roster) {
|
|
|
36
43
|
function readAllowEntry(raw, behavior, roster) {
|
|
37
44
|
const inert = (reason) => ({ inert: reason });
|
|
38
45
|
const row = (compiled) => ({ compiled });
|
|
46
|
+
if (typeof raw !== "string")
|
|
47
|
+
return { inert: "grammar", detail: { valueType: valueTypeLabel(raw) } };
|
|
39
48
|
if (raw !== raw.trim())
|
|
40
49
|
return inert("grammar");
|
|
41
50
|
const parsed = parsePermissionRule(raw);
|
|
@@ -94,11 +103,11 @@ export function compileAllowList(entries, behavior, roster) {
|
|
|
94
103
|
const rows = [];
|
|
95
104
|
const inert = [];
|
|
96
105
|
for (const entry of entries) {
|
|
97
|
-
const read =
|
|
106
|
+
const read = typeof entry !== "string" || !voided ? readAllowEntry(entry, behavior, roster) : { inert: "wildcard_all" };
|
|
98
107
|
if ("compiled" in read)
|
|
99
108
|
rows.push({ entry, compiled: read.compiled });
|
|
100
109
|
else
|
|
101
|
-
inert.push({ entry, reason: read.inert, ...read.detail });
|
|
110
|
+
inert.push({ entry: typeof entry === "string" ? entry : nonStringMemberLabel(entry), reason: read.inert, ...read.detail });
|
|
102
111
|
}
|
|
103
112
|
return { rows, inert };
|
|
104
113
|
}
|
|
@@ -307,6 +316,7 @@ const SHADOW_REASON = {
|
|
|
307
316
|
content_question: "the call is the engine's own question to a person",
|
|
308
317
|
hook: "a PreToolUse hook asks about this call — the deployment's own screening is answered by a person",
|
|
309
318
|
ancestor: "the task that delegated this one owns this question",
|
|
319
|
+
dangerous_delete: "this removal targets a protected location (the filesystem root, a top-level directory, a home directory, or a workspace directory or one of its ancestors) — it is cleared by confirming this call, never by an allow rule",
|
|
310
320
|
unverifiable_delete: "this removal could not be verified safe — it is cleared by confirming this call, never by an allow rule",
|
|
311
321
|
transcript_write: "this write targets the session transcript — it is cleared by confirming this call, never by an allow rule",
|
|
312
322
|
org_rule: "an organization policy rule requires approval for this call",
|
|
@@ -366,7 +376,22 @@ export function cardDisclosureOf(decision, fallbackMessage) {
|
|
|
366
376
|
}
|
|
367
377
|
export const ALLOWED_TOOL_NOT_GRANTED_CODE = "permission.allowed_tool_not_granted";
|
|
368
378
|
const NOTICE_ENTRIES_SHOWN = 5;
|
|
379
|
+
const UNSEEN_IN_ENTRY = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}\p{Zs}]/gu;
|
|
380
|
+
const ENTRY_RENDER_MAX = 122;
|
|
381
|
+
function escapedCodePoint(ch) {
|
|
382
|
+
const cp = ch.codePointAt(0) ?? 0;
|
|
383
|
+
const hex = cp.toString(16).padStart(4, "0");
|
|
384
|
+
return cp > 0xffff ? `\\u{${hex}}` : `\\u${hex}`;
|
|
385
|
+
}
|
|
386
|
+
export function renderListEntry(entry) {
|
|
387
|
+
const quoted = JSON.stringify(entry)
|
|
388
|
+
.replace(/ {2,}/g, (run) => "\\u0020".repeat(run.length))
|
|
389
|
+
.replace(UNSEEN_IN_ENTRY, (ch) => (ch === " " ? ch : escapedCodePoint(ch)));
|
|
390
|
+
return inlineUntrusted(quoted, ENTRY_RENDER_MAX);
|
|
391
|
+
}
|
|
369
392
|
export function inertEntryPhrase(e) {
|
|
393
|
+
if (e.valueType !== undefined)
|
|
394
|
+
return `a value that is ${e.valueType} (grammar: not a string)`;
|
|
370
395
|
const spelling = e.currentName !== undefined ? inlineUntrusted(e.currentName, 120) : undefined;
|
|
371
396
|
const why = (() => {
|
|
372
397
|
if (e.reason === "near_miss")
|
|
@@ -386,7 +411,7 @@ export function inertEntryPhrase(e) {
|
|
|
386
411
|
}
|
|
387
412
|
}
|
|
388
413
|
})();
|
|
389
|
-
return `${
|
|
414
|
+
return `${renderListEntry(e.entry)} (${why})`;
|
|
390
415
|
}
|
|
391
416
|
export function allowedToolsNotGrantedNotice(inert, sessionId) {
|
|
392
417
|
const where = (e) => (e.source === "skill" ? `skill ${inlineUntrusted(e.skill, 80)} ${e.list}` : e.source === "request" ? "this run's allowedTools" : "the deployment's allowedTools");
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
* the ask). Both stay where they are and are consumed beside the origin, not folded into it.
|
|
26
26
|
*/
|
|
27
27
|
import type { PermissionResult } from "./tool-policy.js";
|
|
28
|
+
import type { ContainmentFace } from "../tools/fs/containment-face.js";
|
|
28
29
|
/**
|
|
29
30
|
* The closed set, in AUTHORITY order — the order {@link askOriginOf} resolves a fold in when several
|
|
30
31
|
* facts hold on one call (a hook ask folded over a policy ask, an org stamp over either). The order
|
|
@@ -448,8 +449,12 @@ export type ToolMarkSource = "deployment" | "mcp_annotation" | "a2a_default";
|
|
|
448
449
|
* `classifier_no_ruling` is a classifier station's question to a person because the auto-mode classification did not rule
|
|
449
450
|
* (`classifierNoRuling` — it could not run, or replied with no verdict): the denial-limit fallback's row (only a person
|
|
450
451
|
* answers it; the classifier is never asked again).
|
|
452
|
+
* `dangerous_delete` is a removal whose target is a protected location (`dangerousRemovalOf`: the filesystem root, a
|
|
453
|
+
* top-level directory, a home directory, or a workspace directory — the task root, the working directory, an additional
|
|
454
|
+
* working directory — or an ancestor of one): the reference implementation's dangerous-removal breaker, which no mode,
|
|
455
|
+
* allow rule or classifier answers. `unverifiable_delete` is its sibling for a target the command text cannot resolve.
|
|
451
456
|
*/
|
|
452
|
-
export declare const ASK_CLASSES: readonly ["content_question", "hook", "ancestor", "unverifiable_delete", "transcript_write", "org_rule", "governance_unavailable", "rule_unreadable", "ask_rule", "denial_limit_fallback", "classifier_no_ruling", "requires_user_interaction", "operator_forced", "shell_floor", "write_protection_unresolved", "probe", "write_protection", "tool_marks_mcp", "tool_marks_a2a", "policy_rra", "artifact_shared_read", "mode", "unstated", "ordinary"];
|
|
457
|
+
export declare const ASK_CLASSES: readonly ["content_question", "hook", "ancestor", "dangerous_delete", "unverifiable_delete", "transcript_write", "org_rule", "governance_unavailable", "rule_unreadable", "ask_rule", "denial_limit_fallback", "classifier_no_ruling", "requires_user_interaction", "operator_forced", "shell_floor", "write_protection_unresolved", "probe", "write_protection", "tool_marks_mcp", "tool_marks_a2a", "policy_rra", "artifact_shared_read", "mode", "unstated", "ordinary"];
|
|
453
458
|
export type AskClass = (typeof ASK_CLASSES)[number];
|
|
454
459
|
/** Membership test for {@link AskClass} — the word crosses the wire on `tool_end.gate.disposition`. */
|
|
455
460
|
export declare function isAskClass(v: unknown): v is AskClass;
|
|
@@ -457,7 +462,8 @@ export declare function isAskClass(v: unknown): v is AskClass;
|
|
|
457
462
|
export interface ModeArbitrationRow {
|
|
458
463
|
/** The ask still reaches a person under `bypassPermissions` (the reference implementation's `bypassImmune`). */
|
|
459
464
|
readonly bypassImmune: boolean;
|
|
460
|
-
/** The auto-mode classifier may answer it
|
|
465
|
+
/** The auto-mode classifier may answer it — as the class stands on its own: the folded row of an ask
|
|
466
|
+
* ({@link foldArbitrationRow}) also takes the read face the ask was raised under. */
|
|
461
467
|
readonly classifierEligible: boolean;
|
|
462
468
|
/** The sandbox-admission leg may admit it (containment holds the effect). */
|
|
463
469
|
readonly sandboxAdmission: boolean;
|
|
@@ -473,6 +479,8 @@ export interface ModeArbitrationRow {
|
|
|
473
479
|
* sandbox columns are the fold of the two former per-word lane tables (only `probe` moved: the classifier may
|
|
474
480
|
* now answer it — the read boundary's unestablishable spelling is fed to it as a deny-table row instead), and
|
|
475
481
|
* `ruleMayClear` restates {@link ruleClearanceOf}'s conclusion per class (a real-approval bit is never rule-clearable).
|
|
482
|
+
* `probe`'s `classifierEligible` is the OPEN read face's reading: a narrowed (`roots`) face withholds the classifier from the class,
|
|
483
|
+
* folded where the ask's row is ({@link foldArbitrationRow}).
|
|
476
484
|
* `mode` is the permission mode's OWN question (`plan`'s two sentences, `decisionReason: "mode"`): the reference
|
|
477
485
|
* implementation puts it to a person past every allow rule and never to an automatic lane, and its bypass conversion
|
|
478
486
|
* releases it like any other non-immune ask. `unstated` is the reference implementation's passthrough conversion: an allow
|
|
@@ -503,6 +511,13 @@ export declare const MODE_ARBITRATION: {
|
|
|
503
511
|
readonly ruleMayClear: false;
|
|
504
512
|
readonly hookAskFloor: false;
|
|
505
513
|
};
|
|
514
|
+
readonly dangerous_delete: {
|
|
515
|
+
readonly bypassImmune: true;
|
|
516
|
+
readonly classifierEligible: false;
|
|
517
|
+
readonly sandboxAdmission: false;
|
|
518
|
+
readonly ruleMayClear: false;
|
|
519
|
+
readonly hookAskFloor: true;
|
|
520
|
+
};
|
|
506
521
|
readonly unverifiable_delete: {
|
|
507
522
|
readonly bypassImmune: true;
|
|
508
523
|
readonly classifierEligible: true;
|
|
@@ -683,6 +698,8 @@ export interface AskClassFacts extends AskOriginFacts {
|
|
|
683
698
|
readonly toolMarkSources?: ReadonlySet<ToolMarkSource>;
|
|
684
699
|
readonly a2aIsolatePeers?: boolean;
|
|
685
700
|
readonly ancestorClasses?: ReadonlySet<AskClass>;
|
|
701
|
+
/** The read face the leg this ask was raised on resolved ({@link ContainmentFace}); absent ⇒ none resolved (a leg with no hands). */
|
|
702
|
+
readonly readFace?: ContainmentFace;
|
|
686
703
|
}
|
|
687
704
|
/** The classification the arbitration reads: the whole set, the disclosed word (the set's first member in
|
|
688
705
|
* {@link ASK_CLASSES} order) and the row folded across the set. */
|
|
@@ -718,8 +735,10 @@ export declare function decisionAskClassesOf(d: Extract<PermissionResult, {
|
|
|
718
735
|
}>): Set<AskClass>;
|
|
719
736
|
/** Fold a class set onto one row: `bypassImmune` and `hookAskFloor` by OR (any immune member keeps the ask; any floored
|
|
720
737
|
* member floors a hook's ask), the three permissive switches by AND (one refusing member withholds the lane). An empty
|
|
721
|
-
* set is `ordinary`.
|
|
722
|
-
|
|
738
|
+
* set is `ordinary`. `readFace` is the face the ask was raised under: `roots` takes the classifier's lane from a set that
|
|
739
|
+
* holds a class the narrowed face withholds from it — the one place the face enters, so every reader of `classifierEligible`
|
|
740
|
+
* reads it already folded. */
|
|
741
|
+
export declare function foldArbitrationRow(classes: ReadonlySet<AskClass>, readFace?: ContainmentFace): ModeArbitrationRow;
|
|
723
742
|
/**
|
|
724
743
|
* The ONE class derivation: the decision half ({@link decisionAskClassesOf}) joined with the gate's own facts —
|
|
725
744
|
* the question tool, an ancestor's mark, the org layer's two arms, the persisted lane's unreadable call, the tool
|