@deepwatch/dsh-contracts 0.1.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.
@@ -0,0 +1,259 @@
1
+ /**
2
+ * What one tool call did, recorded because it happened rather than because the
3
+ * model mentioned it.
4
+ *
5
+ * **Why this exists.** An owner evaluation ran 47 model rounds and 76 tool
6
+ * calls — 30 shell, 30 reads, 5 writes, 4 greps, 3 subagents — and Watch
7
+ * recorded none of them. Not because the recording failed: because nothing was
8
+ * listening. Watch's tools were tools the model could choose to call, so a
9
+ * model that never thought about Watch left a product whose entire subject is
10
+ * evidence with an empty ledger, a Library reporting `Index is behind the
11
+ * store`, and a Compare with nothing to compare. The agent wrote its own
12
+ * `verification.json`, reread it with a second generic tool, and reported PASS.
13
+ *
14
+ * The fix is not a better prompt. A capability that depends on the model
15
+ * remembering it exists is not a capability, it is a suggestion. These records
16
+ * are minted from the Harness's own tool-dispatch lifecycle, so a call is
17
+ * recorded whether or not anything in the conversation has ever heard of Watch.
18
+ *
19
+ * **What a record is not.** It is not a verdict. `state: 'completed'` means the
20
+ * dispatch returned; it says nothing about whether the thing the caller wanted
21
+ * is true. Every record starts `UNVERIFIED` and only a verification contract
22
+ * evaluated against evidence may move it — never a tool's own exit code, and
23
+ * never a file the model wrote claiming success. That separation is the reason
24
+ * {@link AgentExecutionState} and {@link Verdict} are two types in this package
25
+ * and not one.
26
+ *
27
+ * @module @deepwatch/dsh-contracts/execution
28
+ */
29
+ import type { AgentExecutionState, Verdict } from './index.js';
30
+ /**
31
+ * The contract version these records are written under.
32
+ *
33
+ * Read by the Bridge and by the Library indexer: a record from a newer writer
34
+ * is refused rather than half-understood. Bumped when a field's meaning
35
+ * changes, not when one is added with a safe absence.
36
+ */
37
+ export declare const EXECUTION_RECORD_VERSION = 1;
38
+ /**
39
+ * What a call did to the world, as far as the Host can tell from the boundary.
40
+ *
41
+ * Classified from the tool's declared surface rather than guessed from its
42
+ * name, and `unknown` is a real answer: a tool this distribution has never
43
+ * seen is not assumed harmless. Verification treats `unknown` as unproved
44
+ * rather than as `none`, so a classification gap fails closed.
45
+ */
46
+ export type SideEffectClass =
47
+ /** Observed state without changing it. */
48
+ 'read'
49
+ /** Changed durable state. */
50
+ | 'write'
51
+ /** Ran a process. */
52
+ | 'execute'
53
+ /** Reached beyond this machine. */
54
+ | 'network'
55
+ /** Changed nothing outside the conversation — a todo edit, a plan note. */
56
+ | 'none'
57
+ /** Not classifiable from the boundary. Never treated as `none`. */
58
+ | 'unknown';
59
+ /**
60
+ * Where a call's paths resolved, relative to the workspace a person chose.
61
+ *
62
+ * `outside_workspace` is recorded rather than hidden. The evaluation that
63
+ * prompted this work searched other drives, read unrelated locations and wrote
64
+ * under the system temporary directory, and none of it appeared anywhere a
65
+ * person could see. An attempt that was refused is still something the owner
66
+ * is entitled to know happened.
67
+ */
68
+ export type WorkspaceScope =
69
+ /** Every affected path resolved inside the selected workspace. */
70
+ 'inside'
71
+ /** At least one path resolved outside it. Recorded whether or not it ran. */
72
+ | 'outside_workspace'
73
+ /** No workspace is selected, so containment could not be decided. */
74
+ | 'no_workspace'
75
+ /** The call names no path. */
76
+ | 'not_applicable';
77
+ /** What the containment gate did about it. */
78
+ export type ScopeDecision = 'allowed' | 'denied' | 'approved' | 'not_evaluated';
79
+ /**
80
+ * The identity that makes a retry a retry rather than a second action.
81
+ *
82
+ * Four parts, all of them stable across an attempt: the session, the turn, the
83
+ * call the model asked for, and which attempt this is. Two records sharing the
84
+ * first three are attempts at one action; a ledger that keyed on the call alone
85
+ * would show a retried write as two writes, which is the difference between
86
+ * "this happened twice" and "this was tried twice and happened once".
87
+ */
88
+ export interface ExecutionIdentity {
89
+ readonly sessionId: string;
90
+ /** The agent turn this call belongs to, as the loop numbers it. */
91
+ readonly turnId: string;
92
+ /** The model-requested call id. Nested dispatches carry their own. */
93
+ readonly callId: string;
94
+ /** 1 for the first attempt. */
95
+ readonly attempt: number;
96
+ }
97
+ /**
98
+ * One tool call, from the boundary that dispatched it.
99
+ *
100
+ * Every field here is either observed at the lifecycle or derived from it. None
101
+ * of it is reported by the tool about itself, and none of it is asserted by the
102
+ * model.
103
+ */
104
+ export interface ToolExecutionRecord extends ExecutionIdentity {
105
+ readonly version: number;
106
+ /**
107
+ * `<sessionId>/<turnId>/<callId>#<attempt>`, the one spelling.
108
+ *
109
+ * Written by {@link executionKey} so two producers cannot disagree about
110
+ * whether a retry is a duplicate.
111
+ */
112
+ readonly idempotencyKey: string;
113
+ /** The root model-requested call, when this is a nested dispatch. */
114
+ readonly rootCallId: string | null;
115
+ /**
116
+ * The subagent whose turn this call belongs to, and the parent turn that
117
+ * spawned it. Both null for a call the top-level agent made directly.
118
+ */
119
+ readonly subagentId: string | null;
120
+ readonly parentTurnId: string | null;
121
+ readonly toolName: string;
122
+ readonly state: AgentExecutionState;
123
+ readonly startedAt: string;
124
+ readonly endedAt: string | null;
125
+ readonly durationMs: number | null;
126
+ /**
127
+ * How the dispatch ended, in the boundary's own words: `ok`, the tool's
128
+ * failure code, `denied`, or `cancelled`. Not a verdict.
129
+ */
130
+ readonly exitStatus: string | null;
131
+ readonly sideEffect: SideEffectClass;
132
+ readonly scope: WorkspaceScope;
133
+ readonly scopeDecision: ScopeDecision;
134
+ /**
135
+ * Paths the call affected, workspace-relative.
136
+ *
137
+ * Relative because an absolute path carries the operating system user's name
138
+ * and the shape of somebody's disk, and neither belongs in a record that is
139
+ * exported, indexed and shown. A path outside the workspace is not recorded
140
+ * as a path at all — {@link WorkspaceScope} says it happened and
141
+ * `outsidePathCount` says how many, which is the fact without the disclosure.
142
+ */
143
+ readonly paths: readonly string[];
144
+ readonly outsidePathCount: number;
145
+ /** The arguments, redacted and bounded. Never the raw argument object. */
146
+ readonly inputSummary: string;
147
+ /** The result, redacted and bounded. Never the full output. */
148
+ readonly outputSummary: string;
149
+ /** `sha256:…` over the full untruncated output, so the summary is checkable. */
150
+ readonly outputDigest: string;
151
+ /**
152
+ * The provenance turn that authorised the provider request this call belongs
153
+ * to, correlating a tool action back to the person who asked for it. Null
154
+ * when the call ran outside any authorised turn, which is itself worth
155
+ * seeing.
156
+ */
157
+ readonly authorisedBy: string | null;
158
+ /**
159
+ * Always `UNVERIFIED` at mint.
160
+ *
161
+ * A tool that returned is a tool that returned. Only
162
+ * {@link VerificationOutcome} moves this, and only from evidence.
163
+ */
164
+ readonly verification: Verdict;
165
+ }
166
+ /** The one spelling of an execution's identity. */
167
+ export declare function executionKey(identity: ExecutionIdentity): string;
168
+ /**
169
+ * Whether two records describe attempts at the same action.
170
+ *
171
+ * The question a ledger asks before it shows a person "this ran twice".
172
+ */
173
+ export declare function isSameAction(a: ExecutionIdentity, b: ExecutionIdentity): boolean;
174
+ /**
175
+ * How long the bounded summaries may be.
176
+ *
177
+ * Bounded because the evaluation that prompted this recorded 2.9M tokens of
178
+ * context, and a ledger that stored every shell transcript would be a second
179
+ * copy of it. Long enough to identify what happened, short enough that a
180
+ * thousand records stay readable and indexable.
181
+ */
182
+ /**
183
+ * The states a record can no longer leave.
184
+ *
185
+ * A dispatch that finished, failed or was refused has an answer. Anything
186
+ * arriving afterwards is a second observation of the same event -- a retried
187
+ * emit, a late listener, an out-of-order post-execute -- and not a new outcome.
188
+ */
189
+ export declare const TERMINAL_STATES: readonly AgentExecutionState[];
190
+ /** Whether a state is the end of an execution's story. */
191
+ export declare function isTerminalState(state: AgentExecutionState): boolean;
192
+ /**
193
+ * How far through its life an execution is, so progress can be compared.
194
+ *
195
+ * Ranked rather than ordered by name because the only question ever asked of
196
+ * it is "is this observation older than the one already recorded", and a
197
+ * comparison that answers that in one place cannot answer it differently in
198
+ * another.
199
+ */
200
+ export declare function stateRank(state: AgentExecutionState): number;
201
+ /**
202
+ * How firmly a scope decision is held.
203
+ *
204
+ * `denied` outranks everything, and that is the whole point. A call the
205
+ * boundary refused is refused; a later observation that did not know about the
206
+ * refusal must not be able to describe it as permitted. This is the single
207
+ * asymmetry in the record, and it is deliberate: softening a denial is the one
208
+ * error that turns a boundary into a label.
209
+ */
210
+ export declare function scopeDecisionRank(decision: ScopeDecision): number;
211
+ /**
212
+ * Merge two observations of one execution into the single canonical record.
213
+ *
214
+ * Both producers of a record -- the containment screen and the settling of a
215
+ * result -- write through here, so neither can overwrite the other by arriving
216
+ * second. The rules are few and each exists because breaking it produced a
217
+ * record that lied:
218
+ *
219
+ * - A denial is permanent. A refused call still travels back through the
220
+ * dispatch layer as an error, and settling that error as an ordinary
221
+ * outcome once rewrote `denied` into `allowed`.
222
+ * - A terminal state is final. Two terminal observations of one execution
223
+ * are the same ending seen twice, not two endings.
224
+ * - Progress only moves forward. A late `running` cannot un-finish a call.
225
+ *
226
+ * Returns `existing` unchanged -- by identity, so a caller can test whether
227
+ * anything moved -- when the incoming observation adds nothing.
228
+ */
229
+ export declare function reconcileExecutionRecords(existing: ToolExecutionRecord, incoming: ToolExecutionRecord): ToolExecutionRecord;
230
+ export declare const SUMMARY_LIMIT = 512;
231
+ /**
232
+ * Cut a summary to {@link SUMMARY_LIMIT}, saying so when it was cut.
233
+ *
234
+ * The marker matters: a reader who cannot tell a short output from a truncated
235
+ * one will eventually read a truncation as the whole answer.
236
+ */
237
+ export declare function boundSummary(text: string, limit?: number): string;
238
+ /** What a redacted secret is replaced by, so the fact survives the value. */
239
+ export declare const SECRET_PLACEHOLDER = "<redacted>";
240
+ /**
241
+ * Remove secret-shaped material from text bound for a record.
242
+ *
243
+ * Deliberately conservative about what it calls a secret and deliberately
244
+ * blunt about what it does with one: the replacement keeps the shape of the
245
+ * line so a reader can still see that a token was passed, without the token.
246
+ *
247
+ * This is not the only defence and must not be treated as one. The Host does
248
+ * not put environment dumps or credential values into summaries in the first
249
+ * place; this is what catches the ones that arrive inside something else.
250
+ */
251
+ export declare function redactSecrets(text: string): string;
252
+ /**
253
+ * Whether a string still looks like it carries a credential.
254
+ *
255
+ * Used by the export gate and by tests, which need to assert absence rather
256
+ * than trust the redactor that produced the text.
257
+ */
258
+ export declare function looksLikeSecret(text: string): boolean;
259
+ //# sourceMappingURL=execution.d.ts.map
@@ -0,0 +1,213 @@
1
+ /**
2
+ * What one tool call did, recorded because it happened rather than because the
3
+ * model mentioned it.
4
+ *
5
+ * **Why this exists.** An owner evaluation ran 47 model rounds and 76 tool
6
+ * calls — 30 shell, 30 reads, 5 writes, 4 greps, 3 subagents — and Watch
7
+ * recorded none of them. Not because the recording failed: because nothing was
8
+ * listening. Watch's tools were tools the model could choose to call, so a
9
+ * model that never thought about Watch left a product whose entire subject is
10
+ * evidence with an empty ledger, a Library reporting `Index is behind the
11
+ * store`, and a Compare with nothing to compare. The agent wrote its own
12
+ * `verification.json`, reread it with a second generic tool, and reported PASS.
13
+ *
14
+ * The fix is not a better prompt. A capability that depends on the model
15
+ * remembering it exists is not a capability, it is a suggestion. These records
16
+ * are minted from the Harness's own tool-dispatch lifecycle, so a call is
17
+ * recorded whether or not anything in the conversation has ever heard of Watch.
18
+ *
19
+ * **What a record is not.** It is not a verdict. `state: 'completed'` means the
20
+ * dispatch returned; it says nothing about whether the thing the caller wanted
21
+ * is true. Every record starts `UNVERIFIED` and only a verification contract
22
+ * evaluated against evidence may move it — never a tool's own exit code, and
23
+ * never a file the model wrote claiming success. That separation is the reason
24
+ * {@link AgentExecutionState} and {@link Verdict} are two types in this package
25
+ * and not one.
26
+ *
27
+ * @module @deepwatch/dsh-contracts/execution
28
+ */
29
+ /**
30
+ * The contract version these records are written under.
31
+ *
32
+ * Read by the Bridge and by the Library indexer: a record from a newer writer
33
+ * is refused rather than half-understood. Bumped when a field's meaning
34
+ * changes, not when one is added with a safe absence.
35
+ */
36
+ export const EXECUTION_RECORD_VERSION = 1;
37
+ /** The one spelling of an execution's identity. */
38
+ export function executionKey(identity) {
39
+ return `${identity.sessionId}/${identity.turnId}/${identity.callId}#${String(identity.attempt)}`;
40
+ }
41
+ /**
42
+ * Whether two records describe attempts at the same action.
43
+ *
44
+ * The question a ledger asks before it shows a person "this ran twice".
45
+ */
46
+ export function isSameAction(a, b) {
47
+ return a.sessionId === b.sessionId && a.turnId === b.turnId && a.callId === b.callId;
48
+ }
49
+ /**
50
+ * How long the bounded summaries may be.
51
+ *
52
+ * Bounded because the evaluation that prompted this recorded 2.9M tokens of
53
+ * context, and a ledger that stored every shell transcript would be a second
54
+ * copy of it. Long enough to identify what happened, short enough that a
55
+ * thousand records stay readable and indexable.
56
+ */
57
+ /**
58
+ * The states a record can no longer leave.
59
+ *
60
+ * A dispatch that finished, failed or was refused has an answer. Anything
61
+ * arriving afterwards is a second observation of the same event -- a retried
62
+ * emit, a late listener, an out-of-order post-execute -- and not a new outcome.
63
+ */
64
+ export const TERMINAL_STATES = ['completed', 'failed', 'cancelled'];
65
+ /** Whether a state is the end of an execution's story. */
66
+ export function isTerminalState(state) {
67
+ return TERMINAL_STATES.includes(state);
68
+ }
69
+ /**
70
+ * How far through its life an execution is, so progress can be compared.
71
+ *
72
+ * Ranked rather than ordered by name because the only question ever asked of
73
+ * it is "is this observation older than the one already recorded", and a
74
+ * comparison that answers that in one place cannot answer it differently in
75
+ * another.
76
+ */
77
+ export function stateRank(state) {
78
+ if (state === 'queued')
79
+ return 0;
80
+ if (state === 'running')
81
+ return 1;
82
+ return 2;
83
+ }
84
+ /**
85
+ * How firmly a scope decision is held.
86
+ *
87
+ * `denied` outranks everything, and that is the whole point. A call the
88
+ * boundary refused is refused; a later observation that did not know about the
89
+ * refusal must not be able to describe it as permitted. This is the single
90
+ * asymmetry in the record, and it is deliberate: softening a denial is the one
91
+ * error that turns a boundary into a label.
92
+ */
93
+ export function scopeDecisionRank(decision) {
94
+ if (decision === 'denied')
95
+ return 3;
96
+ if (decision === 'approved')
97
+ return 2;
98
+ if (decision === 'allowed')
99
+ return 1;
100
+ return 0;
101
+ }
102
+ /**
103
+ * Merge two observations of one execution into the single canonical record.
104
+ *
105
+ * Both producers of a record -- the containment screen and the settling of a
106
+ * result -- write through here, so neither can overwrite the other by arriving
107
+ * second. The rules are few and each exists because breaking it produced a
108
+ * record that lied:
109
+ *
110
+ * - A denial is permanent. A refused call still travels back through the
111
+ * dispatch layer as an error, and settling that error as an ordinary
112
+ * outcome once rewrote `denied` into `allowed`.
113
+ * - A terminal state is final. Two terminal observations of one execution
114
+ * are the same ending seen twice, not two endings.
115
+ * - Progress only moves forward. A late `running` cannot un-finish a call.
116
+ *
117
+ * Returns `existing` unchanged -- by identity, so a caller can test whether
118
+ * anything moved -- when the incoming observation adds nothing.
119
+ */
120
+ export function reconcileExecutionRecords(existing, incoming) {
121
+ // Two records that are not the same execution must never be merged; the
122
+ // caller keyed them wrongly and silently blending them would hide that.
123
+ if (existing.idempotencyKey !== incoming.idempotencyKey)
124
+ return existing;
125
+ const scopeDecision = scopeDecisionRank(existing.scopeDecision)
126
+ >= scopeDecisionRank(incoming.scopeDecision)
127
+ ? existing.scopeDecision
128
+ : incoming.scopeDecision;
129
+ const settled = isTerminalState(existing.state)
130
+ || stateRank(incoming.state) <= stateRank(existing.state);
131
+ if (settled) {
132
+ return scopeDecision === existing.scopeDecision ? existing : { ...existing, scopeDecision };
133
+ }
134
+ // The incoming observation is genuinely further along. It carries the newer
135
+ // outcome, but identity and origin belong to the record that opened.
136
+ return {
137
+ ...incoming,
138
+ sessionId: existing.sessionId,
139
+ turnId: existing.turnId,
140
+ callId: existing.callId,
141
+ attempt: existing.attempt,
142
+ idempotencyKey: existing.idempotencyKey,
143
+ startedAt: existing.startedAt,
144
+ scopeDecision,
145
+ };
146
+ }
147
+ export const SUMMARY_LIMIT = 512;
148
+ /**
149
+ * Cut a summary to {@link SUMMARY_LIMIT}, saying so when it was cut.
150
+ *
151
+ * The marker matters: a reader who cannot tell a short output from a truncated
152
+ * one will eventually read a truncation as the whole answer.
153
+ */
154
+ export function boundSummary(text, limit = SUMMARY_LIMIT) {
155
+ const collapsed = text.replace(/\s+/gu, ' ').trim();
156
+ if (collapsed.length <= limit)
157
+ return collapsed;
158
+ return `${collapsed.slice(0, limit)}… (${String(collapsed.length)} chars)`;
159
+ }
160
+ /**
161
+ * Patterns whose *value* must never reach a record.
162
+ *
163
+ * Keys, bearer tokens and the assignment forms they arrive in. This is a
164
+ * belt-and-braces pass over text that has already been shaped by the caller: the
165
+ * summaries are built from arguments and results the Host chose to include, and
166
+ * this catches the case where a secret is inside one of them — a shell command
167
+ * line with an inline token, an error quoting an Authorization header.
168
+ */
169
+ const SECRET_PATTERNS = [
170
+ // Provider key shapes: sk-…, sk-ant-…, ghp_…, and friends.
171
+ /\b(?:sk|pk|rk)-[A-Za-z0-9_-]{12,}/gu,
172
+ /\b(?:gh[pousr]|github_pat)_[A-Za-z0-9_]{12,}/gu,
173
+ /\bxox[abposr]-[A-Za-z0-9-]{10,}/gu,
174
+ // Bearer and Basic credentials wherever they appear.
175
+ /\b(?:Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{8,}/gu,
176
+ // `KEY=value`, `--token value`, `"apiKey": "value"` and their neighbours.
177
+ /\b[A-Za-z_][A-Za-z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL)\s*[=:]\s*\S+/giu,
178
+ /--(?:api-?key|token|password|secret)(?:[=\s]+)\S+/giu,
179
+ ];
180
+ /** What a redacted secret is replaced by, so the fact survives the value. */
181
+ export const SECRET_PLACEHOLDER = '<redacted>';
182
+ /**
183
+ * Remove secret-shaped material from text bound for a record.
184
+ *
185
+ * Deliberately conservative about what it calls a secret and deliberately
186
+ * blunt about what it does with one: the replacement keeps the shape of the
187
+ * line so a reader can still see that a token was passed, without the token.
188
+ *
189
+ * This is not the only defence and must not be treated as one. The Host does
190
+ * not put environment dumps or credential values into summaries in the first
191
+ * place; this is what catches the ones that arrive inside something else.
192
+ */
193
+ export function redactSecrets(text) {
194
+ let out = text;
195
+ for (const pattern of SECRET_PATTERNS)
196
+ out = out.replace(pattern, SECRET_PLACEHOLDER);
197
+ return out;
198
+ }
199
+ /**
200
+ * Whether a string still looks like it carries a credential.
201
+ *
202
+ * Used by the export gate and by tests, which need to assert absence rather
203
+ * than trust the redactor that produced the text.
204
+ */
205
+ export function looksLikeSecret(text) {
206
+ return SECRET_PATTERNS.some((pattern) => {
207
+ // `RegExp` with `g` carries `lastIndex` between calls; a fresh one per test
208
+ // keeps this a pure predicate.
209
+ const probe = new RegExp(pattern.source, pattern.flags.replace('g', ''));
210
+ return probe.test(text);
211
+ });
212
+ }
213
+ //# sourceMappingURL=execution.js.map
@@ -0,0 +1,145 @@
1
+ /**
2
+ * What a person is told when a conversation cannot run, and what they are not.
3
+ *
4
+ * A prompt failed and the Chat surface showed the reader
5
+ * `@deepseek-ai/dsh-system-prompt`, the route id `llm-deepseek`, the provider
6
+ * key `deepseek-official`, the environment variable `DEEPSEEK_API_KEY`, and a
7
+ * paragraph of sandbox-policy text. Every one of those is true, and not one of
8
+ * them told the reader what to do. Worse, two of them named a provider they had
9
+ * never chosen, so the message actively misled: it read as *DeepWatch is a
10
+ * DeepSeek product that is broken* rather than *nothing is bound yet*.
11
+ *
12
+ * This module draws the line. A raw provider or runtime failure is classified
13
+ * into one of a small closed set of {@link FailureKind}s, each of which becomes
14
+ * a card with a title, one sentence, and exactly one next action. The detailed
15
+ * technical facts are not destroyed — they go to Diagnostics and the Session
16
+ * Log, redacted — but they never render in an ordinary conversation.
17
+ *
18
+ * **Two rules hold everything here together.**
19
+ *
20
+ * The first is that a card carries no implementation identity.
21
+ * {@link assertNoInternalDisclosure} is the executable form of that rule, and
22
+ * it is applied to every card this module produces rather than trusted to
23
+ * review.
24
+ *
25
+ * The second is that nothing here ever carries credential material — and
26
+ * "material" includes the things people reach for when they are trying to be
27
+ * helpful about a key without printing it: a prefix, a suffix, a length, a
28
+ * fingerprint, a hash. A reader does not need to be told their key is 51
29
+ * characters long, and an attacker reading a shared screenshot does.
30
+ *
31
+ * @module @deepwatch/dsh-contracts/failures
32
+ */
33
+ import type { ReadinessBlocker } from './readiness.js';
34
+ /**
35
+ * The closed set of things a person can be told went wrong.
36
+ *
37
+ * Closed on purpose. Every raw failure maps onto one of these or onto
38
+ * {@link FailureKind.unavailable}, and there is no passthrough case that
39
+ * renders a provider's own words: a provider message is written for an API
40
+ * consumer, arrives unlocalised, and has no obligation to avoid naming
41
+ * internals.
42
+ */
43
+ export type FailureKind =
44
+ /** No model is bound to this capability. The first-run case. */
45
+ 'not_configured'
46
+ /** A complete binding exists, but no provider request has succeeded. */
47
+ | 'not_tested'
48
+ /** A credential is referenced and the store could not produce it. */
49
+ | 'credential_unavailable'
50
+ /** The provider answered and rejected the credential. */
51
+ | 'credential_rejected'
52
+ /** The bound model is not one the provider offers any more. */
53
+ | 'model_unavailable'
54
+ /** The provider did not answer usefully. */
55
+ | 'provider_unreachable'
56
+ /** The provider answered and declined for rate reasons. */
57
+ | 'rate_limited'
58
+ /** Policy on this machine forbids the request. */
59
+ | 'policy_forbids'
60
+ /** Something else. Deliberately vague to the reader, detailed in Diagnostics. */
61
+ | 'unavailable';
62
+ /** Where a card's action sends a person. */
63
+ export type FailureActionTarget =
64
+ /** The Role Bindings screen, at the role that is blocked. */
65
+ 'role-bindings'
66
+ /** The provider credential screen. */
67
+ | 'provider-credential'
68
+ /** The model picker for the bound provider. */
69
+ | 'model-selection'
70
+ /** Diagnostics, for the redacted technical detail. */
71
+ | 'diagnostics'
72
+ /** Nothing to configure; the reader can only try again. */
73
+ | 'retry';
74
+ /** One card, as a conversation renders it. */
75
+ export interface FailureCard {
76
+ readonly kind: FailureKind;
77
+ /** A short heading, in a person's words. Never an error code. */
78
+ readonly title: string;
79
+ /** One sentence saying what is true. Never a stack trace, never a route id. */
80
+ readonly detail: string;
81
+ /** The label of the single action that fixes this. */
82
+ readonly action: string;
83
+ readonly target: FailureActionTarget;
84
+ /**
85
+ * Whether Diagnostics holds more about this.
86
+ *
87
+ * Every card offers the path; this says whether following it will find
88
+ * anything, so the product does not send somebody to an empty screen.
89
+ */
90
+ readonly hasDiagnostics: boolean;
91
+ }
92
+ /**
93
+ * The card for one kind.
94
+ *
95
+ * @param kind - the classified failure.
96
+ * @param hasDiagnostics - whether a redacted record was written for it.
97
+ * @returns a card safe to render in an ordinary conversation.
98
+ */
99
+ export declare function failureCard(kind: FailureKind, hasDiagnostics?: boolean): FailureCard;
100
+ /**
101
+ * The card a readiness blocker becomes.
102
+ *
103
+ * The two vocabularies are separate because they answer different questions —
104
+ * a blocker is "what is missing", a card is "what a person sees when they tried
105
+ * anyway" — and this is the one place they are joined, so a blocker can never
106
+ * reach a reader with no card defined for it.
107
+ */
108
+ export declare function cardForBlocker(blocker: ReadinessBlocker): FailureCard;
109
+ /**
110
+ * Classify a raw failure without letting its words through.
111
+ *
112
+ * The input is read for *signals* — an HTTP status, a taxonomy code the
113
+ * Harness already normalised — and the output is a kind. The raw text is
114
+ * never returned, never embedded, and never partially quoted, because a
115
+ * provider's message is exactly where the internal identifiers come from.
116
+ *
117
+ * @param signal - what the runtime managed to normalise about the failure.
118
+ * @returns the kind a reader is told about.
119
+ */
120
+ export declare function classifyFailure(signal: {
121
+ /** HTTP status observed at the provider boundary, when there was one. */
122
+ readonly status?: number | undefined;
123
+ /** The Harness's own taxonomy code (`AUTH`, `RATE_LIMIT`, `NO_ADAPTER`, …). */
124
+ readonly code?: string | undefined;
125
+ }): FailureKind;
126
+ /**
127
+ * Throw when text bound for an ordinary conversation carries an internal name.
128
+ *
129
+ * Applied to cards at construction rather than to the screen at review time.
130
+ * The failure it prevents is not hypothetical: every shape above is one that
131
+ * actually reached a reader, and the reason each got there was that some layer
132
+ * passed a provider or runtime string through "just this once".
133
+ *
134
+ * @param where - the surface being guarded, so a failure says what to fix.
135
+ * @param text - the candidate copy.
136
+ */
137
+ export declare function assertNoInternalDisclosure(where: string, text: string): void;
138
+ /**
139
+ * Whether a card is safe to render, as a boolean rather than a throw.
140
+ *
141
+ * The same check for callers that are validating a table of copy rather than
142
+ * building one value — a test over every kind, typically.
143
+ */
144
+ export declare function isDisclosureSafe(text: string): boolean;
145
+ //# sourceMappingURL=failures.d.ts.map