@sema-agent/core 7.32.1 → 7.33.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +67 -1
- package/dist/brain/anthropic.d.ts +1 -1
- package/dist/brain/anthropic.js +4 -1
- package/dist/brain/open-responses.d.ts +2 -1
- package/dist/brain/open-responses.js +4 -1
- package/dist/brain/openai.d.ts +2 -1
- package/dist/brain/openai.js +4 -1
- package/dist/brain/stream-engine.d.ts +8 -7
- package/dist/brain/stream-engine.js +112 -44
- package/dist/brain/timeout.d.ts +122 -53
- package/dist/brain/timeout.js +49 -5
- package/dist/brain/transport.d.ts +13 -0
- package/dist/brain/transport.js +62 -0
- package/dist/core/runner/prepare-task.js +2 -2
- package/dist/core/runner/prepare-wiring-manifest.js +2 -1
- package/dist/core/secret-env.d.ts +13 -4
- package/dist/core/secret-env.js +17 -8
- package/dist/core/task-event.d.ts +7 -5
- package/dist/core/task-registry.d.ts +1 -7
- package/dist/core/task-registry.js +6 -10
- package/dist/core/tool-policy.d.ts +12 -12
- package/dist/core/wiring-manifest.d.ts +27 -0
- package/dist/core/wiring-manifest.js +11 -0
- package/dist/engine/llm/types.d.ts +5 -5
- package/dist/orchestration/workflow.d.ts +4 -4
- package/dist/orchestration/workflow.js +2 -2
- package/package.json +6 -3
- package/wire-changes/core@7.33.0.json +351 -0
- package/wire-changes/core@7.33.1.json +237 -0
package/dist/brain/timeout.d.ts
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import type { StreamFn } from "../internal/llm.js";
|
|
2
2
|
/**
|
|
3
|
-
* Tiered timeouts (
|
|
4
|
-
* it
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* Tiered timeouts (design/17 §5B; re-cut along the segments of a call in 7.33.0). The task-level
|
|
4
|
+
* `timeoutSec` only bounds the *whole* run; it cannot tell apart a gateway that never completes the
|
|
5
|
+
* handshake, one that accepts the request and then holds the response headers while it works, one
|
|
6
|
+
* that opens the stream and never emits a token, and one that goes quiet mid-answer. Four clocks,
|
|
7
|
+
* one per segment, each named for the segment it bounds:
|
|
7
8
|
*
|
|
8
|
-
* - **connect**: `fetch`
|
|
9
|
-
*
|
|
9
|
+
* - **connect**: TCP + TLS establishment inside `fetch` — the transport's own clock (undici's 10s by
|
|
10
|
+
* default), tuned on the process dispatcher the host installs; not a knob here (`transport.ts`).
|
|
11
|
+
* - **first-byte**: the `fetch()` call → response headers. A stall here is this process cancelling
|
|
12
|
+
* its own request at a deadline it chose, and is announced as such.
|
|
13
|
+
* - **first-token**: headers → the first content delta (a thinking delta counts).
|
|
14
|
+
* - **idle**: silence between deltas once output has started.
|
|
10
15
|
*
|
|
11
|
-
*
|
|
16
|
+
* Every field resolves through the same chain: construction config (an explicit `0` is OFF) >
|
|
17
|
+
* per-call advisory (`StreamOptions.stallTimeouts`) > the engine default constant below.
|
|
12
18
|
*/
|
|
13
19
|
/**
|
|
14
20
|
* The largest delay a host timer can actually wait for. Past this `setTimeout` does not schedule far in
|
|
@@ -28,55 +34,117 @@ export declare const MAX_TIMER_DELAY_MS = 2147483647;
|
|
|
28
34
|
*/
|
|
29
35
|
export declare function resolveStallTimeoutMs(value: number | undefined, knob: string): number | undefined;
|
|
30
36
|
/**
|
|
31
|
-
* The engine's
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
37
|
+
* The engine's DEFAULT watchdog values, one per segment of a model call — the third and last level of
|
|
38
|
+
* the chain every brain reads (`BrainTimeoutConfig` field > per-call advisory `StreamOptions.stallTimeouts`
|
|
39
|
+
* > these). The Runner supplies these same numbers as the advisory on every call, so a Brain supplied by
|
|
40
|
+
* the deployment (BYOM) is told what the engine would arm; a first-party brain arms them itself when
|
|
41
|
+
* neither a config field nor an advisory names the segment. A silent call is therefore always bounded by
|
|
42
|
+
* something finer than the hour-scale {@link BRAIN_CALL_GUARDRAIL_DEFAULT_MS} (a measured production
|
|
43
|
+
* frame once sat 680s with ZERO deltas) unless a deployment switched the watchdog for that segment off
|
|
44
|
+
* by name — an explicit `0` is the only spelling of OFF. The TCP + TLS connect has no row here: it is
|
|
45
|
+
* the transport's own clock (`transport.ts`), not a segment the engine can observe. 量纲:
|
|
46
|
+
* - first-byte 300s: the `fetch()` call → response headers. A self-hosted gateway that runs its
|
|
47
|
+
* prefill BEFORE writing headers is silent here for minutes and healthy; the 30s "connect" clock
|
|
48
|
+
* this replaces cut that prefill and re-sent the same body ten times. 300s is the reference
|
|
49
|
+
* implementation's window for a non-first-party route; the window grows with the request body
|
|
50
|
+
* ({@link firstByteWindow}) and the one retry is widened ({@link FIRST_BYTE_RETRY_WINDOW_FACTOR}).
|
|
41
51
|
* - first-token 600s: bounds the headers→first-delta wait, NOT the model's thought (the first
|
|
42
|
-
* THINKING delta already counts as the first token, design/17 O5
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* one a deployment gets by supplying no `fetchImpl` — undici destroys a body that has produced no
|
|
54
|
-
* bytes for 300s (`bodyTimeout`, default `300e3`), so byte-silence past 300s ends the attempt
|
|
55
|
-
* there and arrives as a read rejection reported `transport`. That report is CORRECT (the
|
|
56
|
-
* transport really did cut it) and the seat to change it exists: `fetchImpl` on both brain configs
|
|
57
|
-
* takes any `fetch`, so a deployment needing the full 600s passes one built on an undici
|
|
58
|
-
* `Agent`/`Dispatcher` with its own `bodyTimeout`. Named here so the next reader who measures ~300s
|
|
59
|
-
* of tolerance does not conclude this number is a lie.
|
|
60
|
-
* {@link import("../orchestration/workflow.js").WORKFLOW_AGENT_STALL_MS} is DERIVED from
|
|
61
|
-
* {@link STALL_CONNECT_MS} + this value, so the coarse agent-restart cannot sit UNDER a single
|
|
62
|
-
* silent provider call. Move this number and that one follows. It is a floor, not
|
|
63
|
-
* a guarantee of ordering — see that constant's own comment for the cases that still invert.
|
|
64
|
-
* - idle 90s: mid-stream silence after output started — a healthy stream emits steadily
|
|
65
|
-
* (comfortably above any provider keep-alive cadence).
|
|
52
|
+
* THINKING delta already counts as the first token, design/17 O5), so a slow reasoner is safe. A
|
|
53
|
+
* minutes-scale bound is FALSE for a self-hosted server working through a long prefill AFTER
|
|
54
|
+
* headers: it emits nothing for 2–3 minutes while healthy, and cancelling it burns the prefill and
|
|
55
|
+
* re-sends a request that burns it again. The default transport keeps NO clock over this segment
|
|
56
|
+
* (the request carries `bodyTimeout: 0`), so this number is the bound actually granted.
|
|
57
|
+
* {@link import("../orchestration/workflow.js").WORKFLOW_AGENT_STALL_MS} is DERIVED from the
|
|
58
|
+
* first-byte lane at {@link STALL_FIRST_BYTE_MS} ({@link firstByteLaneKnobMs}) + this value, so the coarse agent-restart cannot sit UNDER a single
|
|
59
|
+
* silent provider call (a floor, not a guarantee of ordering — see that constant's comment).
|
|
60
|
+
* - idle 300s: mid-stream silence after output started — the reference implementation's idle floor
|
|
61
|
+
* (a self-hosted server can stall for minutes between deltas while a server-side tool runs; a provider
|
|
62
|
+
* keep-alive comment is not a delta and does not count).
|
|
66
63
|
* ADVISORY: a brain whose construction-time {@link BrainTimeoutConfig} sets the corresponding field
|
|
67
64
|
* keeps it (deployment sovereignty; see the `StreamOptions.stallTimeouts` priority chain).
|
|
68
65
|
*/
|
|
69
|
-
export declare const
|
|
66
|
+
export declare const STALL_FIRST_BYTE_MS = 300000;
|
|
70
67
|
export declare const STALL_FIRST_TOKEN_MS = 600000;
|
|
71
|
-
export declare const STALL_IDLE_MS =
|
|
68
|
+
export declare const STALL_IDLE_MS = 300000;
|
|
69
|
+
/**
|
|
70
|
+
* The retired names of the header-wait bound, each with the name that replaced it: the construction key and
|
|
71
|
+
* the per-call advisory key. The type face no longer has them, but a type only refuses a LITERAL — a
|
|
72
|
+
* spread object (`{ ...timeouts }`) or a JS caller still delivers the key, and a knob that arrives and is
|
|
73
|
+
* then ignored is a deployment setting silently doing nothing. So their arrival is refused, naming the
|
|
74
|
+
* replacement: a tripwire, not a second spelling (nothing reads the value).
|
|
75
|
+
*/
|
|
76
|
+
export declare const RETIRED_TIMEOUT_KNOBS: {
|
|
77
|
+
readonly construction: {
|
|
78
|
+
readonly connectTimeoutMs: "firstByteTimeoutMs";
|
|
79
|
+
};
|
|
80
|
+
readonly advisory: {
|
|
81
|
+
readonly connectMs: "firstByteMs";
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
/** Refuse a retired timeout key present on `holder` (see {@link RETIRED_TIMEOUT_KNOBS}); `where` names the
|
|
85
|
+
* object in the message (`"createOpenAIBrain config"`, `"StreamOptions.stallTimeouts"`). */
|
|
86
|
+
export declare function refuseRetiredTimeoutKnobs(holder: object | undefined, level: keyof typeof RETIRED_TIMEOUT_KNOBS, where: string): void;
|
|
87
|
+
/** The first-byte window grows with the request body: one second per this many bytes, rounded up (the
|
|
88
|
+
* reference implementation's allowance) — a large context on a slow uplink is still UPLOADING when a
|
|
89
|
+
* fixed window would call the gateway silent. */
|
|
90
|
+
export declare const FIRST_BYTE_UPLOAD_BYTES_PER_SECOND = 32768;
|
|
91
|
+
/** A first-byte stall is re-sent at most this many times by default ({@link BrainTimeoutConfig.firstByteRetries}),
|
|
92
|
+
* out of its OWN budget rather than `maxRetries` (the reference implementation: "normally one retry, not the
|
|
93
|
+
* session budget"); the shared ladder counts transport failures, and this is not one. */
|
|
94
|
+
export declare const FIRST_BYTE_RETRY_BUDGET = 1;
|
|
95
|
+
/** The first-byte lane's retry budget: absent ⇒ {@link FIRST_BYTE_RETRY_BUDGET}; `0` / `1` are themselves; anything
|
|
96
|
+
* else is refused (`config.stall_timeout_invalid`), never folded to the default or to `0`. */
|
|
97
|
+
export declare function resolveFirstByteRetries(value: unknown): 0 | 1;
|
|
98
|
+
/** The retry's knob is the first times this — the reference implementation widens its retry to a second
|
|
99
|
+
* knob's default (599s against a 300s first window); one knob here keeps that ratio instead. */
|
|
100
|
+
export declare const FIRST_BYTE_RETRY_WINDOW_FACTOR = 2;
|
|
101
|
+
/** The ONE resolution every watchdog knob reads: construction config (an explicit `0` is OFF) > per-call
|
|
102
|
+
* advisory > the engine default. A bad number at either level is refused ({@link resolveStallTimeoutMs});
|
|
103
|
+
* only an ABSENT value falls through. */
|
|
104
|
+
export declare function resolveWatchdogMs(configured: number | undefined, advisory: number | undefined, engineDefault: number, knob: string): number;
|
|
105
|
+
/** One armed first-byte window, with the two numbers its announcement quotes. */
|
|
106
|
+
export interface FirstByteWindow {
|
|
107
|
+
/** The knob's value for THIS attempt (the configured value, widened on the retry). */
|
|
108
|
+
knobMs: number;
|
|
109
|
+
/** The upload allowance added for the request body, ms (0 for an empty body). */
|
|
110
|
+
allowanceMs: number;
|
|
111
|
+
/** What is actually armed: `knobMs + allowanceMs`, capped at {@link MAX_TIMER_DELAY_MS}. `0` = off. */
|
|
112
|
+
windowMs: number;
|
|
113
|
+
}
|
|
114
|
+
/** The first-byte window for one attempt; `retryIndex` = first-byte stalls already suffered (0 first),
|
|
115
|
+
* each widening the knob by {@link FIRST_BYTE_RETRY_WINDOW_FACTOR}. A knob of `0` (OFF) stays off. */
|
|
116
|
+
export declare function firstByteWindow(knobMs: number | undefined, bodyBytes: number, retryIndex: number): FirstByteWindow;
|
|
117
|
+
/**
|
|
118
|
+
* The most a first-byte lane armed at `knobMs` waits on the gateway in total before it gives up: the probe
|
|
119
|
+
* window and every widened retry its budget allows ({@link FIRST_BYTE_RETRY_BUDGET},
|
|
120
|
+
* {@link FIRST_BYTE_RETRY_WINDOW_FACTOR}) — read off {@link firstByteWindow}, the same function that arms
|
|
121
|
+
* each attempt, so the sum cannot drift from what is armed. The knob part only: the upload allowance
|
|
122
|
+
* depends on the request body and the backoff between attempts is not a wait on the gateway, so the lane's
|
|
123
|
+
* real wall time is this plus both — a LOWER bound.
|
|
124
|
+
*/
|
|
125
|
+
export declare function firstByteLaneKnobMs(knobMs: number): number;
|
|
72
126
|
export interface BrainTimeoutConfig {
|
|
73
|
-
/**
|
|
74
|
-
|
|
127
|
+
/**
|
|
128
|
+
* `fetch()` call → response headers, ms: the window a gateway has to START answering (until 7.33.0 a
|
|
129
|
+
* knob named `connectTimeoutMs` bounded this wait; it is gone — the connect clock is the transport's). Absent ⇒
|
|
130
|
+
* {@link STALL_FIRST_BYTE_MS}; the armed window grows with the request body ({@link firstByteWindow});
|
|
131
|
+
* a stall earns ONE retry at a widened window ({@link firstByteRetries}), then fails naming this knob.
|
|
132
|
+
* Fires as a `stall` (this process cancelled its own request), never as a transport fault. `0` ⇒ off.
|
|
133
|
+
*/
|
|
134
|
+
firstByteTimeoutMs?: number;
|
|
135
|
+
/**
|
|
136
|
+
* The first-byte lane's own retry budget: `1` (absent ⇒ {@link FIRST_BYTE_RETRY_BUDGET}) or `0` — the first
|
|
137
|
+
* window's expiry is the call's terminal, for a host whose own failover ladder should take a silent gateway after
|
|
138
|
+
* one window. Independent of `maxRetries` both ways; construction only; any other value fails the call naming this
|
|
139
|
+
* knob. The derived floors ({@link firstByteLaneKnobMs}) read the default.
|
|
140
|
+
*/
|
|
141
|
+
firstByteRetries?: 0 | 1;
|
|
75
142
|
/**
|
|
76
143
|
* After the stream opens, cancel it if no content delta (text / thinking / tool input) arrives
|
|
77
144
|
* within this many ms → `BrainError("network", "first-token timeout")`. For reasoning models the
|
|
78
145
|
* **first thinking delta also counts** as the first token, so a long-thinking model isn't killed
|
|
79
|
-
* for being slow to its first *visible* word (design/17 O5).
|
|
146
|
+
* for being slow to its first *visible* word (design/17 O5). Absent ⇒ {@link STALL_FIRST_TOKEN_MS};
|
|
147
|
+
* `0` ⇒ off.
|
|
80
148
|
*/
|
|
81
149
|
firstTokenTimeoutMs?: number;
|
|
82
150
|
/**
|
|
@@ -84,18 +152,19 @@ export interface BrainTimeoutConfig {
|
|
|
84
152
|
* After the first delta, re-armed on every delta; if the stream then goes silent for this many ms
|
|
85
153
|
* (a gateway that hung mid-output) → `reader.cancel()` + `BrainError("network", "stream idle timeout")`.
|
|
86
154
|
* Complements `firstTokenTimeoutMs`: that one tolerates a long *first* token (reasoning), this one
|
|
87
|
-
* catches a stall *after* output has started, where a healthy stream emits steadily.
|
|
155
|
+
* catches a stall *after* output has started, where a healthy stream emits steadily. Absent ⇒
|
|
156
|
+
* {@link STALL_IDLE_MS}; `0` ⇒ off.
|
|
88
157
|
*/
|
|
89
158
|
idleTimeoutMs?: number;
|
|
90
159
|
}
|
|
91
160
|
/**
|
|
92
|
-
* A per-
|
|
161
|
+
* A per-attempt abort controller that fires at the first-byte deadline while still forwarding an
|
|
93
162
|
* outer (task) abort. The returned `signal` governs the **whole** request including the streamed body
|
|
94
|
-
* — so once headers arrive, call `clearTimer()` (NOT `dispose()`) to stop the
|
|
95
|
-
*
|
|
96
|
-
*
|
|
163
|
+
* — so once headers arrive, call `clearTimer()` (NOT `dispose()`) to stop the deadline while keeping
|
|
164
|
+
* the outer-abort link alive for the body read; call `dispose()` only when the request is fully done
|
|
165
|
+
* or being discarded.
|
|
97
166
|
*/
|
|
98
|
-
export declare function
|
|
167
|
+
export declare function createAttemptController(firstByteWindowMs: number | undefined, outerSignal: AbortSignal | undefined): {
|
|
99
168
|
signal: AbortSignal;
|
|
100
169
|
timedOut: () => boolean;
|
|
101
170
|
clearTimer: () => void;
|
|
@@ -110,7 +179,7 @@ export declare function createConnectController(connectTimeoutMs: number | undef
|
|
|
110
179
|
* Default ceiling on how long the ENGINE waits for a single brain call (ruled 2026-08-03: one hour).
|
|
111
180
|
* Deliberately far above any legitimate call: this is a wedged-run backstop, not a latency policy —
|
|
112
181
|
* a deployment that wants a tight per-call bound sets the fine tier above ({@link BrainTimeoutConfig},
|
|
113
|
-
* or the advisory {@link
|
|
182
|
+
* or the advisory {@link STALL_FIRST_BYTE_MS} family the engine supplies by default). All of these clocks
|
|
114
183
|
* run in parallel with the task's own `limits.timeoutSec`; whichever expires first owns the run.
|
|
115
184
|
*/
|
|
116
185
|
export declare const BRAIN_CALL_GUARDRAIL_DEFAULT_MS = 3600000;
|
package/dist/brain/timeout.js
CHANGED
|
@@ -14,10 +14,54 @@ export function resolveStallTimeoutMs(value, knob) {
|
|
|
14
14
|
}
|
|
15
15
|
return value;
|
|
16
16
|
}
|
|
17
|
-
export const
|
|
17
|
+
export const STALL_FIRST_BYTE_MS = 300_000;
|
|
18
18
|
export const STALL_FIRST_TOKEN_MS = 600_000;
|
|
19
|
-
export const STALL_IDLE_MS =
|
|
20
|
-
export
|
|
19
|
+
export const STALL_IDLE_MS = 300_000;
|
|
20
|
+
export const RETIRED_TIMEOUT_KNOBS = {
|
|
21
|
+
construction: { connectTimeoutMs: "firstByteTimeoutMs" },
|
|
22
|
+
advisory: { connectMs: "firstByteMs" },
|
|
23
|
+
};
|
|
24
|
+
export function refuseRetiredTimeoutKnobs(holder, level, where) {
|
|
25
|
+
if (holder === undefined || holder === null)
|
|
26
|
+
return;
|
|
27
|
+
for (const [retired, replacement] of Object.entries(RETIRED_TIMEOUT_KNOBS[level])) {
|
|
28
|
+
if (!(retired in holder))
|
|
29
|
+
continue;
|
|
30
|
+
const e = new Error(`${where}: \`${retired}\` was removed in 7.33.0 and is refused rather than ignored — the fetch()→response-headers wait it bounded is \`${replacement}\` (0 = off); the TCP + TLS connect is the transport's own clock, tuned on the process dispatcher (setGlobalDispatcher(new Agent({ connect: { timeout } })))`);
|
|
31
|
+
e.code = "config.stall_timeout_retired";
|
|
32
|
+
throw e;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export const FIRST_BYTE_UPLOAD_BYTES_PER_SECOND = 32_768;
|
|
36
|
+
export const FIRST_BYTE_RETRY_BUDGET = 1;
|
|
37
|
+
export function resolveFirstByteRetries(value) {
|
|
38
|
+
if (value === undefined)
|
|
39
|
+
return FIRST_BYTE_RETRY_BUDGET;
|
|
40
|
+
if (value === 0 || value === 1)
|
|
41
|
+
return value;
|
|
42
|
+
const shown = typeof value === "string" ? JSON.stringify(value) : typeof value === "bigint" ? `${value}n` : typeof value === "object" && value !== null ? "an object" : String(value);
|
|
43
|
+
const e = new Error(`firstByteRetries must be 0 or 1 (got ${shown}) — 1 re-sends a first-byte stall once at a widened window (the default), 0 ends the call at the first window so the caller's own failover takes it`);
|
|
44
|
+
e.code = "config.stall_timeout_invalid";
|
|
45
|
+
throw e;
|
|
46
|
+
}
|
|
47
|
+
export const FIRST_BYTE_RETRY_WINDOW_FACTOR = 2;
|
|
48
|
+
export function resolveWatchdogMs(configured, advisory, engineDefault, knob) {
|
|
49
|
+
return resolveStallTimeoutMs(configured ?? advisory, knob) ?? engineDefault;
|
|
50
|
+
}
|
|
51
|
+
export function firstByteWindow(knobMs, bodyBytes, retryIndex) {
|
|
52
|
+
if (knobMs === undefined || knobMs <= 0)
|
|
53
|
+
return { knobMs: 0, allowanceMs: 0, windowMs: 0 };
|
|
54
|
+
const widened = knobMs * FIRST_BYTE_RETRY_WINDOW_FACTOR ** Math.max(0, retryIndex);
|
|
55
|
+
const allowanceMs = Math.ceil(Math.max(0, bodyBytes) / FIRST_BYTE_UPLOAD_BYTES_PER_SECOND) * 1000;
|
|
56
|
+
return { knobMs: widened, allowanceMs, windowMs: Math.min(widened + allowanceMs, MAX_TIMER_DELAY_MS) };
|
|
57
|
+
}
|
|
58
|
+
export function firstByteLaneKnobMs(knobMs) {
|
|
59
|
+
let total = 0;
|
|
60
|
+
for (let retryIndex = 0; retryIndex <= FIRST_BYTE_RETRY_BUDGET; retryIndex++)
|
|
61
|
+
total += firstByteWindow(knobMs, 0, retryIndex).knobMs;
|
|
62
|
+
return total;
|
|
63
|
+
}
|
|
64
|
+
export function createAttemptController(firstByteWindowMs, outerSignal) {
|
|
21
65
|
const ac = new AbortController();
|
|
22
66
|
let timedOut = false;
|
|
23
67
|
const onAbort = () => ac.abort(outerSignal?.reason);
|
|
@@ -28,11 +72,11 @@ export function createConnectController(connectTimeoutMs, outerSignal) {
|
|
|
28
72
|
outerSignal.addEventListener("abort", onAbort, { once: true });
|
|
29
73
|
}
|
|
30
74
|
let timer;
|
|
31
|
-
if (
|
|
75
|
+
if (firstByteWindowMs && firstByteWindowMs > 0) {
|
|
32
76
|
timer = setTimeout(() => {
|
|
33
77
|
timedOut = true;
|
|
34
78
|
ac.abort();
|
|
35
|
-
},
|
|
79
|
+
}, firstByteWindowMs);
|
|
36
80
|
}
|
|
37
81
|
const clearTimer = () => {
|
|
38
82
|
if (timer)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Dispatcher } from "undici";
|
|
2
|
+
/**
|
|
3
|
+
* The one dispatcher every first-party brain hands its fetch: the sending members are the engine's
|
|
4
|
+
* ({@link SENDING_MEMBERS}); every other member — state, lifecycle, events — is the host's, read when
|
|
5
|
+
* accessed ({@link hostMember}), so nothing a fetch starts reading has to be added to a list here.
|
|
6
|
+
* Stateless; owns no sockets.
|
|
7
|
+
*/
|
|
8
|
+
export declare const ENGINE_DISPATCHER: Dispatcher;
|
|
9
|
+
/** The `fetch` a brain sends its requests through: the caller's `fetchImpl` (else Node's `fetch`), each
|
|
10
|
+
* request carrying {@link ENGINE_DISPATCHER} in its init. */
|
|
11
|
+
export declare function resolveFetch(config: {
|
|
12
|
+
fetchImpl?: typeof fetch;
|
|
13
|
+
}): typeof fetch;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Dispatcher, getGlobalDispatcher } from "undici";
|
|
2
|
+
import UnwrapHandler from "undici/lib/handler/unwrap-handler.js";
|
|
3
|
+
import WrapHandler from "undici/lib/handler/wrap-handler.js";
|
|
4
|
+
function handlerApi(handler) {
|
|
5
|
+
return typeof handler.onRequestStart === "function" ? 2 : 1;
|
|
6
|
+
}
|
|
7
|
+
function inSlotApi(api, handler) {
|
|
8
|
+
return api === 1 ? UnwrapHandler.unwrap(handler) : WrapHandler.wrap(handler);
|
|
9
|
+
}
|
|
10
|
+
function slot(version) {
|
|
11
|
+
const held = Reflect.get(globalThis, Symbol.for(`undici.globalDispatcher.${String(version)}`));
|
|
12
|
+
return isDispatcher(held) ? held : undefined;
|
|
13
|
+
}
|
|
14
|
+
function isDispatcher(value) {
|
|
15
|
+
return typeof value === "object" && value !== null && typeof Reflect.get(value, "dispatch") === "function";
|
|
16
|
+
}
|
|
17
|
+
function hostDispatcher(callerApi) {
|
|
18
|
+
const own = callerApi === 2 ? slot(2) : undefined;
|
|
19
|
+
return own !== undefined ? { dispatcher: own, api: 2 } : { dispatcher: slot(1) ?? getGlobalDispatcher(), api: 1 };
|
|
20
|
+
}
|
|
21
|
+
function hostDispatchers() {
|
|
22
|
+
const out = [];
|
|
23
|
+
for (const d of [slot(2), slot(1), getGlobalDispatcher()])
|
|
24
|
+
if (d !== undefined && !out.includes(d))
|
|
25
|
+
out.push(d);
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
function hostMember(key) {
|
|
29
|
+
const hosts = hostDispatchers();
|
|
30
|
+
for (const host of hosts) {
|
|
31
|
+
const value = Reflect.get(host, key, host);
|
|
32
|
+
if (value)
|
|
33
|
+
return typeof value === "function" ? value.bind(host) : value;
|
|
34
|
+
}
|
|
35
|
+
const newest = hosts[0];
|
|
36
|
+
return newest === undefined ? undefined : Reflect.get(newest, key, newest);
|
|
37
|
+
}
|
|
38
|
+
const CALLER_API = Symbol("engine.callerDispatcherApi");
|
|
39
|
+
class EngineDispatcher extends Dispatcher {
|
|
40
|
+
dispatch(options, handler) {
|
|
41
|
+
const { [CALLER_API]: callerApi, ...rest } = options;
|
|
42
|
+
const host = hostDispatcher(callerApi ?? handlerApi(handler));
|
|
43
|
+
return host.dispatcher.dispatch({ ...rest, headersTimeout: 0, bodyTimeout: 0 }, inSlotApi(host.api, handler));
|
|
44
|
+
}
|
|
45
|
+
compose(...interceptors) {
|
|
46
|
+
const composed = super.compose(...interceptors);
|
|
47
|
+
const chain = composed.dispatch;
|
|
48
|
+
const enter = (options, handler) => {
|
|
49
|
+
const stamped = { ...options, [CALLER_API]: options[CALLER_API] ?? handlerApi(handler) };
|
|
50
|
+
return chain(stamped, handler);
|
|
51
|
+
};
|
|
52
|
+
return new Proxy(composed, { get: (target, key, receiver) => (key === "dispatch" ? enter : Reflect.get(target, key, receiver)) });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const SENDING_MEMBERS = new Set(Reflect.ownKeys(Dispatcher.prototype).filter((key) => key !== "constructor" && key !== "close" && key !== "destroy"));
|
|
56
|
+
export const ENGINE_DISPATCHER = new Proxy(new EngineDispatcher(), {
|
|
57
|
+
get: (engine, key, standIn) => (SENDING_MEMBERS.has(key) ? Reflect.get(engine, key, standIn) : hostMember(key)),
|
|
58
|
+
});
|
|
59
|
+
export function resolveFetch(config) {
|
|
60
|
+
const inner = config.fetchImpl ?? fetch;
|
|
61
|
+
return (input, init) => inner(input, { ...init, dispatcher: ENGINE_DISPATCHER });
|
|
62
|
+
}
|
|
@@ -13,7 +13,7 @@ import { isValidReminderMark, mintReminderMark } from "../reminder-mint.js";
|
|
|
13
13
|
import { refuseInvalidApproverPosture } from "../auto-mode-directive.js";
|
|
14
14
|
import { emitTrace } from "../trace.js";
|
|
15
15
|
import { reservedCollisions, reservedFor } from "../../brain/request-params.js";
|
|
16
|
-
import {
|
|
16
|
+
import { STALL_FIRST_BYTE_MS, STALL_FIRST_TOKEN_MS, STALL_IDLE_MS, withBrainCallGuardrail } from "../../brain/timeout.js";
|
|
17
17
|
import { OFFLOAD_TOOL_NAME } from "../tool-result-store.js";
|
|
18
18
|
import { OUTPUT_TOOL_NAME, createOutputTool } from "./synthetic-tools.js";
|
|
19
19
|
import { compileOutputSchema } from "./strict-output-schema.js";
|
|
@@ -649,7 +649,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
649
649
|
: {}),
|
|
650
650
|
stallTimeoutsPerCall: () => {
|
|
651
651
|
callIssuedAtRef.current = Date.now();
|
|
652
|
-
return {
|
|
652
|
+
return { firstByteMs: STALL_FIRST_BYTE_MS, firstTokenMs: STALL_FIRST_TOKEN_MS, idleMs: STALL_IDLE_MS };
|
|
653
653
|
},
|
|
654
654
|
...(spec.resilience !== undefined ? { resilience: spec.resilience } : {}),
|
|
655
655
|
loopTrace: (step) => {
|
|
@@ -11,7 +11,7 @@ import { inlineUntrusted } from "../untrusted-text.js";
|
|
|
11
11
|
import { mountWriteProtection } from "./write-protection-station.js";
|
|
12
12
|
import { autoRunOnRecommendationEffective } from "./memory-consolidation-driver.js";
|
|
13
13
|
import { GLOBAL_USAGE_KEY, usageRetryAfterMs, windowsGovernCost } from "../usage-window-store.js";
|
|
14
|
-
import { LSP_CONTRACT_ID, countElicitOptIns, deriveLspSeam, deriveWiringManifest, resolveAskSeamForm, resolveDeclaredDurability, resolveElicitSeam, resolveQuestionSeam, resolveSubagentTranscriptTier } from "../wiring-manifest.js";
|
|
14
|
+
import { LSP_CONTRACT_ID, countElicitOptIns, deriveHandsSeam, deriveLspSeam, deriveWiringManifest, resolveAskSeamForm, resolveDeclaredDurability, resolveElicitSeam, resolveQuestionSeam, resolveSubagentTranscriptTier } from "../wiring-manifest.js";
|
|
15
15
|
import { DEFAULT_IRREVERSIBLE_SCOPE } from "./checkpoint-scope.js";
|
|
16
16
|
export function mcpManifestEntries(entries, statuses) {
|
|
17
17
|
return (entries ?? []).map((entry) => {
|
|
@@ -218,6 +218,7 @@ export function prepareWiringManifest(input) {
|
|
|
218
218
|
tools: toolRoster,
|
|
219
219
|
hooks: wiredHookEntries(hooks, spec.hooks !== undefined ? "spec" : "deps", preToolUseObservational),
|
|
220
220
|
...(lspManagerWired ? { lsp: deriveLspSeam(toolRoster.entries.some((e) => e.contract.contractId === LSP_CONTRACT_ID), lspDiagnosticsLive) } : {}),
|
|
221
|
+
hands: deriveHandsSeam(handsEnabled),
|
|
221
222
|
fileHistory: fileHistoryFact,
|
|
222
223
|
});
|
|
223
224
|
peerLaneRefs.askEffective = wiringManifest.ask.effective;
|
|
@@ -18,6 +18,12 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import type { RedactionConfidence, RedactionSource } from "./arg-summary.js";
|
|
20
20
|
import { type EngineNotice } from "./engine-notice.js";
|
|
21
|
+
/** A name table as the exact-name arm reads it: membership of an UPPER-CASED name, given the entry's value when
|
|
22
|
+
* the caller has one. A `Set` of exact names is one (it ignores the value); the exemption side is the union of
|
|
23
|
+
* its two tables. */
|
|
24
|
+
export interface EnvNameTable {
|
|
25
|
+
has(upperName: string, value?: string): boolean;
|
|
26
|
+
}
|
|
21
27
|
/** Which rule classified a key as secret-shaped — the env arm's `kind` vocabulary. */
|
|
22
28
|
export type SecretEnvFindingKind = "suffix-rule" | "namespace-rule" | "exact-name";
|
|
23
29
|
/** One env entry the `scrub` policy dropped: the key (the VALUE is never carried — this record is meant
|
|
@@ -30,14 +36,17 @@ export interface SecretEnvFinding {
|
|
|
30
36
|
}
|
|
31
37
|
/** What the two exact-name lists say about a name, before any shape rule is consulted. */
|
|
32
38
|
export type ExactEnvNameVerdict = "credential" | "exempt" | "unknown";
|
|
33
|
-
/** The exact-name arm of the classification, taking both lists as arguments
|
|
39
|
+
/** The exact-name arm of the classification, taking both lists as arguments (the exemption side is the exact
|
|
40
|
+
* names plus the named families). The credential list is read
|
|
34
41
|
* FIRST: an exemption can never take a name away from it, so a name landing on both lists resolves to the
|
|
35
42
|
* credential reading — the fail-closed direction (a wrongly-exempted name leaks; a wrongly-dropped one
|
|
36
43
|
* does not). Module-level export (not part of the package surface): the two real lists are disjoint, so
|
|
37
44
|
* only a controlled collision can tell this precedence from an inverted one. */
|
|
38
|
-
export declare function classifyExactEnvName(key: string, credentialNames:
|
|
39
|
-
/** Is `key` a secret-pattern env var (one the fail-closed `scrub` policy drops before spawning a shell)?
|
|
40
|
-
|
|
45
|
+
export declare function classifyExactEnvName(key: string, credentialNames: EnvNameTable, exemptNames: EnvNameTable, value?: string): ExactEnvNameVerdict;
|
|
46
|
+
/** Is `key` a secret-pattern env var (one the fail-closed `scrub` policy drops before spawning a shell)? Pass the
|
|
47
|
+
* entry's `value` to get exactly the scrub's answer for that entry: a named-family exemption holds only for a value
|
|
48
|
+
* that clears the family's screen, so without a value such a member reads as secret (the name alone cannot clear). */
|
|
49
|
+
export declare function isSecretEnvKey(key: string, value?: string): boolean;
|
|
41
50
|
/** Drop every secret-pattern key (see {@link isSecretEnvKey}) from an env object — the fail-closed `scrub`
|
|
42
51
|
* policy. `findings` is an OPTIONAL caller-owned collector: pass an array to learn which entries were
|
|
43
52
|
* dropped and why, read it back after the call. Omitting it leaves this function byte-for-byte what it
|
package/dist/core/secret-env.js
CHANGED
|
@@ -15,21 +15,30 @@ const NON_SECRET_ENV_EXACT_NAMES = new Set([
|
|
|
15
15
|
"MAX_BATCH_PREFILL_TOKENS", "MAX_WAITING_TOKENS", "MAX_TOP_N_TOKENS", "RATE_LIMIT_TOKENS",
|
|
16
16
|
"PROMPT_TOKENS", "CONTEXT_TOKENS",
|
|
17
17
|
]);
|
|
18
|
+
const NON_SECRET_ENV_INDEXED_FAMILIES = [
|
|
19
|
+
{ stem: "GIT_CONFIG_KEY", valueClears: (value) => !/[a-z][a-z0-9+.-]*:\/\/[^/@]+@/i.test(value) },
|
|
20
|
+
];
|
|
21
|
+
const indexedFamilyMember = (stem) => new RegExp(`^${stem}_(?:0|[1-9][0-9]*)$`);
|
|
22
|
+
const NON_SECRET_ENV_INDEXED_FAMILY_ROWS = NON_SECRET_ENV_INDEXED_FAMILIES.map((f) => ({ member: indexedFamilyMember(f.stem), valueClears: f.valueClears }));
|
|
23
|
+
const NON_SECRET_ENV_NAMES = {
|
|
24
|
+
has: (upperName, value) => NON_SECRET_ENV_EXACT_NAMES.has(upperName) ||
|
|
25
|
+
(value !== undefined && NON_SECRET_ENV_INDEXED_FAMILY_ROWS.some((row) => row.member.test(upperName) && row.valueClears(value))),
|
|
26
|
+
};
|
|
18
27
|
const SECRET_ENV_KIND_CONFIDENCE = {
|
|
19
28
|
"exact-name": "high",
|
|
20
29
|
"suffix-rule": "medium",
|
|
21
30
|
"namespace-rule": "medium",
|
|
22
31
|
};
|
|
23
|
-
export function classifyExactEnvName(key, credentialNames, exemptNames) {
|
|
32
|
+
export function classifyExactEnvName(key, credentialNames, exemptNames, value) {
|
|
24
33
|
const upper = key.toUpperCase();
|
|
25
|
-
if (credentialNames.has(upper))
|
|
34
|
+
if (credentialNames.has(upper, value))
|
|
26
35
|
return "credential";
|
|
27
|
-
if (exemptNames.has(upper))
|
|
36
|
+
if (exemptNames.has(upper, value))
|
|
28
37
|
return "exempt";
|
|
29
38
|
return "unknown";
|
|
30
39
|
}
|
|
31
|
-
function classifySecretEnvKey(key) {
|
|
32
|
-
const byExactName = classifyExactEnvName(key, SECRET_ENV_EXACT_NAMES,
|
|
40
|
+
function classifySecretEnvKey(key, value) {
|
|
41
|
+
const byExactName = classifyExactEnvName(key, SECRET_ENV_EXACT_NAMES, NON_SECRET_ENV_NAMES, value);
|
|
33
42
|
if (byExactName === "credential")
|
|
34
43
|
return "exact-name";
|
|
35
44
|
if (byExactName === "exempt")
|
|
@@ -38,13 +47,13 @@ function classifySecretEnvKey(key) {
|
|
|
38
47
|
return "suffix-rule";
|
|
39
48
|
return SEMA_NAMESPACE_SECRET_RE.test(key) ? "namespace-rule" : undefined;
|
|
40
49
|
}
|
|
41
|
-
export function isSecretEnvKey(key) {
|
|
42
|
-
return classifySecretEnvKey(key) !== undefined;
|
|
50
|
+
export function isSecretEnvKey(key, value) {
|
|
51
|
+
return classifySecretEnvKey(key, value) !== undefined;
|
|
43
52
|
}
|
|
44
53
|
export function scrubSecretEnv(env, findings) {
|
|
45
54
|
const out = {};
|
|
46
55
|
for (const k of Object.keys(env)) {
|
|
47
|
-
const kind = classifySecretEnvKey(k);
|
|
56
|
+
const kind = classifySecretEnvKey(k, env[k]);
|
|
48
57
|
if (kind === undefined) {
|
|
49
58
|
out[k] = env[k];
|
|
50
59
|
continue;
|
|
@@ -141,12 +141,14 @@ export interface BrainStatus {
|
|
|
141
141
|
* approximate across a clock adjustment — {@link retryInMs} stays the authoritative RELATIVE quantity.
|
|
142
142
|
*/
|
|
143
143
|
retryAtMs?: number;
|
|
144
|
-
/** On a `waiting_first_token` frame: how long the
|
|
145
|
-
*
|
|
146
|
-
*
|
|
144
|
+
/** On a `waiting_first_token` frame: how long the current wait has lasted, ms, at the moment the
|
|
145
|
+
* frame is emitted (the emitting process's clock; approximate across a hop). The wait is one
|
|
146
|
+
* segment's: the response-header wait before headers arrive, the first-token wait after — each
|
|
147
|
+
* starts its own count. Absent on every other phase. */
|
|
147
148
|
elapsedMs?: number;
|
|
148
|
-
/** On a `waiting_first_token` frame: the
|
|
149
|
-
*
|
|
149
|
+
/** On a `waiting_first_token` frame: the ceiling of the segment being waited on, ms — the armed
|
|
150
|
+
* first-byte window before headers, the armed first-token watchdog after — so a consumer can render
|
|
151
|
+
* "45s of 300s". Absent on every other phase. */
|
|
150
152
|
timeoutMs?: number;
|
|
151
153
|
/** 1-based index of the attempt that just failed (the wait precedes attempt `attempt + 1`);
|
|
152
154
|
* same numbering as the `brain.retry` telemetry frame. Absent on frames that are not a retry wait. */
|
|
@@ -386,8 +386,7 @@ export declare class TaskRegistry {
|
|
|
386
386
|
/** Background output-file read exemption: true iff `canonicalPath` is EXACTLY the registered output
|
|
387
387
|
* file of a background bash row that (a) the caller can access (same owner/scope/session visibility as
|
|
388
388
|
* a TaskOutput poll — {@link canAccess}) and (b) was registered on the caller's OWN execution env —
|
|
389
|
-
* `env` identity is part of the match, same discipline as {@link
|
|
390
|
-
* {@link runningShellIds}. A canonical path only names a file within ONE env's filesystem
|
|
389
|
+
* `env` identity is part of the match, same discipline as {@link runningShellIds}. A canonical path only names a file within ONE env's filesystem
|
|
391
390
|
* namespace: on a shared registry, a row minted on env A must never exempt a same-named outside-root
|
|
392
391
|
* file in env B's namespace (same session or not), so instance equality — never a string proxy — is
|
|
393
392
|
* required. The output file is minted by the ENGINE itself (createTempFile
|
|
@@ -610,11 +609,6 @@ export declare class TaskRegistry {
|
|
|
610
609
|
* completion notification at the terminal state. `unref()`d — a watcher never keeps the process alive.
|
|
611
610
|
* Stops itself on terminal, on a vanished shell (env disposed), or when the handle is evicted. */
|
|
612
611
|
private startBashWatcher;
|
|
613
|
-
/** RETAINED shells (THE retain predicate) still running on THIS env, any owner. Env identity is part of the
|
|
614
|
-
* match (a shared registry never exempts another env's ids). No caller in src since the park-only parks
|
|
615
|
-
* moved to the run-boundary dispose (which spares every running row: {@link runningShellIds}); removal
|
|
616
|
-
* scheduled for the next minor. */
|
|
617
|
-
retainedShellIds(env: unknown): BackgroundShellId[];
|
|
618
612
|
/** Every shell-lane row still RUNNING on this env, any owner — the run-boundary dispose's except-list (the
|
|
619
613
|
* run teardown and the park-only parks: `sweepRunBoundaryBackgroundShells`).
|
|
620
614
|
* A run's teardown disposes only shells it can attribute to itself: its settle sweep has already killed
|
|
@@ -606,7 +606,12 @@ export class TaskRegistry {
|
|
|
606
606
|
}
|
|
607
607
|
let outcome;
|
|
608
608
|
try {
|
|
609
|
-
|
|
609
|
+
const seat = handle.env.killBackgroundNow;
|
|
610
|
+
if (seat === undefined)
|
|
611
|
+
shutdownDebug("hard kill: seat absent", { id: handle.id, shellId: handle.shellId });
|
|
612
|
+
outcome = seat === undefined ? "no_pgid" : seat.call(handle.env, handle.shellId);
|
|
613
|
+
if (seat !== undefined && outcome === "no_pgid")
|
|
614
|
+
shutdownDebug("hard kill: seat answered no_pgid", { id: handle.id, shellId: handle.shellId });
|
|
610
615
|
}
|
|
611
616
|
catch (err) {
|
|
612
617
|
shutdownDebug("hard kill: seat threw", { id: handle.id, shellId: handle.shellId, error: thrownText(err) });
|
|
@@ -835,15 +840,6 @@ export class TaskRegistry {
|
|
|
835
840
|
handle.watcher = setInterval(() => void tick(), 1_000);
|
|
836
841
|
handle.watcher.unref?.();
|
|
837
842
|
}
|
|
838
|
-
retainedShellIds(env) {
|
|
839
|
-
const out = [];
|
|
840
|
-
for (const handle of this.handles.values()) {
|
|
841
|
-
if ((handle.type === "background_bash" || handle.type === "monitor") && handle.status === "running" && shellRowRetained(handle) && handle.env === env) {
|
|
842
|
-
out.push(handle.shellId);
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
return out;
|
|
846
|
-
}
|
|
847
843
|
runningShellIds(env) {
|
|
848
844
|
const out = [];
|
|
849
845
|
for (const handle of this.handles.values()) {
|
|
@@ -341,18 +341,18 @@ export type PermissionResult = {
|
|
|
341
341
|
* `probe*` above; ENGINE-STAMPED and VALIDATED at the tighten, and deliberately NOT merged onto an ask
|
|
342
342
|
* another layer raised, which a grant leaves standing. */
|
|
343
343
|
probeReadRootCandidate?: import("./types.js").ReadRootGrantCandidate;
|
|
344
|
-
/** An EXPLICIT `ask` permission rule matched this call (design/127 DSL — the CC `alwaysAskRules`
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
*
|
|
355
|
-
*
|
|
344
|
+
/** An EXPLICIT `ask` permission rule matched this call (design/127 DSL — the CC `alwaysAskRules` shape); carries the
|
|
345
|
+
* matched rule's text. Present ⇔ a rule someone WROTE says "ask about this", never for a `defaultAction:"ask"` fallback
|
|
346
|
+
* (an unmatched call is default-closed posture, not a per-call instruction — CC draws the same line: only a matched ask
|
|
347
|
+
* rule diverts the auto-mode lane, a mode-default ask stays classifier-eligible). Consumer: the gate's auto-mode
|
|
348
|
+
* classifier step refuses to resolve an ask carrying it — a person's standing "ask me each time" is not classifier
|
|
349
|
+
* hesitation, so a classifier verdict must not be the thing that clears it. A policy self-declaring the member only
|
|
350
|
+
* ever OPTS ITS OWN ask OUT of classifier auto-resolution (tightening — the false-claim direction is safe, unlike the
|
|
351
|
+
* engine-stamped `"hook"` word whose risk was laundering provenance AWAY). {@link combinePolicies} carries it onto the
|
|
352
|
+
* surviving ask when ANY folded-away concurrent ask bore it (monotone, tighten-only); it is consumed inside the gate
|
|
353
|
+
* and NOT copied onto the park/approval-card request — but it is the ONE input of the `ask_rule` origin word they DO
|
|
354
|
+
* carry ({@link origin}): a minter raising an ask for a written rule (a person's settings rule, an operator's must-ask
|
|
355
|
+
* list) that leaves it unset gets `policy` there, indistinguishable from hesitation. */
|
|
356
356
|
matchedAskRule?: string;
|
|
357
357
|
/** design/252 G-2 (additive): the RULE-PROVENANCE evidence behind this ask — see
|
|
358
358
|
* {@link AskRuleEvidence}. ENGINE-STAMPED inside the gate, once, after the org layer and the
|