@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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 oxbshw
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,77 @@
1
+ # @deepwatch/dsh-contracts
2
+
3
+ Watch Bridge wire contracts shared by the Host plugins and the browser halves
4
+
5
+ Part of **DeepWatch** — the agent workspace built on the official
6
+ [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)
7
+ and powered by [Watch Skill](https://github.com/oxbshw/watch-skill) for perception, evidence, memory and
8
+ independent verification.
9
+
10
+ > **Shared contract — depended on by both halves.**
11
+ > Anyone implementing either side of the Bridge — a host plugin, a browser half, or a client of your own.
12
+
13
+ ## Exports
14
+
15
+ - `@deepwatch/dsh-contracts`
16
+ - `@deepwatch/dsh-contracts/identity`
17
+ - `@deepwatch/dsh-contracts/query`
18
+ - `@deepwatch/dsh-contracts/query/validate`
19
+ - `@deepwatch/dsh-contracts/query/wire`
20
+
21
+ ## Install
22
+
23
+ > **Not on npm yet.** Nothing exists under the `@deepwatch` scope. This
24
+ > package is published for the first time by the `deepwatch-v0.1.0`
25
+ > release; until then the command below resolves nothing, and
26
+ > [the workspace README](https://github.com/oxbshw/watch-skill/tree/main/workspace#readme) has the path
27
+ > that works from a checkout.
28
+
29
+ ```sh
30
+ npm install @deepwatch/dsh-contracts
31
+ ```
32
+
33
+ Rarely on its own. [`@deepwatch/dsh-bundle`](https://github.com/oxbshw/watch-skill/tree/main/workspace/packages/watch/bundle#readme)
34
+ composes this package with the rest of DeepWatch and is what a profile
35
+ normally depends on; installing this one directly is for embedding a
36
+ single piece in a composition you control.
37
+
38
+ ## Requirements
39
+
40
+ - Node `^22.19.0 || >=24.0.0`
41
+
42
+ None. Types and schemas only; it pulls in nothing at runtime.
43
+
44
+ ## Stability
45
+
46
+ `0.1.0` — a stable release.
47
+
48
+ Stable means tested, documented and supported — not 1.0. This is a
49
+ pre-1.0 line, and semantic versioning gives `0.x` no compatibility
50
+ guarantee across minor versions: **a `0.MINOR` bump may change or remove
51
+ surface, and a patch will not.** Depend on it with a tilde range
52
+ (`~0.1.0`) if you want that difference enforced by your lockfile
53
+ rather than by a changelog. The usual major-version promise starts at 1.0.
54
+
55
+ ## Side effects
56
+
57
+ Importing a module from this package evaluates no side effects, so a
58
+ bundler may drop what a build does not use. Mounting it in a host is a
59
+ separate matter: what it then reads or writes is governed by the
60
+ workspace boundary and the host's permissions, not by this flag.
61
+
62
+ ## Where this fits
63
+
64
+ The wire shapes both halves agree on. Changing a shape here changes both sides at once, which is the reason it is a package rather than two copies.
65
+
66
+ The twenty packages and how they compose:
67
+ [the package map](https://github.com/oxbshw/watch-skill/blob/main/workspace/docs/packages.md).
68
+ Running DeepWatch, and the gates a change has to pass:
69
+ [the workspace README](https://github.com/oxbshw/watch-skill/tree/main/workspace#readme).
70
+
71
+ ## Attribution
72
+
73
+ Built on DeepSeek Harness · Powered by Watch Skill
74
+
75
+ DeepWatch and Watch Skill are independent projects and are not affiliated
76
+ with or endorsed by DeepSeek. MIT licensed; third-party notices are in
77
+ [THIRD_PARTY_NOTICES.md](https://github.com/oxbshw/watch-skill/blob/main/workspace/THIRD_PARTY_NOTICES.md).
@@ -0,0 +1,114 @@
1
+ /**
2
+ * An approval covers one action, and only the action it was shown.
3
+ *
4
+ * The failure this module exists to prevent is subtle and completely silent
5
+ * when it happens. A person is shown "click Confirm on the payment page" and
6
+ * approves it. Between the approval and the dispatch, something changes the
7
+ * action — a different target, a different page, an extra field — and the
8
+ * approval, which was for a description rather than for a value, still
9
+ * applies. The receipt then records a properly approved action nobody agreed
10
+ * to, and it looks correct in every audit.
11
+ *
12
+ * So an approval binds to a digest of the exact action, and
13
+ * {@link checkApproval} refuses when the action's digest is not the one that
14
+ * was approved. Not warns, not re-prompts silently — refuses, and says which
15
+ * field moved, because "your approval no longer matches" is a sentence a
16
+ * person can act on and "permission denied" is not.
17
+ *
18
+ * Three other properties come along with it, and each closes a way an approval
19
+ * gets stretched past what it covered:
20
+ *
21
+ * - **It expires.** An approval held open across a long session is an approval
22
+ * whose context the person no longer remembers.
23
+ * - **It is single-use by default.** A "yes" to one click is not a yes to
24
+ * every click of that shape.
25
+ * - **It names who gave it.** An approval with no subject cannot be audited,
26
+ * and an unauditable approval is indistinguishable from an assumed one.
27
+ *
28
+ * @module @deepwatch/dsh-contracts/approval
29
+ */
30
+ /** What the person actually agreed to. */
31
+ export interface Approval {
32
+ readonly approvalId: string;
33
+ /**
34
+ * Digest of the exact action that was shown.
35
+ *
36
+ * The load-bearing field. Everything else is metadata about the agreement;
37
+ * this is the agreement.
38
+ */
39
+ readonly inputDigest: string;
40
+ /** A short description of what was shown, for the audit trail and the UI. */
41
+ readonly summary: string;
42
+ readonly grantedByUserId: string;
43
+ readonly grantedAtMs: number;
44
+ readonly expiresAtMs: number;
45
+ /**
46
+ * How many times it may be used.
47
+ *
48
+ * One, unless somebody deliberately granted more. A standing approval is a
49
+ * real thing people sometimes want and never the default.
50
+ */
51
+ readonly maxUses: number;
52
+ readonly uses: number;
53
+ /** Set when the approval was withdrawn before it was used. */
54
+ readonly revokedAtMs: number | null;
55
+ }
56
+ /** An action asking to be dispatched. */
57
+ export interface PendingAction {
58
+ readonly operationId: string;
59
+ readonly inputDigest: string;
60
+ readonly summary: string;
61
+ /** Whether this could change something outside Watch. */
62
+ readonly consequential: boolean;
63
+ }
64
+ /** Why an approval did not cover an action. */
65
+ export type ApprovalRefusalCode = 'no_approval' | 'digest_mismatch' | 'expired' | 'revoked' | 'exhausted' | 'wrong_person';
66
+ /** The outcome of checking an approval against an action. */
67
+ export type ApprovalDecision = {
68
+ readonly ok: true;
69
+ readonly approval: Approval;
70
+ } | {
71
+ readonly ok: false;
72
+ readonly code: ApprovalRefusalCode;
73
+ readonly message: string;
74
+ readonly fix: string;
75
+ };
76
+ /**
77
+ * Whether an approval covers an action.
78
+ *
79
+ * The digest comparison is first and is the whole point. Note that it happens
80
+ * before the expiry check: an action that does not match should say so rather
81
+ * than say "expired", because those two suggest completely different next
82
+ * steps, and telling somebody to approve again when the action changed
83
+ * underneath them is how the change gets approved.
84
+ */
85
+ export declare function checkApproval(approval: Approval | null, action: PendingAction, context: {
86
+ readonly nowMs: number;
87
+ readonly actorUserId: string;
88
+ }): ApprovalDecision;
89
+ /** Record that an approval was spent. */
90
+ export declare function consume(approval: Approval): Approval;
91
+ /** Withdraw an approval before it is used. */
92
+ export declare function revoke(approval: Approval, atMs: number): Approval;
93
+ /**
94
+ * How long an approval lasts by default.
95
+ *
96
+ * Two minutes: long enough for a page to load and an action to dispatch, short
97
+ * enough that it cannot survive somebody walking away from the machine.
98
+ */
99
+ export declare const DEFAULT_APPROVAL_TTL_MS = 120000;
100
+ /**
101
+ * Grant an approval for exactly one action.
102
+ *
103
+ * Takes the action rather than a description, so an approval cannot be minted
104
+ * for something vaguer than what will be dispatched — which is the other half
105
+ * of the digest rule, and the half that is easy to leave out.
106
+ */
107
+ export declare function grantFor(action: PendingAction, context: {
108
+ readonly approvalId: string;
109
+ readonly grantedByUserId: string;
110
+ readonly nowMs: number;
111
+ readonly ttlMs?: number;
112
+ readonly maxUses?: number;
113
+ }): Approval;
114
+ //# sourceMappingURL=approval.d.ts.map
@@ -0,0 +1,108 @@
1
+ /**
2
+ * An approval covers one action, and only the action it was shown.
3
+ *
4
+ * The failure this module exists to prevent is subtle and completely silent
5
+ * when it happens. A person is shown "click Confirm on the payment page" and
6
+ * approves it. Between the approval and the dispatch, something changes the
7
+ * action — a different target, a different page, an extra field — and the
8
+ * approval, which was for a description rather than for a value, still
9
+ * applies. The receipt then records a properly approved action nobody agreed
10
+ * to, and it looks correct in every audit.
11
+ *
12
+ * So an approval binds to a digest of the exact action, and
13
+ * {@link checkApproval} refuses when the action's digest is not the one that
14
+ * was approved. Not warns, not re-prompts silently — refuses, and says which
15
+ * field moved, because "your approval no longer matches" is a sentence a
16
+ * person can act on and "permission denied" is not.
17
+ *
18
+ * Three other properties come along with it, and each closes a way an approval
19
+ * gets stretched past what it covered:
20
+ *
21
+ * - **It expires.** An approval held open across a long session is an approval
22
+ * whose context the person no longer remembers.
23
+ * - **It is single-use by default.** A "yes" to one click is not a yes to
24
+ * every click of that shape.
25
+ * - **It names who gave it.** An approval with no subject cannot be audited,
26
+ * and an unauditable approval is indistinguishable from an assumed one.
27
+ *
28
+ * @module @deepwatch/dsh-contracts/approval
29
+ */
30
+ /** Refuse, with something the person can do about it. */
31
+ function refuse(code, message, fix) {
32
+ return { ok: false, code, message, fix };
33
+ }
34
+ /**
35
+ * Whether an approval covers an action.
36
+ *
37
+ * The digest comparison is first and is the whole point. Note that it happens
38
+ * before the expiry check: an action that does not match should say so rather
39
+ * than say "expired", because those two suggest completely different next
40
+ * steps, and telling somebody to approve again when the action changed
41
+ * underneath them is how the change gets approved.
42
+ */
43
+ export function checkApproval(approval, action, context) {
44
+ if (!action.consequential) {
45
+ // Reading something needs no approval, and requiring one would train
46
+ // people to click through the ones that matter.
47
+ return approval === null
48
+ ? refuse('no_approval', 'No approval was supplied.', 'This action needs none; dispatch it directly.')
49
+ : { ok: true, approval };
50
+ }
51
+ if (approval === null) {
52
+ return refuse('no_approval', `"${action.summary}" could change something outside Watch and has no approval.`, 'Ask the person to approve this specific action.');
53
+ }
54
+ if (approval.inputDigest !== action.inputDigest) {
55
+ return refuse('digest_mismatch', `The approval was granted for "${approval.summary}", and this action is `
56
+ + `"${action.summary}". The approval does not cover it.`, 'Show the person the action as it is now and ask again.');
57
+ }
58
+ if (approval.revokedAtMs !== null) {
59
+ return refuse('revoked', 'That approval was withdrawn.', 'Ask again if it is still wanted.');
60
+ }
61
+ if (approval.expiresAtMs <= context.nowMs) {
62
+ return refuse('expired', 'That approval has expired.', 'Ask again, so the person is deciding with the current context.');
63
+ }
64
+ if (approval.uses >= approval.maxUses) {
65
+ return refuse('exhausted', `That approval was for ${String(approval.maxUses)} use(s) and has been used `
66
+ + `${String(approval.uses)} time(s).`, 'Ask again for this action.');
67
+ }
68
+ if (approval.grantedByUserId !== context.actorUserId) {
69
+ return refuse('wrong_person', 'That approval was granted by somebody else.', 'Approvals are not transferable. Ask the person acting to approve it themselves.');
70
+ }
71
+ return { ok: true, approval };
72
+ }
73
+ /** Record that an approval was spent. */
74
+ export function consume(approval) {
75
+ return { ...approval, uses: approval.uses + 1 };
76
+ }
77
+ /** Withdraw an approval before it is used. */
78
+ export function revoke(approval, atMs) {
79
+ return { ...approval, revokedAtMs: atMs };
80
+ }
81
+ /**
82
+ * How long an approval lasts by default.
83
+ *
84
+ * Two minutes: long enough for a page to load and an action to dispatch, short
85
+ * enough that it cannot survive somebody walking away from the machine.
86
+ */
87
+ export const DEFAULT_APPROVAL_TTL_MS = 120_000;
88
+ /**
89
+ * Grant an approval for exactly one action.
90
+ *
91
+ * Takes the action rather than a description, so an approval cannot be minted
92
+ * for something vaguer than what will be dispatched — which is the other half
93
+ * of the digest rule, and the half that is easy to leave out.
94
+ */
95
+ export function grantFor(action, context) {
96
+ return {
97
+ approvalId: context.approvalId,
98
+ inputDigest: action.inputDigest,
99
+ summary: action.summary,
100
+ grantedByUserId: context.grantedByUserId,
101
+ grantedAtMs: context.nowMs,
102
+ expiresAtMs: context.nowMs + (context.ttlMs ?? DEFAULT_APPROVAL_TTL_MS),
103
+ maxUses: context.maxUses ?? 1,
104
+ uses: 0,
105
+ revokedAtMs: null,
106
+ };
107
+ }
108
+ //# sourceMappingURL=approval.js.map
@@ -0,0 +1,248 @@
1
+ /**
2
+ * What a person actually chose: which provider, which model, for which role.
3
+ *
4
+ * {@link module:@deepwatch/dsh-contracts/readiness} answers "can this run?"
5
+ * from four separate facts. This module is where three of those facts are
6
+ * *kept* — a durable document recording the decisions, so that reopening the
7
+ * product finds the same bindings rather than an empty screen and a composer
8
+ * pointed at somebody else's default.
9
+ *
10
+ * **A binding is a reference, never a credential.** The document below can be
11
+ * read by anything: it is written to the Harness's own settings file, it rides
12
+ * the settings RPC, it appears in Diagnostics, and it is included in a session
13
+ * export. None of that is safe unless the rule is absolute, so it is: the only
14
+ * credential-shaped field here is {@link RoleBindingRecord.credentialRef}, an
15
+ * opaque handle the Host resolves against its own store. No value, no prefix,
16
+ * no suffix, no length, no hash. {@link assertNoSecretMaterial} is the test
17
+ * this file is held to.
18
+ *
19
+ * **Nothing is bound implicitly.** There is no "default role", no inheritance
20
+ * from one role to another, and no provider that becomes bound because it was
21
+ * the only one configured. A role with no entry in {@link WatchBindings.roles}
22
+ * is unbound, and unbound means the composer refuses. That is the whole point
23
+ * of the module: the failure it exists to prevent was a product that treated a
24
+ * saved credential as a decision the person never made.
25
+ *
26
+ * @module @deepwatch/dsh-contracts/bindings
27
+ */
28
+ import type { Modality, RoleBinding } from './readiness.js';
29
+ /**
30
+ * The settings namespace this document lives in.
31
+ *
32
+ * A DeepWatch-owned section of the Harness's own user-settings document, which
33
+ * is what makes the binding durable, hot-reloaded and editable by hand without
34
+ * DeepWatch inventing a second configuration store beside the one the product
35
+ * already has.
36
+ */
37
+ export declare const BINDINGS_NAMESPACE = "watch-bindings";
38
+ /**
39
+ * The document revision this build writes.
40
+ *
41
+ * Read forward, never rewritten in place: an older document is migrated on
42
+ * read and a newer one is refused rather than silently reinterpreted, because
43
+ * misreading a binding routes somebody's prompt somewhere they did not choose.
44
+ */
45
+ export declare const BINDINGS_VERSION = 1;
46
+ /**
47
+ * The roles a person can bind to a provider, in the order setup presents them.
48
+ *
49
+ * These ids are `RoleId`s from `@deepwatch/dsh-technology`, spelled here rather
50
+ * than imported because `contracts` is the package everything else depends on
51
+ * and must not depend on anything. `tests/bindings-store.test.mjs` asserts the
52
+ * two lists agree, so the duplication is checked rather than trusted -- one
53
+ * role vocabulary with a gate on it, instead of two that drift.
54
+ *
55
+ * It is a *subset*. `verifier`, `ocr_layout`, `reranking` and
56
+ * `speaker_diarization` are served by local engines rather than chosen from a
57
+ * provider catalogue, so offering them here would offer a choice that is not
58
+ * there.
59
+ *
60
+ * `agent_model` is first and is the only one the first conversation needs. The
61
+ * rest are progressive: a product that demanded five bindings before the first
62
+ * message would be a product nobody finished configuring.
63
+ */
64
+ export declare const BINDABLE_ROLES: readonly ["agent_model", "visual_perception", "asr", "audio_understanding", "embeddings"];
65
+ /** One of the roles this product knows how to bind. */
66
+ export type BindableRole = (typeof BINDABLE_ROLES)[number];
67
+ /** The role the first conversation needs, named once so nothing spells it twice. */
68
+ export declare const PRIMARY_ROLE: BindableRole;
69
+ /**
70
+ * What each role is called on screen.
71
+ *
72
+ * `agent_model` is labelled **Chat**, and the difference is not cosmetic. A
73
+ * person configuring this product is not choosing an "agent model" -- they are
74
+ * choosing what answers them in the surface the Harness calls Chat, and every
75
+ * blocked-composer message and setup step has to name the thing they are
76
+ * looking at. The id stays `agent_model` because that is the vocabulary the
77
+ * descriptors and the routing rules already use.
78
+ */
79
+ export declare const ROLE_LABEL: Readonly<Record<BindableRole, string>>;
80
+ /** Whether a string is a role this product binds. */
81
+ export declare function isBindableRole(value: string): value is BindableRole;
82
+ /**
83
+ * What each role is for, in a person's words.
84
+ *
85
+ * Here rather than in a component because the setup flow, the Role Bindings
86
+ * screen and the blocked-composer card all name the same role, and three
87
+ * copies of this sentence would eventually be three different sentences.
88
+ */
89
+ export declare const ROLE_PURPOSE: Readonly<Record<BindableRole, string>>;
90
+ /** The modalities each role's work actually needs a route to support. */
91
+ export declare const ROLE_MODALITIES: Readonly<Record<BindableRole, readonly Modality[]>>;
92
+ /**
93
+ * One stored decision.
94
+ *
95
+ * `boundAt` exists so Role Bindings can say when a choice was made rather than
96
+ * presenting every binding as timeless; it is a decision timestamp, not a
97
+ * verification one. Whether the binding *works* is never stored — that is
98
+ * derived at read time from the live credential and route facts, because a
99
+ * stored "verified" would go stale the moment a key was revoked and would be
100
+ * the same lie about readiness this whole subsystem exists to stop telling.
101
+ */
102
+ export interface RoleBindingRecord {
103
+ /** The provider route id, as the Harness's catalogue names it. */
104
+ readonly provider: string;
105
+ /** The provider-owned model id. */
106
+ readonly model: string;
107
+ /**
108
+ * Opaque handle the Host resolves against its own credential store.
109
+ *
110
+ * Null when the route needs no credential (a local endpoint). Never a value.
111
+ */
112
+ readonly credentialRef: string | null;
113
+ /** ISO-8601 instant the person made this choice. */
114
+ readonly boundAt: string;
115
+ /**
116
+ * What kind of actor wrote this binding.
117
+ *
118
+ * A binding decides where a person's credential gets sent, and a record that
119
+ * cannot say who decided cannot be audited. A document written by somebody
120
+ * clicking Save in Role Bindings and one written by a script are the same
121
+ * bytes, and only one of them is a decision the person made.
122
+ *
123
+ * A *kind*, never an identity. No user name, no account, no machine: those
124
+ * are personal identifiers, this document is exported, rides the settings
125
+ * RPC and appears in Diagnostics, and the question worth answering is "did a
126
+ * person choose this" rather than "which person".
127
+ */
128
+ readonly boundBy: BindingActor;
129
+ }
130
+ /**
131
+ * Who wrote a binding.
132
+ *
133
+ * `unknown` is the honest reading of a document written before this field
134
+ * existed, or by a hand edit, or by any writer that did not say. It is not a
135
+ * defect and it is not `person`: presenting an unattributed binding as a
136
+ * deliberate choice is exactly the substitution this field exists to prevent.
137
+ */
138
+ export type BindingActor = 'person' | 'setup' | 'unknown';
139
+ /** The actors a stored document may name. */
140
+ export declare const BINDING_ACTORS: readonly BindingActor[];
141
+ /** Whether a stored value is an actor this build understands. */
142
+ export declare function isBindingActor(value: unknown): value is BindingActor;
143
+ /** The whole document, as stored. */
144
+ export interface WatchBindings {
145
+ readonly version: number;
146
+ /** One entry per bound role. An absent role is unbound; there is no default. */
147
+ readonly roles: Readonly<Partial<Record<BindableRole, RoleBindingRecord>>>;
148
+ }
149
+ /** The document a profile that has never been configured has. */
150
+ export declare const EMPTY_BINDINGS: WatchBindings;
151
+ /**
152
+ * A model id worth storing.
153
+ *
154
+ * Deliberately permissive about shape — provider model ids are provider-owned
155
+ * and this product does not get to decide that `openai/gpt-4o` is malformed —
156
+ * and deliberately strict about the things that make a stored value dangerous:
157
+ * control characters, newlines and absurd length, all of which arrive from a
158
+ * hand-edited settings file rather than from the picker.
159
+ */
160
+ export declare function isStorableId(value: unknown): value is string;
161
+ /**
162
+ * Read a stored document, keeping only what is well-formed.
163
+ *
164
+ * A hand-edited settings file is a supported way to configure this product, so
165
+ * a malformed entry must not take the whole document with it: the bad role is
166
+ * dropped and the rest survive. Dropping is the safe direction — an unbound
167
+ * role refuses at the composer, where a person is told what to fix, whereas a
168
+ * half-read binding would route a prompt somewhere nobody chose.
169
+ *
170
+ * @param raw - whatever the settings document held.
171
+ * @returns a document this build can act on.
172
+ */
173
+ export declare function readBindings(raw: unknown): WatchBindings;
174
+ /**
175
+ * The document with one role bound, as a new value.
176
+ *
177
+ * Never mutates: the caller holds a snapshot it may still be rendering from,
178
+ * and a document edited underneath a React tree is a stale-render bug that
179
+ * shows somebody the binding they had a moment ago.
180
+ */
181
+ export declare function withBinding(current: WatchBindings, role: BindableRole, record: RoleBindingRecord): WatchBindings;
182
+ /** The document with one role unbound. */
183
+ export declare function withoutBinding(current: WatchBindings, role: BindableRole): WatchBindings;
184
+ /**
185
+ * The readiness-shaped view of one stored role, or null when it is unbound.
186
+ *
187
+ * The join between this module and `readiness`: storage keeps records, the
188
+ * gate takes {@link RoleBinding}s, and this is the only place that converts
189
+ * one into the other — so the modalities a role is checked against always come
190
+ * from {@link ROLE_MODALITIES} rather than from whatever a call site guessed.
191
+ */
192
+ export declare function bindingFor(bindings: WatchBindings, role: BindableRole): RoleBinding | null;
193
+ /** Whether a role has a stored decision at all. Not whether it can run. */
194
+ export declare function isBound(bindings: WatchBindings, role: BindableRole): boolean;
195
+ /**
196
+ * Every provider a stored binding names, once each.
197
+ *
198
+ * What Settings uses to decide which providers to show credential state for:
199
+ * the ones a person actually pointed something at, rather than all
200
+ * thirty-seven routes the catalogue carries.
201
+ */
202
+ export declare function boundProviders(bindings: WatchBindings): readonly string[];
203
+ /**
204
+ * Whether a provider/model pair is one this profile actually bound.
205
+ *
206
+ * The authoritative question, and deliberately the *narrow* one. It does not
207
+ * ask whether the route exists, whether a credential is stored, or whether the
208
+ * provider is reachable — those are the Host's to answer at the moment of the
209
+ * request. It asks the only thing a stored document can answer: did somebody
210
+ * choose this pair for something.
211
+ *
212
+ * That is what makes it usable as a gate at a routing boundary. A request for
213
+ * a pair nobody bound is a request nobody authorised, whatever the client that
214
+ * produced it believed — a stale tab holding a selection that has since been
215
+ * changed, or a caller that set one directly and skipped the screens.
216
+ *
217
+ * Any bound role counts, not only the one being served: a person who bound
218
+ * OpenRouter to Chat has authorised that route, and the title and compaction
219
+ * calls that ride the same selection are the same authorisation, not new ones.
220
+ *
221
+ * @param bindings - the stored document.
222
+ * @param provider - the route the request names.
223
+ * @param model - the model the request names.
224
+ * @returns whether some role in this profile is bound to exactly that pair.
225
+ */
226
+ export declare function isRoutePermitted(bindings: WatchBindings, provider: string, model: string): boolean;
227
+ /**
228
+ * Every distinct provider/model pair this profile bound, for a diagnostic.
229
+ *
230
+ * A refusal that says "this route is not bound" is not much use without the
231
+ * ability to say what *is*, and that list is a set of choices rather than
232
+ * anything sensitive — no credential, no reference, no host path.
233
+ */
234
+ export declare function permittedRoutes(bindings: WatchBindings): readonly string[];
235
+ /**
236
+ * Throw when a document about to be stored or shown carries secret material.
237
+ *
238
+ * A programming error rather than a runtime condition: every write path builds
239
+ * this document from a picker, so a value matching one of these means a code
240
+ * path has started copying a credential into a place that is read back in
241
+ * plain text. Failing loudly at the write is the only point where that is
242
+ * still cheap to fix.
243
+ *
244
+ * @param where - the surface being guarded, for a message that can be acted on.
245
+ * @param bindings - the document about to leave a trusted boundary.
246
+ */
247
+ export declare function assertNoSecretMaterial(where: string, bindings: WatchBindings): void;
248
+ //# sourceMappingURL=bindings.d.ts.map