@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.
Files changed (98) hide show
  1. package/CHANGELOG.md +245 -0
  2. package/dist/agents/subagent.d.ts +2 -1
  3. package/dist/agents/subagent.js +33 -14
  4. package/dist/agents/tool-filter.d.ts +65 -4
  5. package/dist/agents/tool-filter.js +127 -4
  6. package/dist/brain/errors.d.ts +23 -2
  7. package/dist/brain/errors.js +26 -2
  8. package/dist/brain/stream-engine.d.ts +2 -1
  9. package/dist/brain/stream-engine.js +10 -9
  10. package/dist/core/agent-definition.d.ts +2 -2
  11. package/dist/core/allow-entry.d.ts +25 -1
  12. package/dist/core/allow-entry.js +29 -4
  13. package/dist/core/ask-origin.d.ts +23 -4
  14. package/dist/core/ask-origin.js +10 -4
  15. package/dist/core/checkpoint-store.d.ts +28 -15
  16. package/dist/core/checkpoint-store.js +4 -13
  17. package/dist/core/decision-receipt.js +1 -1
  18. package/dist/core/effective-path-target.d.ts +7 -6
  19. package/dist/core/effective-path-target.js +3 -7
  20. package/dist/core/engine-notice.d.ts +33 -14
  21. package/dist/core/engine-notice.js +14 -7
  22. package/dist/core/gate-lanes.js +2 -0
  23. package/dist/core/gate-outcome.d.ts +5 -0
  24. package/dist/core/gate-outcome.js +2 -0
  25. package/dist/core/governance-codes.d.ts +4 -3
  26. package/dist/core/governance-codes.js +1 -0
  27. package/dist/core/hooks.d.ts +2 -0
  28. package/dist/core/memory-engine/content-origin.d.ts +0 -34
  29. package/dist/core/memory-engine/content-origin.js +9 -4
  30. package/dist/core/memory-engine/engine.d.ts +2 -2
  31. package/dist/core/memory-engine/engine.js +7 -14
  32. package/dist/core/memory-engine/file-backend.d.ts +2 -2
  33. package/dist/core/memory-engine/file-backend.js +5 -7
  34. package/dist/core/memory-engine/layout.d.ts +41 -4
  35. package/dist/core/memory-engine/layout.js +35 -5
  36. package/dist/core/permission-rules.d.ts +4 -0
  37. package/dist/core/permission-rules.js +5 -2
  38. package/dist/core/real-approval.d.ts +9 -6
  39. package/dist/core/real-approval.js +5 -5
  40. package/dist/core/runner/child-tree.d.ts +13 -0
  41. package/dist/core/runner/child-tree.js +3 -0
  42. package/dist/core/runner/contracts.d.ts +9 -18
  43. package/dist/core/runner/prepare-ask-lane.d.ts +2 -0
  44. package/dist/core/runner/prepare-ask-lane.js +2 -2
  45. package/dist/core/runner/prepare-config-doors.js +3 -1
  46. package/dist/core/runner/prepare-execution-env.js +2 -1
  47. package/dist/core/runner/prepare-gate-stations.d.ts +3 -0
  48. package/dist/core/runner/prepare-gate-stations.js +2 -1
  49. package/dist/core/runner/prepare-listings.d.ts +4 -4
  50. package/dist/core/runner/prepare-listings.js +2 -22
  51. package/dist/core/runner/prepare-park-ask.d.ts +2 -0
  52. package/dist/core/runner/prepare-park-ask.js +2 -2
  53. package/dist/core/runner/prepare-policy-chain.d.ts +2 -0
  54. package/dist/core/runner/prepare-policy-chain.js +4 -2
  55. package/dist/core/runner/prepare-task.js +10 -6
  56. package/dist/core/runner/resume-policy-outcome.js +2 -1
  57. package/dist/core/runner/run-leg.js +1 -1
  58. package/dist/core/runner/run-notification-lane.js +2 -2
  59. package/dist/core/runner/runtask.d.ts +2 -0
  60. package/dist/core/runner/runtask.js +3 -5
  61. package/dist/core/runner/steer-admission.d.ts +10 -11
  62. package/dist/core/runner/steer-admission.js +6 -8
  63. package/dist/core/runner/stream-steer-verb.d.ts +5 -4
  64. package/dist/core/runner/stream-steer-verb.js +8 -10
  65. package/dist/core/runner/sub-agent-deny-lists.d.ts +32 -0
  66. package/dist/core/runner/sub-agent-deny-lists.js +121 -0
  67. package/dist/core/runner/synthetic-tools.d.ts +1 -1
  68. package/dist/core/runner/synthetic-tools.js +3 -2
  69. package/dist/core/session-policy-store.d.ts +6 -2
  70. package/dist/core/session-policy-store.js +2 -1
  71. package/dist/core/skill-grant.d.ts +10 -2
  72. package/dist/core/skill-grant.js +20 -4
  73. package/dist/core/steer-replay.d.ts +46 -0
  74. package/dist/core/steer-replay.js +22 -0
  75. package/dist/core/task-event.d.ts +3 -2
  76. package/dist/core/task-notification.d.ts +12 -1
  77. package/dist/core/task-notification.js +13 -1
  78. package/dist/core/task-spec.d.ts +5 -3
  79. package/dist/core/task-stream.d.ts +12 -6
  80. package/dist/core/tool-errors.d.ts +2 -2
  81. package/dist/core/tool-policy.d.ts +15 -4
  82. package/dist/core/tool-policy.js +9 -13
  83. package/dist/core/tool-spec.d.ts +20 -2
  84. package/dist/core/types.d.ts +2 -2
  85. package/dist/core/types.js +1 -1
  86. package/dist/index.d.ts +7 -4
  87. package/dist/index.js +7 -4
  88. package/dist/orchestration/workflow-types.d.ts +5 -0
  89. package/dist/orchestration/workflow.d.ts +3 -2
  90. package/dist/orchestration/workflow.js +16 -7
  91. package/dist/tools/fs/removal-targets.d.ts +82 -0
  92. package/dist/tools/fs/removal-targets.js +236 -0
  93. package/dist/tools/fs/shell-read-write-targets.d.ts +27 -0
  94. package/dist/tools/fs/shell-read-write-targets.js +1 -1
  95. package/package.json +1 -1
  96. package/test/export-surface.snapshot.json +43 -5
  97. package/wire-changes/core@7.44.0.json +469 -0
  98. 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. The asymmetric prepare-time fail-loud for
20
- * `TaskSpec.agents` DENY entries is a deliberate sema extension (no upstream deny-list exists): an
21
- * allow-typo silently narrows (safe direction), a deny-typo silently widens (unacceptable).
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 = allowTools !== undefined && !allowTools.includes("*") ? new Set(allowTools) : undefined;
3
- const deny = denyTools && denyTools.length > 0 ? new Set(denyTools) : undefined;
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 = allowTools !== undefined && !allowTools.includes("*") ? new Set(allowTools) : undefined;
9
- const deny = denyTools && denyTools.length > 0 ? new Set(denyTools) : undefined;
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) => {
@@ -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 two forms: the runtime's own shell sentence, or a
178
- * `cause` that is a transport fact rather than a parse verdict. A `TypeError` with neither was never built.
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;
@@ -138,8 +138,13 @@ function hasStaleConnectionCode(e) {
138
138
  }
139
139
  return false;
140
140
  }
141
- const DETERMINISTIC_CONNECT_CODES = new Set(["ECONNREFUSED", "ENOTFOUND"]);
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 walkConnectFailure(e, { left: CONNECT_GRAPH_NODE_BUDGET }) === "deterministic" ? "deterministic" : "indeterminate";
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; see `errors.ts` `classifyConnectFailure`).
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, classifyConnectFailure, classifyFetchThrow, classifyHttp, describeNetworkError, isProviderEchoShape, namesTheFailure, readProviderErrorCode, readProviderRequestId, refusedTheRequest } from "./errors.js";
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 deterministicConnect = netErr !== undefined && classifyConnectFailure(netErr) === "deterministic";
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 reconnectDetail = errClass !== "connect_refused"
556
- ? "connection lost, reconnecting"
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
- throw new BrainError("network", describeNetworkError(netErr), undefined, { apiError: true });
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). See {@link allowTools} for the per-agent delegation-tool note. */
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` reason: the judgment's word, and the spelling to write instead when there is one. */
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
@@ -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 = voided ? { inert: "wildcard_all" } : readAllowEntry(entry, behavior, roster);
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 `${inlineUntrusted(JSON.stringify(e.entry), 122)} (${why})`;
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
- export declare function foldArbitrationRow(classes: ReadonlySet<AskClass>): ModeArbitrationRow;
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