@threahq/bot-runtime-client 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 +99 -0
- package/archive-grace.d.ts +112 -0
- package/attachment-files.d.ts +15 -0
- package/crypto.d.ts +206 -0
- package/index.d.ts +14 -0
- package/index.js +2344 -0
- package/index.js.map +19 -0
- package/invocation-control.d.ts +128 -0
- package/keyring.d.ts +242 -0
- package/package.json +49 -0
- package/sealed-stream-client.d.ts +145 -0
- package/sealed.d.ts +305 -0
- package/transport-test-helpers.d.ts +53 -0
- package/transport.d.ts +132 -0
- package/types.d.ts +243 -0
- package/user-key.d.ts +53 -0
- package/ws-hint.d.ts +17 -0
package/types.d.ts
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import type { StreamEnvelope } from "./crypto.js";
|
|
2
|
+
import type { InvocationControlScheduler } from "./invocation-control.js";
|
|
3
|
+
/**
|
|
4
|
+
* The ack the `/bot` write events return. `ok: true` means the server persisted
|
|
5
|
+
* the write; `ok: false` carries a `code` (`NOT_FOUND`, `FORBIDDEN`,
|
|
6
|
+
* `INVALID_PAYLOAD`, `INTERNAL_ERROR`, …) the client uses to tell a terminal
|
|
7
|
+
* failure from one worth an HTTP retry.
|
|
8
|
+
*/
|
|
9
|
+
export interface BotWriteAck {
|
|
10
|
+
ok: boolean;
|
|
11
|
+
data?: Record<string, unknown>;
|
|
12
|
+
code?: string;
|
|
13
|
+
message?: string;
|
|
14
|
+
}
|
|
15
|
+
/** One trace step. A single `recordSteps` call may carry several. */
|
|
16
|
+
export interface StepFrame {
|
|
17
|
+
stepType: string;
|
|
18
|
+
content: string;
|
|
19
|
+
/**
|
|
20
|
+
* Idempotency key. The transport mints one per frame if absent and sends the
|
|
21
|
+
* same value over WS and the HTTP fallback, so a step can never be persisted
|
|
22
|
+
* twice under the same key (the server dedups on it).
|
|
23
|
+
*/
|
|
24
|
+
clientStepId?: string;
|
|
25
|
+
/**
|
|
26
|
+
* `started` opens a tool row before the tool finishes (tool_call/tool_error
|
|
27
|
+
* only). A started frame must carry its own clientStepId; the finishing frame
|
|
28
|
+
* reuses it.
|
|
29
|
+
*/
|
|
30
|
+
phase?: "started";
|
|
31
|
+
/** Wall-clock tool runtime on the finishing frame; requires clientStepId. */
|
|
32
|
+
durationMs?: number;
|
|
33
|
+
}
|
|
34
|
+
/** The `bot:hello` registration payload — mirrors the server's `helloSchema`. */
|
|
35
|
+
export interface BotRuntimeHello {
|
|
36
|
+
instanceId: string;
|
|
37
|
+
runtimeKind: string;
|
|
38
|
+
runtimeSessionId?: string;
|
|
39
|
+
displayName?: string | null;
|
|
40
|
+
status?: "available" | "busy" | "offline" | "error";
|
|
41
|
+
acceptingInvocations?: boolean;
|
|
42
|
+
supportedCapabilities: string[];
|
|
43
|
+
capabilities?: Record<string, unknown>;
|
|
44
|
+
manifest?: {
|
|
45
|
+
output: {
|
|
46
|
+
reply?: boolean;
|
|
47
|
+
trace?: boolean;
|
|
48
|
+
sources?: boolean;
|
|
49
|
+
};
|
|
50
|
+
input?: {
|
|
51
|
+
updates: "live" | "restart";
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
/** ISO cursor echoed from the previous hello ack so the bootstrap only replays unseen events. */
|
|
55
|
+
sinceCursor?: string;
|
|
56
|
+
/**
|
|
57
|
+
* This install's end-to-end keyring (see `E2eKeyring`). The server stores the
|
|
58
|
+
* advertised set as the instance's complete keyring, so it must ride every
|
|
59
|
+
* hello AND presence write: omitting it on a heartbeat leaves the stored set
|
|
60
|
+
* alone, and sending `[]` unregisters every key.
|
|
61
|
+
*/
|
|
62
|
+
e2eKeys?: {
|
|
63
|
+
keyId: string;
|
|
64
|
+
publicKey: string;
|
|
65
|
+
streamId?: string;
|
|
66
|
+
}[];
|
|
67
|
+
/**
|
|
68
|
+
* The keyring's default key, for a server from before the registry. Both name
|
|
69
|
+
* the same key, so a mixed-version rollout addresses one key either way.
|
|
70
|
+
*/
|
|
71
|
+
publicKey?: string;
|
|
72
|
+
publicKeyId?: string;
|
|
73
|
+
}
|
|
74
|
+
/** `bot:e2e_grant`: this bot became an actor on a sealed scratchpad root. */
|
|
75
|
+
export interface BotE2eGrantPayload {
|
|
76
|
+
workspaceId: string;
|
|
77
|
+
botId: string;
|
|
78
|
+
streamId: string;
|
|
79
|
+
}
|
|
80
|
+
/** `bot:e2e_revoke`: this bot is no longer an actor on a sealed scratchpad root. */
|
|
81
|
+
export interface BotE2eRevokePayload {
|
|
82
|
+
workspaceId: string;
|
|
83
|
+
botId: string;
|
|
84
|
+
streamId: string;
|
|
85
|
+
}
|
|
86
|
+
/** The bootstrap snapshot the server returns in the `bot:hello` ack. */
|
|
87
|
+
export interface BotHelloBootstrap {
|
|
88
|
+
serverGeneratedAt?: string;
|
|
89
|
+
/**
|
|
90
|
+
* This bot's own id, as the server authenticated it. A sealed decision card's
|
|
91
|
+
* AAD names its requester, so a runtime cannot seal one until it knows which
|
|
92
|
+
* bot it is. Absent against a server from before that field shipped.
|
|
93
|
+
*/
|
|
94
|
+
botId?: string;
|
|
95
|
+
availableInvocations: unknown[];
|
|
96
|
+
ownedClaims: unknown[];
|
|
97
|
+
/** Sealed scratchpads this bot is an actor on — the catch-up for `bot:e2e_grant`. */
|
|
98
|
+
e2eGrantedStreamIds: string[];
|
|
99
|
+
}
|
|
100
|
+
/** Wakeup/hint callbacks the transport fires from server→client socket events. */
|
|
101
|
+
/** Slim nudge emitted to the workspace runtime room when a delegation is created (roadmap 5.4). */
|
|
102
|
+
export interface DelegationAvailableNudge {
|
|
103
|
+
workspaceId: string;
|
|
104
|
+
streamId: string;
|
|
105
|
+
delegationId?: string;
|
|
106
|
+
title?: string;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The decision outcome pushed to the requesting runtime's session room
|
|
110
|
+
* (`decision:resolved` / `decision:cancelled`). Mirrors the server's
|
|
111
|
+
* `botDecisionPayloadSchema`; `extensions/*` do not depend on `@threahq/types`.
|
|
112
|
+
*/
|
|
113
|
+
export interface BotDecisionPayload {
|
|
114
|
+
workspaceId: string;
|
|
115
|
+
botId: string;
|
|
116
|
+
streamId: string;
|
|
117
|
+
runtimeSessionId: string;
|
|
118
|
+
decisionId: string;
|
|
119
|
+
status: DecisionRequestStatus;
|
|
120
|
+
optionId: string | null;
|
|
121
|
+
note: string | null;
|
|
122
|
+
/** The sealed half of the answer's note, on an encrypted stream; `note` is null there. */
|
|
123
|
+
noteCiphertext: string | null;
|
|
124
|
+
noteEnvelope: StreamEnvelope | null;
|
|
125
|
+
/** Who answered — a sealed note's AAD names them, so opening one needs it. */
|
|
126
|
+
decidedBy: string | null;
|
|
127
|
+
version: number;
|
|
128
|
+
}
|
|
129
|
+
export type DecisionRequestStatus = "open" | "resolved" | "cancelled" | "expired";
|
|
130
|
+
export interface DecisionOption {
|
|
131
|
+
id: string;
|
|
132
|
+
/** Omitted on a sealed card: the labels travel inside the ciphertext. */
|
|
133
|
+
label?: string;
|
|
134
|
+
tone?: "primary" | "neutral" | "destructive";
|
|
135
|
+
}
|
|
136
|
+
export interface DecisionResolution {
|
|
137
|
+
optionId: string;
|
|
138
|
+
note?: string;
|
|
139
|
+
/** The sealed note, on an encrypted stream. */
|
|
140
|
+
noteCiphertext?: string;
|
|
141
|
+
noteEnvelope?: StreamEnvelope;
|
|
142
|
+
/** Absent when the resolution was reconstructed from a socket push, which carries only the answer. */
|
|
143
|
+
decidedBy?: string;
|
|
144
|
+
decidedAt?: string;
|
|
145
|
+
}
|
|
146
|
+
/** Body of `POST /streams/:streamId/decisions`. */
|
|
147
|
+
export interface CreateDecisionRequestBody {
|
|
148
|
+
/** Omitted on a sealed card: the title travels inside the ciphertext. */
|
|
149
|
+
title?: string;
|
|
150
|
+
bodyMarkdown?: string;
|
|
151
|
+
options: DecisionOption[];
|
|
152
|
+
/**
|
|
153
|
+
* The card's id, minted by the requester. Required with `sealed` — the AAD
|
|
154
|
+
* binds the ciphertext to the id, so the id has to exist before the seal.
|
|
155
|
+
*/
|
|
156
|
+
decisionId?: string;
|
|
157
|
+
/** The sealed question, on an encrypted stream. Ids and tones stay in the clear. */
|
|
158
|
+
sealed?: {
|
|
159
|
+
ciphertext: string;
|
|
160
|
+
envelope: StreamEnvelope;
|
|
161
|
+
};
|
|
162
|
+
allowNote?: boolean;
|
|
163
|
+
externalRef?: string;
|
|
164
|
+
expiresInMs?: number;
|
|
165
|
+
runtimeSessionId: string;
|
|
166
|
+
invocationId?: string;
|
|
167
|
+
}
|
|
168
|
+
/** Minimal mirror of the server's `DecisionRequest` wire shape — what the SDK reads. */
|
|
169
|
+
export interface DecisionRequest {
|
|
170
|
+
id: string;
|
|
171
|
+
workspaceId: string;
|
|
172
|
+
streamId: string;
|
|
173
|
+
requesterBotId?: string;
|
|
174
|
+
requesterRuntimeSessionId?: string;
|
|
175
|
+
requesterInvocationId?: string;
|
|
176
|
+
status: DecisionRequestStatus;
|
|
177
|
+
title: string;
|
|
178
|
+
options: DecisionOption[];
|
|
179
|
+
/** Present on a sealed card; `title` and every option `label` are placeholders then. */
|
|
180
|
+
ciphertext?: string;
|
|
181
|
+
envelope?: StreamEnvelope;
|
|
182
|
+
allowNote: boolean;
|
|
183
|
+
externalRef?: string;
|
|
184
|
+
resolution?: DecisionResolution;
|
|
185
|
+
expiresAt?: string;
|
|
186
|
+
version: number;
|
|
187
|
+
}
|
|
188
|
+
export interface BotRuntimeTransportCallbacks {
|
|
189
|
+
/** New work is claimable — the runtime should drain its claim loop. */
|
|
190
|
+
onInvocationAvailable?: () => void;
|
|
191
|
+
/** A delegation was created somewhere in the workspace — a delegation runner should drain. */
|
|
192
|
+
onDelegationAvailable?: (payload: DelegationAvailableNudge) => void;
|
|
193
|
+
/** Another instance claimed an invocation (stop racing). */
|
|
194
|
+
onInvocationClaimed?: (payload: unknown) => void;
|
|
195
|
+
/** The active scratchpad actor changed for some stream. */
|
|
196
|
+
onActiveActorChanged?: (payload: unknown) => void;
|
|
197
|
+
/** This bot was invited into a sealed scratchpad; a per-stream keyring mints its key here. */
|
|
198
|
+
onE2eGrant?: (payload: BotE2eGrantPayload) => void;
|
|
199
|
+
/** This bot's grant on a sealed scratchpad was taken back; a per-stream keyring drops its key here. */
|
|
200
|
+
onE2eRevoke?: (payload: BotE2eRevokePayload) => void;
|
|
201
|
+
/** The server asked the runtime to re-announce itself; the transport re-sends hello automatically and also fires this. */
|
|
202
|
+
onResync?: () => void;
|
|
203
|
+
/** The scratchpad this runtime session is linked to was archived; the link is ended server-side. Wind down. */
|
|
204
|
+
onSessionArchived?: (payload: unknown) => void;
|
|
205
|
+
/** The archived scratchpad was unarchived; the link is active again server-side. Cancel the wind-down and reattach. */
|
|
206
|
+
onSessionRestored?: (payload: unknown) => void;
|
|
207
|
+
/** The `bot:hello` ack landed; carries the bootstrap snapshot. */
|
|
208
|
+
onBootstrap?: (bootstrap: BotHelloBootstrap) => void;
|
|
209
|
+
/** A decision this runtime opened was answered; the requester unblocks on it. */
|
|
210
|
+
onDecisionResolved?: (payload: BotDecisionPayload) => void;
|
|
211
|
+
/** A decision this runtime opened was cancelled (or expired) without an answer. */
|
|
212
|
+
onDecisionCancelled?: (payload: BotDecisionPayload) => void;
|
|
213
|
+
/** A hello-ready socket became unavailable; wake any HTTP delivery backstop parked on the healthy-socket cadence. */
|
|
214
|
+
onDisconnected?: () => void;
|
|
215
|
+
}
|
|
216
|
+
export interface BotRuntimeTransportOptions {
|
|
217
|
+
baseUrl: string;
|
|
218
|
+
workspaceId: string;
|
|
219
|
+
apiKey: string;
|
|
220
|
+
hello: BotRuntimeHello;
|
|
221
|
+
beforeHello?: (hello: BotRuntimeHello) => void;
|
|
222
|
+
callbacks?: BotRuntimeTransportCallbacks;
|
|
223
|
+
/** How long to wait for a write-event ack before falling back to HTTP. Default 5s. */
|
|
224
|
+
wsAckTimeoutMs?: number;
|
|
225
|
+
/** Socket.IO reconnection backoff ceiling. Default 30s. */
|
|
226
|
+
reconnectionDelayMaxMs?: number;
|
|
227
|
+
/** HTTP fallback request timeout. Default 30s. */
|
|
228
|
+
fetchTimeoutMs?: number;
|
|
229
|
+
/** Observed-claim retry/poll cadence. Default 5s. */
|
|
230
|
+
controlRetryDelayMs?: number;
|
|
231
|
+
/** Earliest observed-claim renewal delay. Default 1s. */
|
|
232
|
+
controlMinRenewDelayMs?: number;
|
|
233
|
+
/** Optional observed-claim timer scheduler for deterministic hosts/tests. */
|
|
234
|
+
controlScheduler?: InvocationControlScheduler;
|
|
235
|
+
/**
|
|
236
|
+
* How long a socket may sit disconnected before `connect()` tears it down and
|
|
237
|
+
* redials from a fresh ws hint. Default 3 min. Socket.IO's own retry loop
|
|
238
|
+
* handles brief drops; this backstop catches the wedged states it can't — a
|
|
239
|
+
* stale ws hint after the backend moved, or a client stuck post-kick.
|
|
240
|
+
*/
|
|
241
|
+
staleSocketRedialMs?: number;
|
|
242
|
+
log?: (message: string) => void;
|
|
243
|
+
}
|
package/user-key.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type WebCryptoKey } from "./crypto.js";
|
|
2
|
+
/**
|
|
3
|
+
* Recovering a user's identity key from a passphrase, outside the browser.
|
|
4
|
+
*
|
|
5
|
+
* The web app wraps the UIK's private half in AES-256-GCM under an Argon2id
|
|
6
|
+
* KEK and hands the server nothing but the ciphertext. A CLI on a machine that
|
|
7
|
+
* has never run the web app fetches that bundle from
|
|
8
|
+
* `GET /api/v1/workspaces/{ws}/me/e2e-key` and repeats the derivation here.
|
|
9
|
+
*
|
|
10
|
+
* Every constant below is wire format shared with
|
|
11
|
+
* `apps/frontend/src/lib/crypto/{passphrase,keys}.ts`. `user-key.parity.test.ts`
|
|
12
|
+
* wraps with the browser code and unwraps with this one, so drift fails CI
|
|
13
|
+
* rather than locking someone out of their own streams.
|
|
14
|
+
*/
|
|
15
|
+
export interface KdfParams {
|
|
16
|
+
algorithm: "argon2id";
|
|
17
|
+
/** Memory cost in kibibytes (Argon2 `m`). */
|
|
18
|
+
m: number;
|
|
19
|
+
/** Iteration count (Argon2 `t`). */
|
|
20
|
+
t: number;
|
|
21
|
+
/** Parallelism degree (Argon2 `p`). */
|
|
22
|
+
p: number;
|
|
23
|
+
/** Argon2 algorithm version. 19 = `0x13`, current as of RFC 9106. */
|
|
24
|
+
version: number;
|
|
25
|
+
}
|
|
26
|
+
export declare const DEFAULT_KDF_PARAMS: KdfParams;
|
|
27
|
+
/**
|
|
28
|
+
* Derive the 32-byte AES-GCM key-encryption key a wrapped bundle was sealed
|
|
29
|
+
* under. Non-extractable: nothing downstream needs the raw bytes, and the
|
|
30
|
+
* passphrase should not become recoverable material sitting in a variable.
|
|
31
|
+
*/
|
|
32
|
+
export declare function deriveKEK(passphrase: string, salt: Uint8Array, params?: KdfParams): Promise<WebCryptoKey>;
|
|
33
|
+
/** The GCM tag rejected the derived KEK: a wrong passphrase, or a tampered bundle. */
|
|
34
|
+
export declare class WrongPassphraseError extends Error {
|
|
35
|
+
constructor();
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Open a `[version (1) | iv (12) | AES-GCM ciphertext]` bundle and re-import
|
|
39
|
+
* the X25519 private key. A tag mismatch is a `WrongPassphraseError`; a
|
|
40
|
+
* malformed or unsupported bundle throws its own error, so a caller can tell
|
|
41
|
+
* "you typed it wrong" from "this bundle is not what we can read".
|
|
42
|
+
*/
|
|
43
|
+
export declare function unwrapPrivate(bundle: Uint8Array, kek: WebCryptoKey): Promise<WebCryptoKey>;
|
|
44
|
+
export interface UnlockUserKeyInput {
|
|
45
|
+
passphrase: string;
|
|
46
|
+
/** `encryptedPrivateBundle` exactly as the API returns it. */
|
|
47
|
+
encryptedPrivateBundle: Uint8Array;
|
|
48
|
+
/** `kdfSalt` exactly as the API returns it. */
|
|
49
|
+
kdfSalt: Uint8Array;
|
|
50
|
+
kdfParams: KdfParams;
|
|
51
|
+
}
|
|
52
|
+
/** The whole passphrase → private key path in one call. */
|
|
53
|
+
export declare function unlockUserKey(input: UnlockUserKeyInput): Promise<WebCryptoKey>;
|
package/ws-hint.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface WsHint {
|
|
2
|
+
url: string;
|
|
3
|
+
path: string;
|
|
4
|
+
namespace: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function isObject(value: unknown): value is Record<string, unknown>;
|
|
7
|
+
/**
|
|
8
|
+
* Normalize the `{ wsUrl }` the edge workspace-router returns from
|
|
9
|
+
* `GET /api/workspaces/:id/config` into a connectable hint. Defaults match the
|
|
10
|
+
* server: the default Socket.IO path and the `/bot` namespace.
|
|
11
|
+
*/
|
|
12
|
+
export declare function parseWsHint(value: unknown): WsHint | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* Append the `/bot` namespace to the pathname while preserving any query string.
|
|
15
|
+
* A naive `${url}${namespace}` concat breaks staging URLs that carry `?region=…`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function buildBotSocketUrl(hint: WsHint): string;
|