@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 +21 -0
- package/README.md +77 -0
- package/lib/approval.d.ts +114 -0
- package/lib/approval.js +108 -0
- package/lib/bindings.d.ts +248 -0
- package/lib/bindings.js +333 -0
- package/lib/digests.d.ts +77 -0
- package/lib/digests.js +103 -0
- package/lib/execution.d.ts +259 -0
- package/lib/execution.js +213 -0
- package/lib/failures.d.ts +145 -0
- package/lib/failures.js +204 -0
- package/lib/identity.d.ts +54 -0
- package/lib/identity.js +74 -0
- package/lib/index.d.ts +382 -0
- package/lib/index.js +79 -0
- package/lib/language.d.ts +158 -0
- package/lib/language.js +202 -0
- package/lib/paths.d.ts +194 -0
- package/lib/paths.js +286 -0
- package/lib/presentation.d.ts +90 -0
- package/lib/presentation.js +158 -0
- package/lib/query/validate.d.ts +74 -0
- package/lib/query/validate.js +243 -0
- package/lib/query/wire.d.ts +409 -0
- package/lib/query/wire.js +37 -0
- package/lib/query.d.ts +362 -0
- package/lib/query.js +431 -0
- package/lib/readiness.d.ts +198 -0
- package/lib/readiness.js +209 -0
- package/lib/workspace.d.ts +141 -0
- package/lib/workspace.js +158 -0
- package/package.json +60 -0
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
|
package/lib/approval.js
ADDED
|
@@ -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
|