@byok-sdk/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.
Files changed (91) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +11 -0
  3. package/dist/adapters/claude/claude-adapter.d.ts +155 -0
  4. package/dist/adapters/claude/events.d.ts +91 -0
  5. package/dist/adapters/claude/permission-mapping.d.ts +142 -0
  6. package/dist/adapters/claude/process-client.d.ts +103 -0
  7. package/dist/adapters/claude/resolve-approval-mcp-bin.d.ts +45 -0
  8. package/dist/adapters/claude/resolve-bin.d.ts +27 -0
  9. package/dist/adapters/codex/codex-adapter.d.ts +97 -0
  10. package/dist/adapters/codex/events.d.ts +41 -0
  11. package/dist/adapters/codex/permission-mapping.d.ts +107 -0
  12. package/dist/adapters/codex/process-runner.d.ts +86 -0
  13. package/dist/adapters/codex/resolve-bin.d.ts +25 -0
  14. package/dist/adapters/pi/events.d.ts +58 -0
  15. package/dist/adapters/pi/permission-mapping.d.ts +46 -0
  16. package/dist/adapters/pi/pi-adapter.d.ts +29 -0
  17. package/dist/adapters/pi/resolve-bin.d.ts +44 -0
  18. package/dist/adapters/pi/rpc-client.d.ts +101 -0
  19. package/dist/bin/approval-mcp-server.d.ts +69 -0
  20. package/dist/bin/audit-log.d.ts +212 -0
  21. package/dist/bin/byok-agent.d.ts +2 -0
  22. package/dist/bin/byok-agent.js +12347 -0
  23. package/dist/bin/byok-agent.js.map +1 -0
  24. package/dist/bin/byok-approval-mcp.d.ts +2 -0
  25. package/dist/bin/byok-approval-mcp.js +498 -0
  26. package/dist/bin/byok-approval-mcp.js.map +1 -0
  27. package/dist/bin/commands/approvals.d.ts +31 -0
  28. package/dist/bin/commands/approve-reject.d.ts +29 -0
  29. package/dist/bin/commands/doctor.d.ts +15 -0
  30. package/dist/bin/commands/pair.d.ts +7 -0
  31. package/dist/bin/commands/runtimes.d.ts +7 -0
  32. package/dist/bin/commands/service.d.ts +54 -0
  33. package/dist/bin/commands/start.d.ts +21 -0
  34. package/dist/bin/commands/status.d.ts +25 -0
  35. package/dist/bin/commands/support-bundle.d.ts +7 -0
  36. package/dist/bin/commands/tasks.d.ts +27 -0
  37. package/dist/bin/commands/unpair.d.ts +124 -0
  38. package/dist/bin/commands/workspaces.d.ts +16 -0
  39. package/dist/bin/config.d.ts +41 -0
  40. package/dist/bin/control-client.d.ts +50 -0
  41. package/dist/bin/format.d.ts +67 -0
  42. package/dist/bin/runtime-probe.d.ts +34 -0
  43. package/dist/bin/tasks-view.d.ts +49 -0
  44. package/dist/daemon/approvals.d.ts +87 -0
  45. package/dist/daemon/auth-manager.d.ts +52 -0
  46. package/dist/daemon/blob-client.d.ts +22 -0
  47. package/dist/daemon/connection-manager.d.ts +502 -0
  48. package/dist/daemon/control-protocol.d.ts +299 -0
  49. package/dist/daemon/control-server.d.ts +61 -0
  50. package/dist/daemon/create-daemon.d.ts +364 -0
  51. package/dist/daemon/cursor-store.d.ts +30 -0
  52. package/dist/daemon/daemon-owner.d.ts +24 -0
  53. package/dist/daemon/deterministic-jitter.d.ts +20 -0
  54. package/dist/daemon/device-keys.d.ts +45 -0
  55. package/dist/daemon/device-proof-signer.d.ts +41 -0
  56. package/dist/daemon/environment.d.ts +96 -0
  57. package/dist/daemon/git-workspace-store.d.ts +49 -0
  58. package/dist/daemon/git-workspace.d.ts +80 -0
  59. package/dist/daemon/http-client.d.ts +8 -0
  60. package/dist/daemon/journal/journal.d.ts +342 -0
  61. package/dist/daemon/journal/sqlite-journal.d.ts +173 -0
  62. package/dist/daemon/journal/sqlite-support.d.ts +96 -0
  63. package/dist/daemon/journal/storage-policy.d.ts +368 -0
  64. package/dist/daemon/long-poll-transport.d.ts +142 -0
  65. package/dist/daemon/observer.d.ts +345 -0
  66. package/dist/daemon/operational-health.d.ts +87 -0
  67. package/dist/daemon/policy.d.ts +16 -0
  68. package/dist/daemon/progress-batcher.d.ts +31 -0
  69. package/dist/daemon/runtime-capabilities.d.ts +36 -0
  70. package/dist/daemon/session-workspace-store.d.ts +75 -0
  71. package/dist/daemon/store.d.ts +54 -0
  72. package/dist/daemon/task-runner.d.ts +862 -0
  73. package/dist/daemon/truth-memory-client.d.ts +94 -0
  74. package/dist/daemon/url.d.ts +60 -0
  75. package/dist/daemon/ws-transport.d.ts +110 -0
  76. package/dist/diagnostics/diagnostics.d.ts +114 -0
  77. package/dist/diagnostics/support-bundle.d.ts +81 -0
  78. package/dist/index.d.ts +42 -0
  79. package/dist/index.js +10056 -0
  80. package/dist/index.js.map +1 -0
  81. package/dist/lifecycle/create-service-lifecycle.d.ts +59 -0
  82. package/dist/lifecycle/exec-runner.d.ts +141 -0
  83. package/dist/lifecycle/launchd.d.ts +55 -0
  84. package/dist/lifecycle/service-types.d.ts +177 -0
  85. package/dist/lifecycle/systemd.d.ts +46 -0
  86. package/dist/lifecycle/winsw.d.ts +64 -0
  87. package/dist/types.d.ts +178 -0
  88. package/dist/util/async-queue.d.ts +45 -0
  89. package/dist/util/atomic-write.d.ts +86 -0
  90. package/dist/util/secure-dir.d.ts +124 -0
  91. package/package.json +66 -0
@@ -0,0 +1,364 @@
1
+ import type { RuntimeId } from '@byok-sdk/protocol';
2
+ import type { PermissionPolicy } from '@byok-sdk/protocol';
3
+ import type { RuntimeAdapter, GitWorkspaceConfig } from '../types';
4
+ import type { BackoffOptions, LivenessOptions } from './ws-transport';
5
+ import { type OperationalHealthSnapshot } from './operational-health';
6
+ import { type DaemonEventListener, type DaemonTaskInfo, type Unsubscribe } from './observer';
7
+ import { GitWorkspaceManager } from './git-workspace';
8
+ import { GitWorkspaceStore } from './git-workspace-store';
9
+ import { type DeviceRecord } from './store';
10
+ import { type LocalTaskJournal } from './journal/journal';
11
+ import { type JournalOpenFaultSeam } from './journal/sqlite-support';
12
+ import { LocalStoragePressureEngine, type LocalStoragePolicyInput } from './journal/storage-policy';
13
+ import type { ProgressBatcherOptions } from './progress-batcher';
14
+ /**
15
+ * Optional white-label product display info — purely opaque passthrough
16
+ * (never interpreted, validated, or rendered by the daemon itself). Carried
17
+ * through to `DaemonStatus.branding` (see `status()` below) so a downstream
18
+ * CLI or audit log can render/stamp product identity without the daemon
19
+ * needing to know anything about presentation. Deliberately a small,
20
+ * open-ish shape rather than an exhaustive theming schema — add fields here
21
+ * only as concrete consumers (CLI UX, audit log) need them.
22
+ */
23
+ export interface DaemonBranding {
24
+ /** Product/company name for banners, prompts, audit log entries, etc. */
25
+ displayName?: string;
26
+ /** Support/help URL surfaced alongside branding. */
27
+ supportUrl?: string;
28
+ /** Brand accent color (any CSS-color-like string — hex, name, etc.); not parsed or validated here. */
29
+ accent?: string;
30
+ }
31
+ /**
32
+ * S3b (L-002): opt-in durable local journal for hosted deployments
33
+ * (architecture §12.7.2).
34
+ *
35
+ * Explicit and off by default, because it changes what an ack MEANS. With no
36
+ * journal configured (the self-hosted default this SDK has always shipped),
37
+ * the daemon's redelivery cursor advances on handler success exactly as
38
+ * before. With one configured, every inbound envelope is committed and fsynced
39
+ * to `<storeDir>/daemon.db` BEFORE the handler resolves — and since
40
+ * `ConnectionManager` only advances the cursor once the handler resolves
41
+ * (`connection-manager.ts`), acking before committing stops being something
42
+ * this daemon can express, rather than something it is careful not to do.
43
+ *
44
+ * Turning this on when the runtime has no working `node:sqlite` is a typed
45
+ * construction failure, not a downgrade to a file journal — see
46
+ * `JournalUnavailableError`. That refusal is the point (§12.7.2): a journal
47
+ * that does not fsync loses tasks only under power-cut timings, so it passes
48
+ * every test anyone will ever run against it.
49
+ */
50
+ export interface HostedJournalConfig {
51
+ /**
52
+ * The backend. Only `sqlite` today, and spelled out rather than implied by
53
+ * the section's presence so adding a second one later is an additive change
54
+ * to a closed set instead of a re-interpretation of an existing config.
55
+ */
56
+ mode: 'sqlite';
57
+ /**
58
+ * The tenant every journal row on this device is scoped to (§12.7.2's
59
+ * minimum fact set opens with tenant/product/device). Required: a hosted
60
+ * daemon that cannot say which tenant its durable evidence belongs to has
61
+ * evidence nobody can act on.
62
+ */
63
+ tenantId: string;
64
+ /** Bound on waiting for the journal's write lock, ms. Defaults to the journal's own bound. */
65
+ busyTimeoutMs?: number;
66
+ /** Per-record byte bound. Defaults to the journal's own bound; oversized records are refused, never truncated. */
67
+ maxRecordBytes?: number;
68
+ /**
69
+ * S3b (L-003): opt-in local storage policy — §12.7.2.1's watermarks,
70
+ * classified GC, and bounded WAL compaction. See `LocalStoragePolicyInput`
71
+ * (`journal/storage-policy.ts`); `maxStoreBytes` and `minFreeBytes` are the
72
+ * only two a host must supply.
73
+ *
74
+ * Optional WITHIN hosted journal mode, and off by default there too: a
75
+ * journal without a policy is a daemon that records durably and never
76
+ * declines, which is exactly the right behaviour on a host that manages its
77
+ * own disk. Setting it is what turns local storage into an admission-control
78
+ * input — hard pressure declines new offers (retryably) while terminal
79
+ * flush, delete, export and recovery keep running, and `emergency` refuses
80
+ * to ack at all rather than acking a row it cannot durably record.
81
+ */
82
+ storagePolicy?: LocalStoragePolicyInput;
83
+ }
84
+ export interface DaemonConfig {
85
+ productName: string;
86
+ productId: string;
87
+ serverUrl: string;
88
+ deviceName?: string;
89
+ workspaceRoot: string;
90
+ /** Disabled by default. Enables local-only Git checkpoint repositories. */
91
+ gitWorkspace?: GitWorkspaceConfig;
92
+ /** Disabled by default. Enables the durable local task journal — see {@link HostedJournalConfig}. */
93
+ hostedJournal?: HostedJournalConfig;
94
+ /**
95
+ * Restricts which runtimes this daemon will ever use — enforced in two
96
+ * places that must stay consistent: `createDaemon` (this file) builds its
97
+ * bundled adapter set FROM this list (unset = all three bundled adapters
98
+ * — pi, claude, codex; set = exactly the listed runtime ids, unknown ids
99
+ * ignored — see `buildDefaultAdapters`), and `TaskRunner.pickAdapter`
100
+ * (`task-runner.ts`) separately fail-closed-rejects any `task.offer`
101
+ * naming a runtime outside this list regardless of which adapters were
102
+ * constructed. `createDaemonWithAdapters` callers supply their own
103
+ * `adapters` array directly, so for them this field is enforcement-only,
104
+ * unchanged from M1/M2.
105
+ */
106
+ runtimeAllowlist?: string[];
107
+ /**
108
+ * M5 batch-3 (workstream 1): explicit auto-select priority order for
109
+ * `TaskRunner.pickAdapter`'s no-explicit-runtime branch (`task-runner.ts`)
110
+ * — tried in listed order; the first candidate that is both PRESENT
111
+ * (`adapter.detect()`) and CAPABLE (declares the offer's
112
+ * `PermissionPolicy.mode` in its own `capabilities().permissionModes` —
113
+ * see `adapterSupportsMode`) wins. Unset defaults to
114
+ * `DEFAULT_RUNTIME_PREFERENCE` (`task-runner.ts`): `['claude', 'codex',
115
+ * 'pi']` — pi LAST, deliberately.
116
+ *
117
+ * Product decision: pi is this SDK's FALLBACK runtime, not its default.
118
+ * Before this field existed, the auto-select path had no notion of
119
+ * priority at all — it simply walked `deps.adapters` in whatever order
120
+ * `buildDefaultAdapters`/the embedder constructed them, which for
121
+ * `createDaemon`'s bundled set meant `ALL_RUNTIME_IDS`'s construction
122
+ * order (`['pi', 'claude', 'codex']`) doubled as the de-facto selection
123
+ * priority — silently making pi the default winner whenever it was
124
+ * present, for no better reason than being listed first in an array never
125
+ * meant to encode a priority. This field makes the real priority an
126
+ * explicit, independently-configurable decision instead of an accident of
127
+ * construction order — see `ALL_RUNTIME_IDS`'s own doc comment below for
128
+ * the construction-vs-selection-order split this introduces.
129
+ *
130
+ * Only affects the auto-select (no `task.offer.runtime`) path — an offer
131
+ * naming an explicit runtime is unaffected (unchanged semantics: that
132
+ * exact adapter is used, or the offer is declined; never a fallback
133
+ * substitution). Independent of `runtimeAllowlist` above (which restricts
134
+ * WHICH runtimes are eligible at all): this only orders the attempt
135
+ * sequence among whatever that allowlist, if set, already let through.
136
+ */
137
+ runtimePreference?: RuntimeId[];
138
+ /**
139
+ * The device operator's configured policy CEILING — every `task.offer`'s
140
+ * own policy is merged against this and fail-closed-rejected if it asks
141
+ * for more latitude than this allows (`daemon/policy.ts`'s
142
+ * `computeEffectivePolicy`).
143
+ *
144
+ * M5 batch-3 (workstream 1): `workspaceRoot` set on THIS ceiling is still
145
+ * merged into the effective policy handed to an adapter as
146
+ * `ctx.policy.workspaceRoot` (`computeEffectivePolicy` is unchanged) — but
147
+ * no bundled adapter (pi/claude/codex) actually reads or enforces it;
148
+ * every adapter derives its real confinement from `ctx.workspaceDir` (the
149
+ * daemon-created per-task directory) instead — see docs/security.md's
150
+ * "Workspace confinement is a convention, not a sandbox" section. Setting
151
+ * it here is therefore silently inert rather than actively dangerous by
152
+ * itself (an OFFER independently asking for its OWN `workspaceRoot` is a
153
+ * separate, fail-closed-declined case — see `TaskRunner.handleOffer` —
154
+ * precisely because THAT looks like a live security control when it
155
+ * isn't). `start()` below logs a loud, one-time `console.warn` whenever
156
+ * this ceiling sets `workspaceRoot`, so an operator who configured it
157
+ * expecting real enforcement finds out immediately instead of trusting a
158
+ * control nothing honors.
159
+ */
160
+ permissionDefaults?: PermissionPolicy;
161
+ storeDir?: string;
162
+ /** Optional white-label branding — see `DaemonBranding`. Carried through verbatim to `status().branding`. */
163
+ branding?: DaemonBranding;
164
+ /**
165
+ * M5: per-device, per-runtime escape hatch into the environment allowlist
166
+ * `task-runner.ts` builds each task's spawn environment from
167
+ * (`daemon/environment.ts`'s `buildRuntimeEnv`) — keyed by runtime id
168
+ * (`'pi' | 'claude' | 'codex'`, though not typed that narrowly here since
169
+ * an id with no matching adapter is simply never looked up). `allow`
170
+ * entries are exact variable names or `*`-suffixed prefixes, merged in
171
+ * alongside that runtime adapter's own declared
172
+ * `environmentRequirements()` — this can never override the hard
173
+ * `BYOK_*` deny (see `environment.ts`'s own doc comment).
174
+ */
175
+ runtimeEnvironment?: Record<string, {
176
+ allow?: string[];
177
+ }>;
178
+ /**
179
+ * M5: explicit escape hatch for `url.ts`'s `assertServerUrlAllowed` — see
180
+ * that function's own doc comment for the full allow/deny rule. Default
181
+ * (unset/`false`): a `serverUrl` using plaintext `ws:`/`http:` is only
182
+ * accepted when its host is loopback (`localhost`/`*.localhost`,
183
+ * `127.0.0.0/8`, `::1`); anything else over plaintext throws a typed
184
+ * `InsecureServerUrlError` from `pair()`/`start()` below, BEFORE any
185
+ * network call is attempted, rather than silently sending the pairing
186
+ * code / device credentials to a remote host in the clear. Set this
187
+ * `true` only when you have deliberately decided to accept that risk
188
+ * (e.g. a trusted private network with no TLS terminator in front of the
189
+ * server) — doing so also logs a loud `console.warn` (see
190
+ * `checkServerUrl`, this file) every time it actually changes the
191
+ * outcome. Never overrides an unsupported scheme (anything other than
192
+ * `http:`/`https:`/`ws:`/`wss:`), which is refused unconditionally.
193
+ */
194
+ dangerouslyAllowInsecureRemote?: boolean;
195
+ /**
196
+ * M5 batch-3 (workstream 2): caps accumulated (approximate,
197
+ * serialized-event-length) agent-event output bytes this daemon will
198
+ * tolerate for a single task before tearing it down as a resource-limit
199
+ * violation (`task.fail`, `retryable: false`, reason prefixed
200
+ * `resource limit exceeded: maxTaskOutputBytes` — see
201
+ * `MAX_OUTPUT_BYTES_EXCEEDED_REASON_PREFIX`, `task-runner.ts`) — see
202
+ * `TaskRunner.pump`'s own doc comment for exactly what's counted and what
203
+ * isn't. Default {@link DEFAULT_MAX_TASK_OUTPUT_BYTES} (64 MiB) when
204
+ * unset.
205
+ *
206
+ * `0` or a negative number is a config validation error, thrown
207
+ * synchronously from `createDaemonWithAdapters`/`createDaemon` — NOT a
208
+ * supported way to disable the cap. Pass `Number.POSITIVE_INFINITY`
209
+ * explicitly instead to opt out of enforcement altogether.
210
+ */
211
+ maxTaskOutputBytes?: number;
212
+ /**
213
+ * M5 batch-3 (workstream 2): deadline bound on the graceful-shutdown
214
+ * sequence's own wait for `TaskRunner.shutdownActiveTasks` to finish
215
+ * interrupting/failing every active task before this daemon proceeds to
216
+ * actually stop regardless — see `runShutdownSequence`'s own doc comment
217
+ * for the full sequence this bounds, and for why every graceful-shutdown
218
+ * path (`stop()`, `unpair()`, the control socket's `shutdown` RPC) now
219
+ * shares it. Default 10s (`SHUTDOWN_TASK_TEARDOWN_DEADLINE_MS`) — the same
220
+ * bound the control-socket shutdown path already used before this field
221
+ * existed.
222
+ */
223
+ shutdownGraceMs?: number;
224
+ }
225
+ export interface DaemonStatus {
226
+ paired: boolean;
227
+ connected: boolean;
228
+ /** 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. */
229
+ degraded: boolean;
230
+ /** 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. */
231
+ revoked: boolean;
232
+ deviceId?: string;
233
+ activeTaskCount: number;
234
+ /** Passthrough of `DaemonConfig.branding` — `undefined` when the product configured none. See `DaemonBranding`. */
235
+ branding?: DaemonBranding;
236
+ /** Local lifecycle/retry budget, separate from transport fallback state. */
237
+ operationalHealth: OperationalHealthSnapshot;
238
+ }
239
+ export interface Daemon {
240
+ pair(pairingCode: string): Promise<DeviceRecord>;
241
+ start(): Promise<void>;
242
+ stop(): Promise<void>;
243
+ status(): DaemonStatus;
244
+ /**
245
+ * M3-2a: local observability — subscribe to live `DaemonEvent`s (task
246
+ * feed, connection/pairing state changes, runtime-detection results) as
247
+ * they happen on THIS daemon, no SaaS-side polling required. Returns an
248
+ * unsubscribe function; a listener that throws is caught and logged, never
249
+ * propagated (see `observer.ts`). Purely additive: emitting these never
250
+ * changes `status()` or any existing wire/task behavior.
251
+ */
252
+ subscribe(listener: DaemonEventListener): Unsubscribe;
253
+ /** M3-2a: current locally-known tasks and their derived state/summary (for a `tasks` CLI subcommand) — reflects only what this daemon has observed since it started; see `observer.ts`'s `DaemonObserver.tasks`. */
254
+ tasks(): DaemonTaskInfo[];
255
+ /**
256
+ * M3-2a: clears this device's persisted identity/credentials and
257
+ * disconnects — the next `start()` throws until `pair()` is called again.
258
+ * Safe to call at any point in the daemon's lifecycle (never paired,
259
+ * paired-but-not-started, or running).
260
+ */
261
+ unpair(): Promise<void>;
262
+ /**
263
+ * M3-2a: locally resolve a task currently paused on `needs_approval` —
264
+ * drives the exact same code path a server-sent `task.approve` does
265
+ * (`TaskRunner.handleApprove`), invoked directly instead of over the wire.
266
+ * Honest-but-currently-unexercised: none of the three bundled adapters
267
+ * (pi/claude/codex) ever actually pauses for approval — each one's
268
+ * `resolveApproval` throws unconditionally (see `toRuntimeInfoCapabilities`'s
269
+ * doc comment below) — so calling this against a task on this daemon today
270
+ * always fails that task with a clear reason instead of resuming it,
271
+ * exactly as an honest, doing-nothing-magic implementation should. Ready
272
+ * for the day a runtime adapter implements real interactive approval, with
273
+ * no further changes needed here. A no-op (resolves immediately) for a
274
+ * `taskId` this daemon doesn't currently have active.
275
+ */
276
+ approve(taskId: string): Promise<void>;
277
+ /** M3-2a: same as {@link approve} but rejects — see that method's doc comment. */
278
+ reject(taskId: string, reason?: string): Promise<void>;
279
+ }
280
+ /** Internal seam so tests can substitute stub adapters / faster backoff+batch+liveness+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. */
281
+ export interface DaemonOverrides {
282
+ backoff?: BackoffOptions;
283
+ batch?: ProgressBatcherOptions;
284
+ liveness?: LivenessOptions;
285
+ /** 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. */
286
+ approvalTimeoutMs?: number;
287
+ /** 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. */
288
+ shutdown?: {
289
+ /** Bound on how long `shutdownTask` waits for a single task's own `session.interrupt()` before giving up on it specifically and reporting `task.fail` anyway. Default `DEFAULT_SHUTDOWN_INTERRUPT_TIMEOUT_MS`. */
290
+ taskInterruptTimeoutMs?: number;
291
+ /** Bound on how long the control-socket shutdown path waits for the outbox to actually drain before closing the connection. Default `DEFAULT_SHUTDOWN_OUTBOX_DRAIN_TIMEOUT_MS`. */
292
+ outboxDrainTimeoutMs?: number;
293
+ };
294
+ longPoll?: {
295
+ /** Consecutive never-acked WS connect failures before falling back to long-poll. Default 3. */
296
+ wsFailureThreshold?: number;
297
+ /** While long-polling, how often to retry establishing WS. Default 5 minutes. */
298
+ wsRetryIntervalMs?: number;
299
+ /** Backoff between failed long-poll HTTP attempts. Default 2s. */
300
+ retryDelayMs?: number;
301
+ /** 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. */
302
+ idleDelayMs?: number;
303
+ };
304
+ /**
305
+ * Test/product injection seam for the local Git workspace boundary. The
306
+ * supplied manager/store are used only when `config.gitWorkspace` is enabled;
307
+ * an absent Git config therefore cannot accidentally activate either object.
308
+ */
309
+ gitWorkspace?: {
310
+ manager?: GitWorkspaceManager;
311
+ store?: GitWorkspaceStore;
312
+ };
313
+ /**
314
+ * S3b: injection seam for the durable local journal, mirroring
315
+ * `gitWorkspace` above — used only when `config.hostedJournal` is set, so an
316
+ * absent journal config cannot accidentally activate a supplied object. Also
317
+ * the host-injected-implementation path §12.7.2 allows: a backend other than
318
+ * `SqliteLocalTaskJournal` is acceptable if it meets the same durability
319
+ * contract (a resolved `appendEnvelope` means fsynced), and unacceptable
320
+ * otherwise — this seam does not verify that, the host asserting it does.
321
+ *
322
+ * A journal supplied here is NOT closed by `stop()`; whoever injected it
323
+ * owns its lifetime.
324
+ */
325
+ hostedJournal?: {
326
+ journal?: LocalTaskJournal;
327
+ /** Test-only post-open SQLite initialization fault seam. */
328
+ openFaults?: JournalOpenFaultSeam;
329
+ /**
330
+ * S3b (L-003): injection seam for the storage pressure engine, same rule
331
+ * as `journal` above — used only when `config.hostedJournal` is set, and
332
+ * NOT started or stopped by this daemon. Whoever injects one owns its
333
+ * cadence, which is what lets the disk-pressure matrix drive `tick()` by
334
+ * hand with fake usage/free providers and no timer at all.
335
+ *
336
+ * An injected engine still answers both questions the daemon asks it
337
+ * (admission under hard pressure, ack-critical refusal under emergency)
338
+ * and still backs the `storage` section of the control-socket status.
339
+ */
340
+ pressureEngine?: LocalStoragePressureEngine;
341
+ };
342
+ }
343
+ export declare function createDaemonWithAdapters(config: DaemonConfig, adapters: RuntimeAdapter[], overrides?: DaemonOverrides): Daemon;
344
+ /**
345
+ * Public white-label entry point (M0-M3): the "5-line launcher" — a product
346
+ * only needs a `DaemonConfig`, no hand-built adapter list. The bundled
347
+ * adapter set is built from `config.runtimeAllowlist` (see
348
+ * `buildDefaultAdapters` and that field's own doc comment for the exact
349
+ * unset-vs-set contract); with no `runtimeAllowlist` configured, the default
350
+ * is ALL THREE bundled adapters (pi, claude, codex), not pi alone: M0/M1
351
+ * hard-wired pi-only unconditionally, which meant any product wanting
352
+ * claude/codex had to drop to `createDaemonWithAdapters` and hand-build
353
+ * adapters just to get a runtime that was already built into this SDK.
354
+ * `detectRuntimes` only ever advertises what's actually present on the
355
+ * device (protocol §10 gap #4), so constructing an adapter for a runtime the
356
+ * device doesn't have costs one quick failed `--version` probe at `start()`
357
+ * and is otherwise invisible — there's no reason to withhold it by default.
358
+ * Products that DO want to restrict to a subset set `runtimeAllowlist`
359
+ * (also independently enforced at task-pick time — see
360
+ * `TaskRunnerDeps.runtimeAllowlist` / `TaskRunner.pickAdapter`); products
361
+ * needing something this can't build (custom adapter options, a fourth
362
+ * in-house runtime, test stubs) use `createDaemonWithAdapters` directly.
363
+ */
364
+ export declare function createDaemon(config: DaemonConfig): Daemon;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Persists the highest processed server->daemon envelope `seq` per
3
+ * (server, device) pair (protocol §9, at-least-once redelivery), so a
4
+ * restarted daemon can send an accurate `conn.hello.cursor` and the server
5
+ * can skip re-delivering envelopes it already knows were handled. Keyed by a
6
+ * hash of `serverUrl` + `deviceId` together (not just `serverUrl`) so
7
+ * filenames stay filesystem-safe regardless of scheme/port/path.
8
+ *
9
+ * Finding F5 (stale cursor across re-pair): `POST /byok/pair` always mints a
10
+ * brand new `deviceId` (see `packages/server/src/http.ts`), including on a
11
+ * re-pair against the same `serverUrl` (e.g. recovering from revocation,
12
+ * protocol §6.3). A cursor keyed by `serverUrl` alone would hand the fresh
13
+ * device's very first connection a stale, unrelated cursor value left over
14
+ * from whatever device previously used this URL — the new device's own
15
+ * server-side outbox starts its `seq` counter back at 1, so that stale
16
+ * cursor would make the server's redelivery filter (`seq > cursor`) throw
17
+ * away every legitimate envelope sent to it. Keying by the pair means a new
18
+ * deviceId always starts with a genuinely fresh (absent) cursor entry;
19
+ * `clear()` additionally lets `create-daemon.ts`'s `pair()` proactively wipe
20
+ * the previous device's entry for this `serverUrl` as a hygiene measure.
21
+ */
22
+ export declare class CursorStore {
23
+ private readonly storeDir;
24
+ constructor(storeDir: string);
25
+ private fileFor;
26
+ load(serverUrl: string, deviceId: string): Promise<number | undefined>;
27
+ save(serverUrl: string, deviceId: string, cursor: number): Promise<void>;
28
+ /** Remove any persisted cursor for (serverUrl, deviceId) — a no-op if none exists. Called from `pair()` (finding F5) so a device that's about to be replaced never leaves a cursor a future, unrelated device could somehow inherit. */
29
+ clear(serverUrl: string, deviceId: string): Promise<void>;
30
+ }
@@ -0,0 +1,24 @@
1
+ export declare const DAEMON_OWNER_FILENAME = "daemon-owner.json";
2
+ interface OwnerRecord {
3
+ version: 2;
4
+ pid: number;
5
+ nonce: string;
6
+ role: 'daemon' | 'doctor';
7
+ acquiredAt: string;
8
+ processStartedAt: string;
9
+ livenessPort: number;
10
+ }
11
+ export interface DaemonOwnerLease {
12
+ release(): Promise<void>;
13
+ }
14
+ export declare class DaemonOwnerActiveError extends Error {
15
+ readonly role: OwnerRecord['role'] | 'unknown';
16
+ constructor(role: OwnerRecord['role'] | 'unknown');
17
+ }
18
+ /**
19
+ * Cross-process, fail-closed ownership for store mutations that must never
20
+ * race a daemon. A separate exclusive reclaim file serializes stale-owner recovery so two
21
+ * starters cannot accidentally rename each other's newly-acquired lease.
22
+ */
23
+ export declare function acquireDaemonOwner(storeDir: string, role: OwnerRecord['role'], clock?: () => Date): Promise<DaemonOwnerLease>;
24
+ export {};
@@ -0,0 +1,20 @@
1
+ export type JitterDomain = 'reconnect' | 'upload' | 'maintenance';
2
+ export interface DeterministicJitterInput {
3
+ seed: string;
4
+ domain: JitterDomain;
5
+ sequence: number;
6
+ baseMs: number;
7
+ ratio?: number;
8
+ }
9
+ /**
10
+ * Stable, domain-separated fleet jitter. The same identity/domain/sequence
11
+ * always produces the same integer delay, while a different domain cannot
12
+ * accidentally reuse the same hash stream. There is deliberately no random
13
+ * fallback: callers must have loaded the device identity before constructing
14
+ * an automatic retry loop.
15
+ */
16
+ export declare function deterministicJitterMs(input: DeterministicJitterInput): number;
17
+ export interface FleetJitter {
18
+ delay(domain: JitterDomain, sequence: number, baseMs: number): number;
19
+ }
20
+ export declare function createFleetJitter(productId: string, deviceId: string): FleetJitter;
@@ -0,0 +1,45 @@
1
+ import { type KeyObject } from 'node:crypto';
2
+ /**
3
+ * Device identity keypair (protocol §6): Ed25519, generated once on first
4
+ * pair and reused across re-pairs (§6.3 — "a fresh device keypair is not
5
+ * required" when re-pairing after revocation). The private key never
6
+ * leaves this device; only the public key ever goes over the wire
7
+ * (`PairRequest.devicePublicKey`).
8
+ *
9
+ * Encoding choice: an OKP (Ed25519) key's JWK `x` member is defined by
10
+ * RFC 8037 as the raw 32-byte public key, base64url-encoded with no
11
+ * padding — exactly "Ed25519 public key, base64url-encoded" per
12
+ * docs/protocol.md §6.1, and avoids shipping an ASN.1/SPKI wrapper the
13
+ * server would otherwise have to strip. Signatures are the raw 64-byte
14
+ * Ed25519 signature (`crypto.sign(null, data, privateKey)` — Ed25519 needs
15
+ * no digest algorithm), also base64url-encoded.
16
+ */
17
+ export interface DeviceKeyPair {
18
+ publicKeyBase64Url: string;
19
+ privateKey: KeyObject;
20
+ }
21
+ export declare function generateDeviceKeyPair(): DeviceKeyPair;
22
+ /** PKCS8 PEM — the on-disk storage form (0600 file under storeDir). OS keychain integration is deferred (tracked as a future roadmap item, not promised for any specific milestone). */
23
+ export declare function exportPrivateKeyPem(privateKey: KeyObject): string;
24
+ export declare function importPrivateKeyPem(pem: string): KeyObject;
25
+ /**
26
+ * S1 (GAP-004): the domain-separation prefix this device signs along with a
27
+ * challenge nonce, byte-identical to the server's own
28
+ * `NONCE_SIGNING_DOMAIN` (`packages/server/src/auth.ts`). The device key is a
29
+ * long-lived identity key that later planes will also sign structured
30
+ * messages with; tagging the domain is what stops a signature made for one of
31
+ * those from being replayable as a token-renewal credential.
32
+ *
33
+ * Not shared through a package: the two ends agree on a wire constant, and
34
+ * inventing a dependency between server and client to hold one string literal
35
+ * would couple them far harder than the literal does.
36
+ */
37
+ export declare const NONCE_SIGNING_DOMAIN = "byok-nonce-v1\n";
38
+ /**
39
+ * Sign a challenge nonce with the device private key: the signed message is
40
+ * {@link NONCE_SIGNING_DOMAIN} followed by `nonce` (UTF-8), and the result is
41
+ * the raw 64-byte Ed25519 signature, base64url-encoded (protocol §6.2). A
42
+ * server on the domain-separated contract rejects the undomained form, so
43
+ * there is no variant of this that omits the prefix.
44
+ */
45
+ export declare function signNonce(privateKey: KeyObject, nonce: string): string;
@@ -0,0 +1,41 @@
1
+ import { type DeviceProofEnvelopeV1 } from '@byok-sdk/core';
2
+ import type { DeviceStore } from './store';
3
+ export interface DeviceProofRequest {
4
+ readonly method: string;
5
+ /** Exact origin-relative path, including the query string when present. */
6
+ readonly path: string;
7
+ readonly operation: string;
8
+ readonly resource: string;
9
+ readonly requestId: string;
10
+ readonly body: Uint8Array;
11
+ readonly issuedAt?: string;
12
+ readonly expiresAt?: string;
13
+ readonly nonce?: string;
14
+ }
15
+ /** Local signing seam consumed by the truth client. It never exposes key bytes. */
16
+ export interface DeviceProofSigner {
17
+ sign(request: DeviceProofRequest): Promise<DeviceProofEnvelopeV1>;
18
+ }
19
+ export interface StoredDeviceProofSignerOptions {
20
+ readonly store: Pick<DeviceStore, 'load'>;
21
+ /** Explicit host configuration. Pairing/bearer state is never mined for tenant identity. */
22
+ readonly tenantId: string;
23
+ readonly productId: string;
24
+ readonly keyId: string;
25
+ readonly keyEpoch: number;
26
+ readonly clock?: () => Date;
27
+ }
28
+ /**
29
+ * Signs request-bound S6 proofs with the paired device identity key.
30
+ *
31
+ * The store is read for every signature rather than cached: clearing the paired
32
+ * record immediately removes local signing authority. Canonicalization is
33
+ * imported from `@byok-sdk/core`, the one frozen byte authority; this module only
34
+ * supplies the Node Ed25519 operation.
35
+ */
36
+ export declare class StoredDeviceProofSigner implements DeviceProofSigner {
37
+ #private;
38
+ private readonly options;
39
+ constructor(options: StoredDeviceProofSignerOptions);
40
+ sign(request: DeviceProofRequest): Promise<DeviceProofEnvelopeV1>;
41
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * M5: per-runtime environment allowlist for spawned agent child processes.
3
+ *
4
+ * Before this module existed, `task-runner.ts` built every task's
5
+ * `TaskContext.env` as `process.env` verbatim — the daemon's OWN full
6
+ * environment, unfiltered, handed to whichever runtime CLI (`pi`/`claude`/
7
+ * `codex`) `pickAdapter` selected. Any credential-shaped variable sitting in
8
+ * the daemon's own environment for a completely unrelated reason (an
9
+ * `AWS_SECRET_ACCESS_KEY`, `DATABASE_URL`, `GITHUB_TOKEN` set for the
10
+ * daemon's OWN deployment, nothing to do with any coding-agent runtime) was
11
+ * therefore inherited by every single spawned agent process — a
12
+ * credential-leak gap, not a deliberate design choice.
13
+ *
14
+ * {@link buildRuntimeEnv} replaces that blanket passthrough with an explicit
15
+ * allowlist, built fresh per task from three layers:
16
+ *
17
+ * 1. A small, always-included platform baseline ({@link BASE_PLATFORM_ALLOWLIST}
18
+ * / {@link WINDOWS_BASE_ALLOWLIST}) — the bare minimum any CLI needs to
19
+ * resolve its own binaries/libraries, find a home/temp directory, and
20
+ * behave sanely in a non-interactive shell.
21
+ * 2. Whatever ADDITIONAL names the *specific* runtime adapter about to be
22
+ * spawned declares it actually needs
23
+ * (`RuntimeAdapter.environmentRequirements()` — see `../types.ts`). An
24
+ * adapter that declares nothing at all (doesn't implement the optional
25
+ * method) gets the platform baseline ONLY — fail-closed by construction,
26
+ * not by an extra check here.
27
+ * 3. A per-device, per-runtime operator override (`DaemonConfig
28
+ * .runtimeEnvironment` — see `create-daemon.ts`) — a local escape hatch
29
+ * for a product/operator that knows it needs one more variable forwarded
30
+ * to one specific runtime on this one device.
31
+ *
32
+ * One hard, unconditional deny always wins over all three layers above,
33
+ * including the operator's own override: `BYOK_*`, this SDK's own
34
+ * control-plane variables, must never reach a spawned agent process — see
35
+ * {@link HARD_DENY_PATTERNS}.
36
+ *
37
+ * Every name in every list may be an exact match or a `*`-suffixed prefix
38
+ * (e.g. `'LC_*'` matches `LC_ALL`, `LC_CTYPE`, ...).
39
+ */
40
+ /**
41
+ * What one runtime adapter declares it needs beyond the always-included
42
+ * platform baseline. Returned from the optional
43
+ * `RuntimeAdapter.environmentRequirements()` method (`../types.ts`).
44
+ */
45
+ export interface RuntimeEnvironmentRequirements {
46
+ /**
47
+ * Extra non-secret, config-discovery-shaped variable names this runtime's
48
+ * own CLI reads (e.g. a `<RUNTIME>_CONFIG_DIR`-style override) — anything
49
+ * that isn't itself a credential. Optional: most adapters need nothing
50
+ * beyond the platform baseline.
51
+ */
52
+ baseNames?: readonly string[];
53
+ /**
54
+ * Credential/auth variable names this runtime's own CLI reads to
55
+ * authenticate (e.g. a provider API key). Kept as its own field (distinct
56
+ * from `baseNames`) so a product's own security review can reason about
57
+ * "what credential-shaped names does this runtime get" as a single,
58
+ * explicit list per adapter — see e.g. the pi adapter's
59
+ * `KNOWN_PROVIDER_ENV_VARS`.
60
+ */
61
+ credentialNames?: readonly string[];
62
+ }
63
+ /** Inputs to {@link buildRuntimeEnv}. */
64
+ export interface BuildRuntimeEnvOptions {
65
+ /**
66
+ * The daemon's own ambient environment (normally `process.env`). Never
67
+ * mutated — every returned variable is copied into a fresh object.
68
+ */
69
+ ambient: NodeJS.ProcessEnv;
70
+ /**
71
+ * The selected runtime adapter's own declared requirements —
72
+ * `undefined` (no `environmentRequirements()` implementation on that
73
+ * adapter) means "platform baseline only," fail-closed.
74
+ */
75
+ requirements?: RuntimeEnvironmentRequirements;
76
+ /**
77
+ * This device's own operator-configured escape hatch for this one runtime
78
+ * (`DaemonConfig.runtimeEnvironment?.[adapterId]?.allow`) — merged in like
79
+ * any other allowlist entry, still subject to the hard deny below.
80
+ */
81
+ locallyAllowedNames?: readonly string[];
82
+ /**
83
+ * Test seam: which platform's extra base vars to include
84
+ * ({@link WINDOWS_BASE_ALLOWLIST} vs none) — defaults to `process.platform`
85
+ * so callers never have to think about it, while still letting a test
86
+ * exercise the win32 branch deterministically on any host OS.
87
+ */
88
+ platform?: NodeJS.Platform;
89
+ }
90
+ /**
91
+ * Build the environment one specific runtime's spawned child process should
92
+ * actually receive — a fresh object, never `options.ambient` itself and
93
+ * never mutated in place. See this module's own doc comment for the full
94
+ * allow/deny model.
95
+ */
96
+ export declare function buildRuntimeEnv(options: BuildRuntimeEnvOptions): Record<string, string>;