@byok-sdk/client 0.11.0 → 0.13.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/README.md +28 -3
- package/dist/adapters/index.js +74 -5
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/pi/pi-adapter.d.ts +3 -0
- package/dist/adapters/pi/subagents-policy-extension.js +1 -1
- package/dist/adapters/pi/subagents-policy-extension.js.map +1 -1
- package/dist/agent-home.d.ts +107 -0
- package/dist/agent-memory/index.d.ts +1 -1
- package/dist/agent-memory/index.js +8 -5
- package/dist/agent-memory/index.js.map +1 -1
- package/dist/bin/byok-agent-memory-mcp.js.map +1 -1
- package/dist/bin/byok-agent-message-mcp.js.map +1 -1
- package/dist/bin/byok-agent-team-mcp.js.map +1 -1
- package/dist/bin/byok-agent.js +1279 -642
- package/dist/bin/byok-agent.js.map +1 -1
- package/dist/bin/byok-approval-mcp.js.map +1 -1
- package/dist/daemon/agent-egress-controller.d.ts +4 -0
- package/dist/daemon/auth-manager.d.ts +20 -0
- package/dist/daemon/blob-client.d.ts +24 -6
- package/dist/daemon/connection-manager.d.ts +69 -126
- package/dist/daemon/control-protocol.d.ts +8 -1
- package/dist/daemon/create-daemon.d.ts +28 -13
- package/dist/daemon/device-credential-store.d.ts +16 -0
- package/dist/daemon/long-poll-transport.d.ts +32 -23
- package/dist/daemon/observer.d.ts +1 -1
- package/dist/daemon/replay-cursor.d.ts +9 -0
- package/dist/daemon/task-runner.d.ts +22 -0
- package/dist/daemon/url.d.ts +17 -13
- package/dist/diagnostics/support-bundle.d.ts +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +1278 -641
- package/dist/index.js.map +1 -1
- package/package.json +5 -7
- package/dist/daemon/ws-transport.d.ts +0 -139
|
@@ -9,6 +9,15 @@ import type { DeviceCredentialStore, InMemoryDeviceCredentialStore } from './dev
|
|
|
9
9
|
export declare class DeviceRevokedError extends Error {
|
|
10
10
|
constructor(message?: string);
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Thrown when the local AuthManager deadline or shutdown cancels its own
|
|
14
|
+
* in-flight request. This is deliberately distinct from `DeviceRevokedError`:
|
|
15
|
+
* only an actual challenge/token HTTP 401 is server authority for revocation.
|
|
16
|
+
*/
|
|
17
|
+
export declare class AuthRequestAbortedError extends Error {
|
|
18
|
+
readonly reason: 'deadline' | 'stopped';
|
|
19
|
+
constructor(reason: 'deadline' | 'stopped');
|
|
20
|
+
}
|
|
12
21
|
export interface AuthManagerOptions {
|
|
13
22
|
serverUrl: string;
|
|
14
23
|
store: DeviceStore;
|
|
@@ -23,6 +32,8 @@ export interface AuthManagerOptions {
|
|
|
23
32
|
* permission to supersede this machine's prior active device rows.
|
|
24
33
|
*/
|
|
25
34
|
machineId?: () => Promise<string | undefined>;
|
|
35
|
+
/** Upper bound for one pair/challenge/token fetch plus its response-body read. */
|
|
36
|
+
authRequestDeadlineMs?: number;
|
|
26
37
|
/** Called once revocation is detected, so a caller (ConnectionManager) can stop retrying and surface the state instead of looping. */
|
|
27
38
|
onRevoked?: () => void;
|
|
28
39
|
}
|
|
@@ -42,7 +53,10 @@ export declare class AuthManager {
|
|
|
42
53
|
private stopped;
|
|
43
54
|
private pairing;
|
|
44
55
|
private credentialMutationTail;
|
|
56
|
+
/** The sole cancellation authority for the request currently inside the serialized credential mutation. */
|
|
57
|
+
private activeRequest;
|
|
45
58
|
private readonly credentials;
|
|
59
|
+
private readonly requestDeadlineMs;
|
|
46
60
|
constructor(opts: AuthManagerOptions);
|
|
47
61
|
get deviceId(): string | undefined;
|
|
48
62
|
isRevoked(): boolean;
|
|
@@ -62,6 +76,12 @@ export declare class AuthManager {
|
|
|
62
76
|
/** Always throws — `never` return type lets call sites use `if (x === 401) this.markRevoked();` without an explicit `return`/`throw` of their own. */
|
|
63
77
|
private markRevoked;
|
|
64
78
|
private scheduleProactiveRenewal;
|
|
79
|
+
/**
|
|
80
|
+
* Bounds one complete auth exchange rather than fetch alone. Keeping the
|
|
81
|
+
* controller active through `json()`/`text()` makes a non-cooperative or
|
|
82
|
+
* partial response body cancellable by the same authority that owns fetch.
|
|
83
|
+
*/
|
|
84
|
+
private runRequest;
|
|
65
85
|
private runCredentialMutation;
|
|
66
86
|
/** Read the current paired authority afresh; metadata without its OS secret is re-pair required. */
|
|
67
87
|
private loadRecord;
|
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
import type { BlobRef } from '@byok-sdk/protocol';
|
|
2
2
|
import type { AuthManager } from './auth-manager';
|
|
3
|
+
export type BlobRequestAbortReason = 'deadline' | 'cancelled';
|
|
4
|
+
/** A blob request/body read did not complete before its deadline or its owner cancelled it. */
|
|
5
|
+
export declare class BlobRequestAbortedError extends Error {
|
|
6
|
+
readonly reason: BlobRequestAbortReason;
|
|
7
|
+
constructor(reason: BlobRequestAbortReason);
|
|
8
|
+
}
|
|
9
|
+
export interface BlobClientOptions {
|
|
10
|
+
/** Bound for each individual HTTP request and response-body read. Default: 15 seconds. */
|
|
11
|
+
requestDeadlineMs?: number;
|
|
12
|
+
/** Daemon lifecycle authority; aborting it stops all in-flight blob I/O. */
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
}
|
|
15
|
+
export interface BlobRequestOptions {
|
|
16
|
+
/** Task lifecycle authority; aborting it stops this transfer before finalization. */
|
|
17
|
+
signal?: AbortSignal;
|
|
18
|
+
}
|
|
3
19
|
/** Seam `TaskRunner` depends on, so tests can substitute a fake without spinning up real HTTP endpoints. */
|
|
4
20
|
export interface BlobResolver {
|
|
5
|
-
resolveInstruction(blobRef: BlobRef): Promise<string>;
|
|
6
|
-
uploadArtifact(content: string | Uint8Array, contentType: string, options?: {
|
|
21
|
+
resolveInstruction(blobRef: BlobRef, options?: BlobRequestOptions): Promise<string>;
|
|
22
|
+
uploadArtifact(content: string | Uint8Array, contentType: string, options?: BlobRequestOptions & {
|
|
7
23
|
readonly idempotencyKey?: string;
|
|
8
24
|
}): Promise<BlobRef>;
|
|
9
25
|
}
|
|
@@ -16,11 +32,13 @@ export declare class BlobClient implements BlobResolver {
|
|
|
16
32
|
#private;
|
|
17
33
|
private readonly serverUrl;
|
|
18
34
|
private readonly auth;
|
|
19
|
-
|
|
35
|
+
private readonly options;
|
|
36
|
+
private readonly requestDeadlineMs;
|
|
37
|
+
constructor(serverUrl: string, auth: AuthManager, options?: BlobClientOptions);
|
|
20
38
|
/** `blobRef` -> `GET /byok/blobs/:id/url` -> fetch the presigned download URL -> text content. Always resolves fresh rather than trusting any inlined `BlobRef.url`, per docs/protocol.md §7. */
|
|
21
|
-
resolveInstruction(blobRef: BlobRef): Promise<string>;
|
|
22
|
-
/** `POST /byok/blobs`
|
|
23
|
-
uploadArtifact(content: string | Uint8Array, contentType: string, options?: {
|
|
39
|
+
resolveInstruction(blobRef: BlobRef, options?: BlobRequestOptions): Promise<string>;
|
|
40
|
+
/** `POST /byok/blobs` -> PUT the bytes to the presigned URL -> finalize into a `BlobRef`. */
|
|
41
|
+
uploadArtifact(content: string | Uint8Array, contentType: string, options?: BlobRequestOptions & {
|
|
24
42
|
readonly idempotencyKey?: string;
|
|
25
43
|
}): Promise<BlobRef>;
|
|
26
44
|
}
|
|
@@ -2,17 +2,19 @@ import { type CapabilityFlag, type Envelope, type RuntimeInfo, type ToolsetId }
|
|
|
2
2
|
import { AuthManager } from './auth-manager';
|
|
3
3
|
import type { CursorStore } from './cursor-store';
|
|
4
4
|
import { type FleetJitter } from './deterministic-jitter';
|
|
5
|
-
import {
|
|
6
|
-
export
|
|
5
|
+
import { ReplayCursorTooOldError } from './replay-cursor';
|
|
6
|
+
export { ReplayCursorTooOldError } from './replay-cursor';
|
|
7
|
+
/** The lifecycle of the daemon's one long-poll connection. */
|
|
8
|
+
export type ConnectionState = 'connecting' | 'open' | 'closed' | 'revoked';
|
|
7
9
|
export interface ConnectionManagerOptions {
|
|
8
10
|
serverUrl: string;
|
|
9
11
|
deviceId: string;
|
|
10
12
|
productId: string;
|
|
11
13
|
capabilities: CapabilityFlag[];
|
|
12
|
-
/** U4a Local Agent release version
|
|
14
|
+
/** U4a Local Agent release version, sent unchanged in `conn.hello`. */
|
|
13
15
|
clientVersion?: string;
|
|
14
16
|
runtimes: RuntimeInfo[];
|
|
15
|
-
/** Reads current sorted logical IDs from the validated local registry for every
|
|
17
|
+
/** Reads current sorted logical IDs from the validated local registry for every `conn.hello`. */
|
|
16
18
|
getConfiguredToolsets?: () => readonly ToolsetId[];
|
|
17
19
|
auth: AuthManager;
|
|
18
20
|
cursorStore: CursorStore;
|
|
@@ -23,46 +25,35 @@ export interface ConnectionManagerOptions {
|
|
|
23
25
|
*/
|
|
24
26
|
onEnvelope: (envelope: Envelope) => void | Promise<void>;
|
|
25
27
|
onStateChange?: (state: ConnectionState) => void;
|
|
26
|
-
backoff?: BackoffOptions;
|
|
27
|
-
liveness?: LivenessOptions;
|
|
28
|
-
/** Consecutive never-acked WS connect failures before falling back to long-poll (protocol §8). Default 3. */
|
|
29
|
-
wsFailureThreshold?: number;
|
|
30
|
-
/** While long-polling, how often to retry establishing WS (protocol §8, "e.g. every 5 min"). Default 5 minutes. */
|
|
31
|
-
wsRetryIntervalMs?: number;
|
|
32
28
|
/** Backoff between failed long-poll HTTP attempts. Default 2s. */
|
|
33
29
|
longPollRetryDelayMs?: number;
|
|
34
30
|
/** Minimum delay before the next long-poll request after an empty (no-events) response. Default 250ms. */
|
|
35
31
|
longPollIdleDelayMs?: number;
|
|
36
32
|
fleetJitter?: FleetJitter;
|
|
37
33
|
onOperationalOutcome?: (outcome: 'success' | 'failure', source: 'reconnect' | 'upload') => void;
|
|
34
|
+
onTerminalError?: (error: ReplayCursorTooOldError) => void;
|
|
35
|
+
}
|
|
36
|
+
export interface RejectedOutboundEnvelope {
|
|
37
|
+
readonly envelope: Envelope;
|
|
38
|
+
readonly reason: 'inbound_rejected';
|
|
38
39
|
}
|
|
39
40
|
/**
|
|
40
|
-
* Owns the daemon's one
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* envelope through the same cursor-dedupe/persistence logic (protocol §9),
|
|
45
|
-
* so redelivery is safe regardless of which transport happens to deliver a
|
|
46
|
-
* given envelope — including during the brief overlap window when handing
|
|
47
|
-
* off between them.
|
|
41
|
+
* Owns the daemon's one authenticated long-poll connection to the server.
|
|
42
|
+
* Every received envelope passes through the same cursor-dedupe/persistence
|
|
43
|
+
* logic (protocol §9), so redelivery remains safe after an HTTP retry or a
|
|
44
|
+
* daemon restart.
|
|
48
45
|
*
|
|
49
46
|
* `send()` (Design B, finding N4) pushes onto a single shared outbox this
|
|
50
|
-
* class owns and drains through
|
|
51
|
-
*
|
|
52
|
-
* (finding F6, long-poll is a full transport, not receive-only; see
|
|
53
|
-
* docs/protocol.md §8) — so a transport switch mid-flight never strands a
|
|
54
|
-
* queued envelope. See `drainOutbox`.
|
|
47
|
+
* class owns and drains through `POST /byok/messages`; long-poll is a full
|
|
48
|
+
* bidirectional transport, not a receive-only path. See `drainOutbox`.
|
|
55
49
|
*/
|
|
56
50
|
export declare class ConnectionManager {
|
|
57
51
|
private readonly opts;
|
|
58
52
|
private readonly fleetJitter;
|
|
59
|
-
private readonly ws;
|
|
60
53
|
private readonly longPoll;
|
|
61
|
-
private mode;
|
|
62
|
-
private consecutiveFailures;
|
|
63
|
-
private wsRetryTimer;
|
|
64
|
-
private wsProbeSequence;
|
|
65
54
|
private uploadRetryAttempt;
|
|
55
|
+
private started;
|
|
56
|
+
private connected;
|
|
66
57
|
private cursor;
|
|
67
58
|
/**
|
|
68
59
|
* Finding F3 (at-most-once redelivery): the lowest `task.*` envelope `seq`
|
|
@@ -84,10 +75,9 @@ export declare class ConnectionManager {
|
|
|
84
75
|
* (see `advanceCursor`) — that semantics is unchanged. `deliveredSeq`
|
|
85
76
|
* advances eagerly, the instant a `task.*` envelope is admitted past
|
|
86
77
|
* dedup (see `deliver`/`noteDelivered`), independent of whether its
|
|
87
|
-
* handler has even started, let alone succeeded. It exists so a
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* `handleOffer` is NOT idempotent and must never be re-pulled while a
|
|
78
|
+
* handler has even started, let alone succeeded. It exists so a repeated
|
|
79
|
+
* read at the durable cursor does not re-dispatch an envelope already in
|
|
80
|
+
* flight — `handleOffer` must not start a second adapter session while a
|
|
91
81
|
* first attempt is still running. On WS this same field is written the
|
|
92
82
|
* same way, but since a live WS connection only ever pushes a given `seq`
|
|
93
83
|
* once, it never has an observable effect there beyond mirroring
|
|
@@ -98,18 +88,18 @@ export declare class ConnectionManager {
|
|
|
98
88
|
/** Finding F3: serializes `onEnvelope` calls into a per-connection FIFO — one envelope's handler always fully settles before the next one starts. */
|
|
99
89
|
private processingChain;
|
|
100
90
|
/**
|
|
101
|
-
* Design B (finding N4): the ONE outbound queue
|
|
102
|
-
*
|
|
91
|
+
* Design B (finding N4): the ONE outbound queue holds `Envelope` OBJECTS,
|
|
92
|
+
* never re-encoded/rebuilt strings, so a
|
|
103
93
|
* resend after a failed send attempt is byte-identical to the original
|
|
104
94
|
* (same `id`), which is what lets the server's per-(deviceId,id) dedup
|
|
105
95
|
* (Wave 1) recognize it as a safe no-op retry rather than a second
|
|
106
|
-
* application (protocol §9).
|
|
107
|
-
* touches this queue — see `drainOutbox` — so nothing queued while one
|
|
108
|
-
* transport was active is ever stranded when the other takes over.
|
|
96
|
+
* application (protocol §9).
|
|
109
97
|
*/
|
|
110
98
|
private readonly outbox;
|
|
99
|
+
/** Terminally rejected outbound envelopes, retained as a bounded observable quarantine. */
|
|
100
|
+
private readonly rejectedOutboundEnvelopes;
|
|
111
101
|
/**
|
|
112
|
-
* Finding F5(b): how many envelopes `drainOutbox`
|
|
102
|
+
* Finding F5(b): how many envelopes `drainOutbox` has
|
|
113
103
|
* currently spliced OUT of `this.outbox` for an in-flight (not yet
|
|
114
104
|
* confirmed delivered) `postBatch` call — 0 the rest of the time. See
|
|
115
105
|
* `outboxLength`'s own doc comment for why this needs to be tracked
|
|
@@ -119,6 +109,7 @@ export declare class ConnectionManager {
|
|
|
119
109
|
private draining;
|
|
120
110
|
private stopped;
|
|
121
111
|
private revoked;
|
|
112
|
+
private terminalError;
|
|
122
113
|
private settledWaiters;
|
|
123
114
|
private pendingCursorSave;
|
|
124
115
|
/**
|
|
@@ -161,33 +152,12 @@ export declare class ConnectionManager {
|
|
|
161
152
|
*/
|
|
162
153
|
private cancelPendingDrainRetry;
|
|
163
154
|
/**
|
|
164
|
-
* The capabilities the
|
|
165
|
-
* `string[]` for forward compatibility.
|
|
166
|
-
* long-poll
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
* not per-daemon-lifetime. Cleared to `[]` the instant the acked WS
|
|
171
|
-
* connection ends for ANY reason — an ordinary disconnect (`onWsOutcome`'s
|
|
172
|
-
* `acked` branch), `stop()`, or a transport switch to long-poll
|
|
173
|
-
* (`enterLongPoll`) — and only repopulated by a fresh advertisement from
|
|
174
|
-
* the transport that is still current.
|
|
175
|
-
* The previous version of this doc comment claimed long-poll mode simply
|
|
176
|
-
* "stays at whatever the last real WS `conn.ack` said" — that was the bug:
|
|
177
|
-
* a daemon that once learned e.g. `approval_resolved` from an earlier WS
|
|
178
|
-
* session kept believing it applied to whatever it's connected to NOW,
|
|
179
|
-
* even after a disconnect/degrade where nothing has actually confirmed
|
|
180
|
-
* that's still true (a reconnect could land on a DIFFERENT server behind a
|
|
181
|
-
* load balancer). Concretely, `TaskRunner.sendApprovalResolved` gates
|
|
182
|
-
* `task.approval_resolved` on this list — sending it to a server that
|
|
183
|
-
* doesn't actually understand it over the long-poll path would get a
|
|
184
|
-
* batch-level 400 from `MessagesSendRequestSchema` (protocol §8.2), which
|
|
185
|
-
* `drainOutbox`'s retry-the-same-batch-forever loop then head-of-line
|
|
186
|
-
* blocks EVERY envelope queued behind it on, permanently. Clearing this
|
|
187
|
-
* eagerly means that gate reliably fails closed (falls back to the
|
|
188
|
-
* pre-existing implicit-resume inference, unconditionally — see
|
|
189
|
-
* `sendApprovalResolved`'s own doc comment) the moment the connection that
|
|
190
|
-
* advertised the capability is no longer the one actually in use.
|
|
155
|
+
* The capabilities the current server response advertised — untyped
|
|
156
|
+
* `string[]` for forward compatibility. An advertisement is scoped to the
|
|
157
|
+
* current long-poll response stream and is cleared after an HTTP failure,
|
|
158
|
+
* terminal shutdown, or revocation. This keeps capability-gated outbound
|
|
159
|
+
* messages fail-closed until the current server has explicitly advertised
|
|
160
|
+
* support.
|
|
191
161
|
*/
|
|
192
162
|
private serverCapabilities;
|
|
193
163
|
constructor(opts: ConnectionManagerOptions);
|
|
@@ -198,17 +168,10 @@ export declare class ConnectionManager {
|
|
|
198
168
|
* `drainOutbox`.
|
|
199
169
|
*/
|
|
200
170
|
send(envelope: Envelope): void;
|
|
171
|
+
/** Publish a fresh local configuration snapshot while this daemon is running. */
|
|
172
|
+
refreshHello(): void;
|
|
201
173
|
/**
|
|
202
|
-
*
|
|
203
|
-
* transport is currently active, re-checking `this.mode` fresh on every
|
|
204
|
-
* iteration so a transport switch mid-drain is picked up immediately
|
|
205
|
-
* rather than fighting a stale decision made before the switch.
|
|
206
|
-
*
|
|
207
|
-
* WS: a synchronous, one-at-a-time `sendNow` per envelope while open+
|
|
208
|
-
* acked; stops (without dropping anything — the remainder stays queued)
|
|
209
|
-
* the moment it isn't, and is re-invoked once `onAcked` fires.
|
|
210
|
-
*
|
|
211
|
-
* Long-poll: POSTs the outbox in chunks of at most
|
|
174
|
+
* POSTs the outbox through long-poll in chunks of at most
|
|
212
175
|
* `MAX_MESSAGES_PER_BATCH` (finding P1) — the server hard-caps a single
|
|
213
176
|
* `/byok/messages` batch there (`MessagesSendRequestSchema`, protocol
|
|
214
177
|
* §8.2) and 400s the WHOLE request if it's exceeded, which — before this
|
|
@@ -220,14 +183,9 @@ export declare class ConnectionManager {
|
|
|
220
183
|
* failure that SAME chunk is unshifted back (order-preserving, same
|
|
221
184
|
* Envelope objects/ids — never rebuilt, so a retry is exactly the resend
|
|
222
185
|
* Wave 1's server-side dedup expects) and retried after a short backoff,
|
|
223
|
-
* re-reading `this.mode` each time so a WS recovery that happens
|
|
224
|
-
* mid-retry is honored on the very next loop iteration instead of only
|
|
225
|
-
* after this attempt's backoff chain gives up.
|
|
226
|
-
*
|
|
227
186
|
* Re-entrancy is guarded by `draining`: a call arriving while a drain is
|
|
228
187
|
* already in progress just returns — the in-progress loop's own
|
|
229
|
-
* `while (this.outbox.length > 0)` check
|
|
230
|
-
* pushed (or left over after a mode switch) on its very next iteration.
|
|
188
|
+
* `while (this.outbox.length > 0)` check picks up anything newly pushed.
|
|
231
189
|
*/
|
|
232
190
|
private drainOutbox;
|
|
233
191
|
/**
|
|
@@ -237,35 +195,29 @@ export declare class ConnectionManager {
|
|
|
237
195
|
* in-flight wait immediately instead of leaving `drainOutbox` parked here
|
|
238
196
|
* for up to the rest of the delay before it next checks `this.revoked` —
|
|
239
197
|
* and (b) unref'd, so the timer never keeps the Node process alive by
|
|
240
|
-
* itself while nothing else (
|
|
241
|
-
* legitimately is.
|
|
198
|
+
* itself while nothing else (such as the live long-poll GET) legitimately is.
|
|
242
199
|
*/
|
|
243
200
|
private drainRetryDelay;
|
|
244
|
-
isTransportDegraded(): boolean;
|
|
245
201
|
/**
|
|
246
|
-
* The capabilities the
|
|
247
|
-
*
|
|
248
|
-
* on long-poll. Empty before either transport has supplied its current
|
|
249
|
-
* advertisement, and cleared across disconnect/switch boundaries.
|
|
202
|
+
* The capabilities the latest successful `GET /byok/events` response
|
|
203
|
+
* advertised. Empty before a successful response and after a failed one.
|
|
250
204
|
*/
|
|
251
205
|
getServerCapabilities(): readonly string[];
|
|
206
|
+
getTerminalError(): ReplayCursorTooOldError | undefined;
|
|
252
207
|
isConnected(): boolean;
|
|
253
208
|
isRevoked(): boolean;
|
|
254
209
|
/**
|
|
255
|
-
* Resolves
|
|
256
|
-
* connection
|
|
257
|
-
* lets `daemon.start()` return promptly even when WS is unavailable from
|
|
258
|
-
* the very first attempt, rather than hanging until a WS `conn.ack` that
|
|
259
|
-
* may never come.
|
|
210
|
+
* Resolves after the first successful long-poll response establishes the
|
|
211
|
+
* authenticated connection.
|
|
260
212
|
*
|
|
261
213
|
* Rejects with {@link DeviceRevokedError} — instead of hanging until
|
|
262
214
|
* `timeoutMs` — if the device turns out to be revoked while settling (or
|
|
263
215
|
* already was): a cold `daemon.start()` against an already-revoked device
|
|
264
216
|
* must fail fast, not surface a generic timeout (protocol §6.3).
|
|
265
217
|
*/
|
|
266
|
-
|
|
218
|
+
waitForConnection(timeoutMs?: number): Promise<void>;
|
|
267
219
|
/**
|
|
268
|
-
* Stops
|
|
220
|
+
* Stops the long-poll transport and waits for every in-flight envelope handler
|
|
269
221
|
* (the F3 FIFO chain) and the most recent cursor write to actually land on
|
|
270
222
|
* disk — otherwise a `stop()` racing a just-processed envelope's
|
|
271
223
|
* persistence could lose that cursor advance, or leave a handler running
|
|
@@ -274,14 +226,14 @@ export declare class ConnectionManager {
|
|
|
274
226
|
* Finding F5(b) (cross-model adversarial review): `drainTimeoutMs`, when
|
|
275
227
|
* passed, bounds how long this waits for the shared outbox (`this.outbox`
|
|
276
228
|
* — Design B) to actually finish draining BEFORE flipping `this.stopped`
|
|
277
|
-
* and
|
|
229
|
+
* and stopping the transport. Before this fix, `stop()` set `stopped`
|
|
278
230
|
* synchronously and never waited for `drainOutbox` at all: an envelope
|
|
279
231
|
* `send()` had just pushed moments earlier (e.g. `TaskRunner.shutdownTask`'s
|
|
280
232
|
* own `task.fail`, sent right before `create-daemon.ts`'s
|
|
281
233
|
* `performControlShutdown` calls this) could still be sitting UNSENT in
|
|
282
234
|
* `this.outbox` — mid long-poll retry backoff, or simply not yet picked up
|
|
283
235
|
* by the fire-and-forget `drainOutbox()` `send()` kicked off — and this
|
|
284
|
-
* method would happily proceed to `stopped = true`
|
|
236
|
+
* method would happily proceed to `stopped = true` regardless,
|
|
285
237
|
* after which NOTHING ever drains it again: silently lost, even though
|
|
286
238
|
* `TaskRunner` believed it had been sent. `drainTimeoutMs` omitted (the
|
|
287
239
|
* default) preserves the EXACT prior behavior for every other existing
|
|
@@ -312,6 +264,8 @@ export declare class ConnectionManager {
|
|
|
312
264
|
* for the one case (a hung POST) this finding exists to catch honestly.
|
|
313
265
|
*/
|
|
314
266
|
outboxLength(): number;
|
|
267
|
+
/** A bounded terminal quarantine for operator inspection; these entries are never retried. */
|
|
268
|
+
rejectedOutbox(): readonly RejectedOutboundEnvelope[];
|
|
315
269
|
/**
|
|
316
270
|
* Finding F5(b): polls {@link outboxLength} (not `this.outbox.length`
|
|
317
271
|
* alone — see that method's own doc comment for why a spliced-out,
|
|
@@ -319,15 +273,14 @@ export declare class ConnectionManager {
|
|
|
319
273
|
* a single `drainOutbox()` promise directly — a drain in progress can
|
|
320
274
|
* itself loop through multiple retry/backoff cycles (`drainRetryDelay`)
|
|
321
275
|
* while the server is unreachable, and a fresh, INDEPENDENT
|
|
322
|
-
* `drainOutbox()` call can also be triggered concurrently (`send()
|
|
323
|
-
*
|
|
276
|
+
* `drainOutbox()` call can also be triggered concurrently (`send()`) —
|
|
277
|
+
* polling the one thing
|
|
324
278
|
* that actually matters (is anything still undelivered) can never go
|
|
325
279
|
* stale the way capturing one specific in-flight promise reference
|
|
326
280
|
* could. Kicks off one more `drainOutbox()` attempt itself first
|
|
327
281
|
* (harmless no-op if one is already running — see its own re-entrancy
|
|
328
|
-
* guard) in case nothing is currently actively retrying
|
|
329
|
-
*
|
|
330
|
-
* isn't just passively hoping something else happens to be making
|
|
282
|
+
* guard) in case nothing is currently actively retrying, so this bounded
|
|
283
|
+
* wait isn't just passively hoping something else happens to be making
|
|
331
284
|
* progress.
|
|
332
285
|
*/
|
|
333
286
|
private waitForOutboxDrained;
|
|
@@ -354,9 +307,10 @@ export declare class ConnectionManager {
|
|
|
354
307
|
*/
|
|
355
308
|
private deliver;
|
|
356
309
|
/**
|
|
357
|
-
*
|
|
358
|
-
* against
|
|
359
|
-
*
|
|
310
|
+
* The local watermark `deliver()` dedupes inbound `task.*` envelopes
|
|
311
|
+
* against. It is deliberately NOT the long-poll query cursor: that query
|
|
312
|
+
* is the kernel acknowledgement and uses only the successfully processed
|
|
313
|
+
* `cursor` (see the constructor). Normally this local watermark is
|
|
360
314
|
* `deliveredSeq` — which is always >= `cursor` (every envelope that
|
|
361
315
|
* reaches `advanceCursor` already passed through `noteDelivered` first,
|
|
362
316
|
* see `deliver`) — so this is the literal `max(cursor, deliveredSeq)` the
|
|
@@ -372,11 +326,9 @@ export declare class ConnectionManager {
|
|
|
372
326
|
* whose outcome wasn't known yet. No separate "reset deliveredSeq on
|
|
373
327
|
* reconnect" step is needed for this to be correct — collapsing to
|
|
374
328
|
* `cursor` exactly while stalled already produces the right answer on
|
|
375
|
-
* every
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
-
* re-pulling/re-dispatching something already in flight across a
|
|
379
|
-
* reconnect that happens to land while a handler is still running.
|
|
329
|
+
* every long-poll retry path. NOT resetting it unconditionally on every
|
|
330
|
+
* retry lets `deliveredSeq` keep doing its job of not re-dispatching
|
|
331
|
+
* something already in flight while a handler is still running.
|
|
380
332
|
*/
|
|
381
333
|
private dedupWatermark;
|
|
382
334
|
/** Design A: eagerly advance the in-memory delivery watermark — called for every `task.*` envelope `deliver()` admits past dedup, regardless of transport or of whether its handler has even started yet. */
|
|
@@ -385,9 +337,8 @@ export declare class ConnectionManager {
|
|
|
385
337
|
/**
|
|
386
338
|
* M4 Phase 4 (version-negotiation drill fix): `LongPollClient` calls this
|
|
387
339
|
* for a batch entry it could not parse into a known `Envelope` at all (an
|
|
388
|
-
* unrecognized message type
|
|
389
|
-
*
|
|
390
|
-
* `parseLooseEventsPollResponse`) but which still carried a numeric,
|
|
340
|
+
* unrecognized message type (see `long-poll-transport.ts`'s own doc
|
|
341
|
+
* comment on `parseLooseEventsPollResponse`) but which still carried a numeric,
|
|
391
342
|
* task-class envelope-level `seq` (the caller only invokes this for a
|
|
392
343
|
* `task.`-prefixed type — see `long-poll-transport.ts`'s own
|
|
393
344
|
* `extractSkippableSeq`; `conn.*`-shaped or type-less entries never reach
|
|
@@ -427,7 +378,7 @@ export declare class ConnectionManager {
|
|
|
427
378
|
* `noteDelivered` (the eager, in-memory watermark) stays UNCHAINED —
|
|
428
379
|
* called immediately, unconditionally, regardless of `stalledAtSeq` —
|
|
429
380
|
* matching `deliver()`'s own eager, unconditional call for a real
|
|
430
|
-
* envelope: its only job is "don't re-
|
|
381
|
+
* envelope: its only job is "don't re-dispatch something already handed off,"
|
|
431
382
|
* independent of outcome, and that property does not depend on FIFO
|
|
432
383
|
* ordering the way the DURABLE cursor does.
|
|
433
384
|
*
|
|
@@ -485,18 +436,10 @@ export declare class ConnectionManager {
|
|
|
485
436
|
*/
|
|
486
437
|
private noteValidationFailure;
|
|
487
438
|
private advanceCursor;
|
|
488
|
-
|
|
489
|
-
* Fires the moment a connection attempt reaches `conn.ack` — independent
|
|
490
|
-
* of whether/when it later closes. This is the ONLY place that can
|
|
491
|
-
* reliably detect "WS is back up" while long-polling: a healthy
|
|
492
|
-
* connection stays open indefinitely, so it never reaches `onWsOutcome`
|
|
493
|
-
* (which is close-only) at all.
|
|
494
|
-
*/
|
|
495
|
-
private onAcked;
|
|
496
|
-
private onWsOutcome;
|
|
439
|
+
private quarantineRejectedOutbound;
|
|
497
440
|
private notifySettled;
|
|
498
|
-
private
|
|
499
|
-
private
|
|
500
|
-
private
|
|
441
|
+
private noteConnected;
|
|
442
|
+
private noteDisconnected;
|
|
443
|
+
private enterReplayCursorTooOld;
|
|
501
444
|
private enterRevoked;
|
|
502
445
|
}
|
|
@@ -5,6 +5,7 @@ import type { StoragePressureState } from './journal/storage-policy';
|
|
|
5
5
|
import type { OperationalHealthSnapshot } from './operational-health';
|
|
6
6
|
import type { LocalAgentReleaseIdentity } from '../release-identity';
|
|
7
7
|
import type { McpToolsetConfig, McpToolsetRegistryStatus } from '../types';
|
|
8
|
+
import type { AgentHomeExecutionStatus } from '../agent-home';
|
|
8
9
|
/**
|
|
9
10
|
* M4 Phase 2: shared local-IPC contract between the daemon's control server
|
|
10
11
|
* (`control-server.ts`) and the CLI's control client (`bin/control-client.ts`)
|
|
@@ -252,7 +253,7 @@ export interface ControlStatusResult {
|
|
|
252
253
|
uptimeMs: number;
|
|
253
254
|
paired: boolean;
|
|
254
255
|
deviceId?: string;
|
|
255
|
-
/** The connection state machine's
|
|
256
|
+
/** The connection state machine's current value: `'open'`, `'revoked'`, `'closed'`, or `'connecting'`. */
|
|
256
257
|
transport: string;
|
|
257
258
|
activeTasks: ControlActiveTask[];
|
|
258
259
|
runtimeIds: string[];
|
|
@@ -276,6 +277,12 @@ export interface ControlStatusResult {
|
|
|
276
277
|
operationalHealth: OperationalHealthSnapshot;
|
|
277
278
|
/** Redacted content-addressed status from the daemon's single local registry. */
|
|
278
279
|
toolsets: McpToolsetRegistryStatus;
|
|
280
|
+
/**
|
|
281
|
+
* WP0: per-canonical-Agent-home execution serialization, counts only —
|
|
282
|
+
* see {@link AgentHomeExecutionStatus}. Absent only for an older control
|
|
283
|
+
* peer that predates the cap.
|
|
284
|
+
*/
|
|
285
|
+
agentHomeExecution?: AgentHomeExecutionStatus;
|
|
279
286
|
}
|
|
280
287
|
export interface ToolsetsReloadParams {
|
|
281
288
|
expectedRevision: string;
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import type { AgentEgressPolicy, RuntimeId } from '@byok-sdk/protocol';
|
|
2
2
|
import type { PermissionPolicy } from '@byok-sdk/protocol';
|
|
3
3
|
import type { RuntimeAdapter, GitWorkspaceConfig, McpToolsetConfig, McpToolsetObservation, McpToolsetRegistryStatus, McpToolsetReloadReceipt } from '../types';
|
|
4
|
-
import { type AgentHomeProjection } from '../agent-home';
|
|
4
|
+
import { type AgentHomeExecutionStatus, type AgentHomeProjection } from '../agent-home';
|
|
5
5
|
import type { AgentRef } from '../agent-home';
|
|
6
6
|
import { type LocalAgentReleaseIdentity } from '../release-identity';
|
|
7
|
-
import type { BackoffOptions, LivenessOptions } from './ws-transport';
|
|
8
7
|
import { type OperationalHealthSnapshot } from './operational-health';
|
|
9
8
|
import { type DaemonEventListener, type DaemonTaskInfo, type Unsubscribe } from './observer';
|
|
10
9
|
import { GitWorkspaceManager } from './git-workspace';
|
|
@@ -91,6 +90,8 @@ export interface DaemonConfig {
|
|
|
91
90
|
productName: string;
|
|
92
91
|
productId: string;
|
|
93
92
|
serverUrl: string;
|
|
93
|
+
/** Bounds one AuthManager pair/challenge/token exchange, including response-body reads. */
|
|
94
|
+
authRequestDeadlineMs?: number;
|
|
94
95
|
deviceName?: string;
|
|
95
96
|
/**
|
|
96
97
|
* Optional override for the client-hashed physical machine identity sent
|
|
@@ -130,6 +131,26 @@ export interface DaemonConfig {
|
|
|
130
131
|
* after the SDK-owned Agent home has passed construction-time preflight.
|
|
131
132
|
*/
|
|
132
133
|
strictAgentOnly?: boolean;
|
|
134
|
+
/**
|
|
135
|
+
* WP0: how many Attempts this daemon lets execute CONCURRENTLY in one
|
|
136
|
+
* canonical Agent home, across every lane and every session. Default
|
|
137
|
+
* {@link DEFAULT_MAX_CONCURRENT_MUTABLE_SESSIONS_PER_AGENT_HOME} (1).
|
|
138
|
+
*
|
|
139
|
+
* The canonical home is every Agent session's cwd, so each concurrent
|
|
140
|
+
* Attempt in it is another writer of the same `MEMORY.md`, `notes/` and
|
|
141
|
+
* `.git`. At the default, a second offer for a home that already has an
|
|
142
|
+
* active Attempt is declined retryably before adapter preparation, the
|
|
143
|
+
* claim, or any process side effect — the busy-home contract downstream
|
|
144
|
+
* hosts already depend on.
|
|
145
|
+
*
|
|
146
|
+
* Raising it above 1 is an explicit host choice that re-enables the
|
|
147
|
+
* 0.12.0 concurrent-session behaviour, including its co-writing exposure;
|
|
148
|
+
* the SDK never falls back to it on its own. Validated up front, the same
|
|
149
|
+
* way `maxTaskOutputBytes` is: a positive safe integer, so `0`, a negative
|
|
150
|
+
* number, `NaN` and a non-integer are construction errors rather than a
|
|
151
|
+
* silently reinterpreted "unlimited".
|
|
152
|
+
*/
|
|
153
|
+
maxConcurrentMutableSessionsPerAgentHome?: number;
|
|
133
154
|
/**
|
|
134
155
|
* Explicit Agent-local/cloud egress selection. Omission still enforces the
|
|
135
156
|
* SDK metadata/status projection, but does not advertise or admit the new
|
|
@@ -255,7 +276,7 @@ export interface DaemonConfig {
|
|
|
255
276
|
/**
|
|
256
277
|
* M5: explicit escape hatch for `url.ts`'s `assertServerUrlAllowed` — see
|
|
257
278
|
* that function's own doc comment for the full allow/deny rule. Default
|
|
258
|
-
* (unset/`false`): a `serverUrl` using plaintext `
|
|
279
|
+
* (unset/`false`): a `serverUrl` using plaintext `http:` is only
|
|
259
280
|
* accepted when its host is loopback (`localhost`/`*.localhost`,
|
|
260
281
|
* `127.0.0.0/8`, `::1`); anything else over plaintext throws a typed
|
|
261
282
|
* `InsecureServerUrlError` from `pair()`/`start()` below, BEFORE any
|
|
@@ -266,7 +287,7 @@ export interface DaemonConfig {
|
|
|
266
287
|
* server) — doing so also logs a loud `console.warn` (see
|
|
267
288
|
* `checkServerUrl`, this file) every time it actually changes the
|
|
268
289
|
* outcome. Never overrides an unsupported scheme (anything other than
|
|
269
|
-
* `http:`/`https
|
|
290
|
+
* `http:`/`https:`), which is refused unconditionally.
|
|
270
291
|
*/
|
|
271
292
|
dangerouslyAllowInsecureRemote?: boolean;
|
|
272
293
|
/**
|
|
@@ -473,8 +494,6 @@ export interface DaemonStatus {
|
|
|
473
494
|
localAgentRelease: Readonly<LocalAgentReleaseIdentity>;
|
|
474
495
|
paired: boolean;
|
|
475
496
|
connected: boolean;
|
|
476
|
-
/** True once the connection has fallen back to long-poll (protocol §8) — transport info only (finding F6): long-poll is a full transport, so work still proceeds normally while this holds; outbound envelopes POST to /byok/messages instead of going out over WS. */
|
|
477
|
-
degraded: boolean;
|
|
478
497
|
/** True once the server has revoked this device (401 on challenge/token, protocol §6.3). The only recourse is calling `pair()` again — the daemon does not keep retrying on its own. */
|
|
479
498
|
revoked: boolean;
|
|
480
499
|
deviceId?: string;
|
|
@@ -487,6 +506,8 @@ export interface DaemonStatus {
|
|
|
487
506
|
toolsets: McpToolsetRegistryStatus;
|
|
488
507
|
/** Content-free egress lane watermarks and typed last-drop facts. */
|
|
489
508
|
egress: AgentEgressStatus;
|
|
509
|
+
/** WP0: per-canonical-Agent-home execution serialization — see {@link AgentHomeExecutionStatus}. */
|
|
510
|
+
agentHomeExecution: AgentHomeExecutionStatus;
|
|
490
511
|
}
|
|
491
512
|
export interface Daemon {
|
|
492
513
|
/** Pairing result is intentionally credential-blind. */
|
|
@@ -536,10 +557,8 @@ export interface Daemon {
|
|
|
536
557
|
/** M3-2a: same as {@link approve} but rejects — see that method's doc comment. */
|
|
537
558
|
reject(taskId: string, reason?: string): Promise<void>;
|
|
538
559
|
}
|
|
539
|
-
/** Internal seam so tests can substitute stub adapters / faster
|
|
560
|
+
/** Internal seam so tests can substitute stub adapters / faster batch and long-poll timing. `createDaemonWithAdapters` (which takes this) is also the real entry point for products supplying a hand-built adapter set `createDaemon` can't construct on its own — e.g. custom adapter options, or an adapter that REPLACES a bundled runtime's implementation under the same id. Honest limit: an adapter id outside `pi`/`claude`/`codex` cannot pass wire validation today — `RuntimeIdSchema` (`@byok-sdk/protocol`) is a closed `z.enum(['pi', 'claude', 'codex'])`, and `isRuntimeId` filtering below (see `detectRuntimes`) drops any detected adapter outside that set before it ever reaches a wire-visible field. A genuinely fourth/namespaced runtime id is a future protocol change, not something this seam enables today. */
|
|
540
561
|
export interface DaemonOverrides {
|
|
541
|
-
backoff?: BackoffOptions;
|
|
542
|
-
liveness?: LivenessOptions;
|
|
543
562
|
/** M4 Phase 3: overrides `TaskRunner`'s default out-of-band approval wait (`DEFAULT_APPROVAL_TIMEOUT_MS`, 10 minutes) before an unanswered `requestApproval` force-resolves as a fail-closed rejection. */
|
|
544
563
|
approvalTimeoutMs?: number;
|
|
545
564
|
/** Finding F5: overrides for the control-socket shutdown path's own bounded waits — see `TaskRunner.shutdownTask`'s and `ConnectionManager.stop`'s own doc comments. Both default to 5s; neither affects an ordinary (non-shutdown-RPC) `daemon.stop()` call. */
|
|
@@ -550,10 +569,6 @@ export interface DaemonOverrides {
|
|
|
550
569
|
outboxDrainTimeoutMs?: number;
|
|
551
570
|
};
|
|
552
571
|
longPoll?: {
|
|
553
|
-
/** Consecutive never-acked WS connect failures before falling back to long-poll. Default 3. */
|
|
554
|
-
wsFailureThreshold?: number;
|
|
555
|
-
/** While long-polling, how often to retry establishing WS. Default 5 minutes. */
|
|
556
|
-
wsRetryIntervalMs?: number;
|
|
557
572
|
/** Backoff between failed long-poll HTTP attempts. Default 2s. */
|
|
558
573
|
retryDelayMs?: number;
|
|
559
574
|
/** Minimum delay before the next long-poll request after an empty (no-events) response — avoids busy-looping against a server that responds instantly. Default 250ms. */
|
|
@@ -16,6 +16,18 @@ export interface DeviceMetadata {
|
|
|
16
16
|
* from one pairing response with metadata from another.
|
|
17
17
|
*/
|
|
18
18
|
export type DeviceRecord = DeviceMetadata & DeviceCredentials;
|
|
19
|
+
/**
|
|
20
|
+
* The one durable authority allowed before a first pairing response is
|
|
21
|
+
* received. It keeps the generated key immutable across a lost response, so
|
|
22
|
+
* an exact server-side retry can prove the same public-key binding.
|
|
23
|
+
*/
|
|
24
|
+
export interface FirstPairingAttempt {
|
|
25
|
+
readonly kind: 'first-pairing-attempt-v1';
|
|
26
|
+
readonly deviceName: string;
|
|
27
|
+
readonly devicePublicKey: string;
|
|
28
|
+
readonly devicePrivateKeyPem: string;
|
|
29
|
+
readonly machineId?: string;
|
|
30
|
+
}
|
|
19
31
|
export interface DeviceCommandResult {
|
|
20
32
|
readonly exitCode: number;
|
|
21
33
|
readonly stdout: string;
|
|
@@ -44,6 +56,8 @@ export declare class DeviceCredentialStore {
|
|
|
44
56
|
#private;
|
|
45
57
|
constructor(options: DeviceCredentialStoreOptions);
|
|
46
58
|
read(): Promise<DeviceRecord | undefined>;
|
|
59
|
+
readFirstPairingAttempt(): Promise<FirstPairingAttempt | undefined>;
|
|
60
|
+
saveFirstPairingAttempt(attempt: FirstPairingAttempt): Promise<void>;
|
|
47
61
|
replace(record: DeviceRecord): Promise<void>;
|
|
48
62
|
/** Returns true only after the sole secret authority is confirmed absent. */
|
|
49
63
|
clear(): Promise<boolean>;
|
|
@@ -52,6 +66,8 @@ export declare class DeviceCredentialStore {
|
|
|
52
66
|
export declare class InMemoryDeviceCredentialStore {
|
|
53
67
|
#private;
|
|
54
68
|
read(): Promise<DeviceRecord | undefined>;
|
|
69
|
+
readFirstPairingAttempt(): Promise<FirstPairingAttempt | undefined>;
|
|
70
|
+
saveFirstPairingAttempt(attempt: FirstPairingAttempt): Promise<void>;
|
|
55
71
|
replace(record: DeviceRecord): Promise<void>;
|
|
56
72
|
clear(): Promise<boolean>;
|
|
57
73
|
}
|