@byok-sdk/client 0.2.0 → 0.4.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 (46) hide show
  1. package/README.md +58 -5
  2. package/dist/adapters/claude/claude-adapter.d.ts +6 -19
  3. package/dist/adapters/claude/events.d.ts +3 -0
  4. package/dist/adapters/claude/process-client.d.ts +9 -1
  5. package/dist/adapters/codex/codex-adapter.d.ts +4 -15
  6. package/dist/adapters/codex/process-runner.d.ts +4 -1
  7. package/dist/adapters/index.d.ts +4 -2
  8. package/dist/adapters/index.js +1081 -258
  9. package/dist/adapters/index.js.map +1 -1
  10. package/dist/adapters/pi/pi-adapter.d.ts +24 -15
  11. package/dist/adapters/pi/rpc-client.d.ts +9 -1
  12. package/dist/adapters/process-tree.d.ts +19 -0
  13. package/dist/adapters/provider-credential-environment.d.ts +18 -0
  14. package/dist/bin/audit-log.d.ts +12 -0
  15. package/dist/bin/byok-agent.js +2686 -912
  16. package/dist/bin/byok-agent.js.map +1 -1
  17. package/dist/bin/byok-approval-mcp.js +2 -2
  18. package/dist/bin/byok-approval-mcp.js.map +1 -1
  19. package/dist/bin/commands/workspaces.d.ts +11 -0
  20. package/dist/bin/format.d.ts +13 -0
  21. package/dist/bin/runtime-probe.d.ts +1 -1
  22. package/dist/bin/tasks-view.d.ts +13 -0
  23. package/dist/daemon/approvals.d.ts +2 -2
  24. package/dist/daemon/assertion-client.d.ts +68 -0
  25. package/dist/daemon/capabilities-client.d.ts +48 -0
  26. package/dist/daemon/connection-manager.d.ts +4 -2
  27. package/dist/daemon/control-protocol.d.ts +81 -4
  28. package/dist/daemon/control-server.d.ts +18 -1
  29. package/dist/daemon/create-daemon.d.ts +171 -3
  30. package/dist/daemon/daemon-owner.d.ts +37 -0
  31. package/dist/daemon/device-assertion-signer.d.ts +41 -0
  32. package/dist/daemon/device-keys.d.ts +15 -13
  33. package/dist/daemon/environment.d.ts +9 -9
  34. package/dist/daemon/git-workspace.d.ts +21 -0
  35. package/dist/daemon/observer.d.ts +81 -3
  36. package/dist/daemon/presence-publisher.d.ts +98 -0
  37. package/dist/daemon/runtime-capabilities.d.ts +1 -1
  38. package/dist/daemon/skill-pack-installer.d.ts +116 -0
  39. package/dist/daemon/task-runner.d.ts +156 -37
  40. package/dist/daemon/ws-transport.d.ts +3 -1
  41. package/dist/index.d.ts +25 -4
  42. package/dist/index.js +2972 -597
  43. package/dist/index.js.map +1 -1
  44. package/dist/runtime-failure.d.ts +64 -0
  45. package/dist/types.d.ts +114 -58
  46. package/package.json +4 -4
@@ -27,12 +27,20 @@ export declare const HANDSHAKE_TIMEOUT_MS = 3000;
27
27
  * time this matters — see `control-server.ts`'s `startControlServer`);
28
28
  * falls back, whenever the natural path would risk exceeding {@link
29
29
  * UNIX_SOCKET_PATH_SOFT_LIMIT}, to a short, deterministic path nested under
30
- * a PER-DAEMON PRIVATE subdirectory of `os.tmpdir()` — derived from a hash
31
- * of `storeDir` alone, so both the daemon and any CLI invocation pointed at
32
- * the same `storeDir` independently compute the identical fallback path.
30
+ * a PER-DAEMON PRIVATE subdirectory of {@link CONTROL_SOCKET_FALLBACK_ROOT}
31
+ * — derived from a hash of `storeDir` alone, so both the daemon and any CLI
32
+ * invocation pointed at the same `storeDir` independently compute the
33
+ * identical fallback path.
34
+ *
35
+ * That root was `os.tmpdir()` until it was proven to break both halves of
36
+ * that sentence: it reads `TMPDIR`, so the daemon (under a service manager)
37
+ * and the CLI (in an operator shell) derived DIFFERENT addresses for one
38
+ * store, and under a `TMPDIR` nested in the same long tree the fallback came
39
+ * out LONGER than the path it escaped — `bind()` `EINVAL`, and the daemon
40
+ * ran on with no control socket at all.
33
41
  *
34
42
  * Nested one level deep (rather than a bare `<hash>.sock` file directly in
35
- * the shared, world-traversable `os.tmpdir()`) specifically so
43
+ * that shared, world-traversable root) specifically so
36
44
  * `control-server.ts`'s `bindControlEndpoint` can create+chmod that
37
45
  * subdirectory 0700 BEFORE ever binding inside it — the directory's own
38
46
  * mode gates traversal into it regardless of the socket file's own
@@ -292,6 +300,75 @@ export interface ApprovalsRequestResult {
292
300
  approved: boolean;
293
301
  reason?: string;
294
302
  }
303
+ /**
304
+ * Params for `assertion.issue`: a sibling local process (the host's own CLI,
305
+ * installed alongside this daemon) asking the daemon to mint one short-lived,
306
+ * audience-scoped device assertion with the paired device key. See
307
+ * `@byok-sdk/core`'s `device-assertion.ts` for the envelope, and
308
+ * `create-daemon.ts`'s handler for the six fail-closed gates every call passes
309
+ * through in a fixed order.
310
+ *
311
+ * One field, and nothing else. In particular there is deliberately no caller
312
+ * identity, no requested TTL, and no requested claim set: every process running
313
+ * as this UID can reach the control socket, so anything a caller "tells" the
314
+ * daemon about itself is decoration, and a caller-chosen lifetime is just the
315
+ * TTL ceiling handed to whoever asks.
316
+ */
317
+ export interface AssertionIssueParams {
318
+ audience: string;
319
+ }
320
+ /**
321
+ * Bound on the `audience` a caller may send, in UTF-8 bytes — mirrors
322
+ * `@byok-sdk/core`'s `DEVICE_ASSERTION_AUDIENCE_MAX_BYTES`. Restated here rather
323
+ * than imported so the WIRE bound is checked before anything reaches the claim
324
+ * schema: this is the frame-level shape gate, and it must reject an oversized
325
+ * value without that value ever reaching a signer or an audit line.
326
+ */
327
+ export declare const ASSERTION_AUDIENCE_MAX_BYTES = 256;
328
+ /**
329
+ * Strict shape check. `undefined` means `bad_request` — a distinct gate from
330
+ * `audience_denied` (see `create-daemon.ts`): "you sent something that is not a
331
+ * request" and "you asked for an audience you may not have" are different
332
+ * facts, and collapsing them would let a caller probe the allowlist by
333
+ * malforming requests.
334
+ *
335
+ * Rejects an unknown key outright rather than ignoring it. A tolerated extra
336
+ * field is how a future caller comes to believe it can influence the claim set.
337
+ */
338
+ export declare function parseAssertionIssueParams(value: unknown): AssertionIssueParams | undefined;
339
+ /**
340
+ * Result of `assertion.issue`. `assertion` is the full signing envelope
341
+ * (`DeviceAssertionEnvelopeV1`), carried as an opaque JSON value on this wire —
342
+ * the caller hands it to the host's cloud, which parses and verifies it with
343
+ * core's own `verifyDeviceAssertion`. `expiresAt` is repeated outside the
344
+ * envelope purely so a caller can schedule a refresh without parsing claims it
345
+ * has no business interpreting.
346
+ */
347
+ export interface AssertionIssueResult {
348
+ assertion: unknown;
349
+ expiresAt: string;
350
+ }
351
+ /**
352
+ * The six `ControlError` codes `assertion.issue` can answer with, in the exact
353
+ * order the handler checks them (`create-daemon.ts`). Each one is a distinct
354
+ * refusal with a distinct cause; none of them ever signs anything.
355
+ *
356
+ * - `assertion_disabled` — this daemon has no `deviceAssertion` config, or an
357
+ * empty audience allowlist. The feature is OFF by default.
358
+ * - `bad_request` — params were not `{audience: string}` within the byte bound.
359
+ * - `audience_denied` — the audience is not in the configured allowlist. The
360
+ * message never echoes the allowlist: a refusal must not be an enumeration
361
+ * oracle.
362
+ * - `shutting_down` — a shutdown has been requested. Closes the window between
363
+ * the shutdown RPC being acknowledged and the control socket actually
364
+ * closing, during which a device that is being unpaired could otherwise still
365
+ * mint credentials.
366
+ * - `revoked` — the server has revoked this device.
367
+ * - `not_paired` — there is no device record on disk (never paired, or already
368
+ * cleared).
369
+ */
370
+ export declare const ASSERTION_ISSUE_ERROR_CODES: readonly ['assertion_disabled', 'bad_request', 'audience_denied', 'shutting_down', 'revoked', 'not_paired'];
371
+ export type AssertionIssueErrorCode = (typeof ASSERTION_ISSUE_ERROR_CODES)[number];
295
372
  export type ShutdownReason = 'unpair' | 'operator';
296
373
  export interface ShutdownParams {
297
374
  reason?: ShutdownReason;
@@ -38,7 +38,24 @@ export interface ControlServerOptions {
38
38
  export interface ControlServerHandle {
39
39
  /** The bound Unix socket path or Windows pipe name. */
40
40
  endpoint: string;
41
- /** Stops accepting new connections, destroys every open one, and removes the socket/token files (a Windows pipe leaves no file to remove). Idempotent. */
41
+ /**
42
+ * Stops accepting new connections and destroys every open one, WITHOUT
43
+ * removing the socket/token files. Idempotent, and the first half of
44
+ * {@link close}.
45
+ *
46
+ * Split out from `close()` for `create-daemon.ts`'s shutdown sequence
47
+ * (plan `shutdown-lease-order`): the socket/token files ARE this daemon's
48
+ * only external liveness signal (`isControlDaemonGone` in
49
+ * `bin/control-client.ts` reads "token file gone AND a connect refused"),
50
+ * so a shutdown must be able to stop serving RPCs, release its
51
+ * store-mutation lease, and only THEN publish "gone" by removing the
52
+ * files. Between the two stages a connect is already refused while the
53
+ * token file still exists — which reads as "still running", the
54
+ * conservative answer, rather than as "exited while still holding the
55
+ * lease".
56
+ */
57
+ stopServing(): Promise<void>;
58
+ /** Runs {@link stopServing}, then removes the socket/token files (a Windows pipe leaves no file to remove). Idempotent. */
42
59
  close(): Promise<void>;
43
60
  }
44
61
  /**
@@ -1,6 +1,6 @@
1
1
  import type { RuntimeId } from '@byok-sdk/protocol';
2
2
  import type { PermissionPolicy } from '@byok-sdk/protocol';
3
- import type { RuntimeAdapter, GitWorkspaceConfig } from '../types';
3
+ import type { RuntimeAdapter, GitWorkspaceConfig, McpToolsetConfig } from '../types';
4
4
  import type { BackoffOptions, LivenessOptions } from './ws-transport';
5
5
  import { type OperationalHealthSnapshot } from './operational-health';
6
6
  import { type DaemonEventListener, type DaemonTaskInfo, type Unsubscribe } from './observer';
@@ -10,6 +10,7 @@ import { type DeviceRecord } from './store';
10
10
  import { type LocalTaskJournal } from './journal/journal';
11
11
  import { type JournalOpenFaultSeam } from './journal/sqlite-support';
12
12
  import { LocalStoragePressureEngine, type LocalStoragePolicyInput } from './journal/storage-policy';
13
+ import { type ResultDocumentExtractor } from './task-runner';
13
14
  import type { ProgressBatcherOptions } from './progress-batcher';
14
15
  /**
15
16
  * Optional white-label product display info — purely opaque passthrough
@@ -104,12 +105,18 @@ export interface DaemonConfig {
104
105
  * unchanged from M1/M2.
105
106
  */
106
107
  runtimeAllowlist?: string[];
108
+ /**
109
+ * Separate-process Pi BYOK credential boundary. Required only for a
110
+ * `dispatchSelection` in the BYOK lane; subscription runtimes and legacy
111
+ * Pi tasks do not invoke it.
112
+ */
113
+ piByokLauncher?: import('../adapters/pi/pi-adapter').PiByokLauncherConfig;
107
114
  /**
108
115
  * M5 batch-3 (workstream 1): explicit auto-select priority order for
109
116
  * `TaskRunner.pickAdapter`'s no-explicit-runtime branch (`task-runner.ts`)
110
117
  * — tried in listed order; the first candidate that is both PRESENT
111
118
  * (`adapter.detect()`) and CAPABLE (declares the offer's
112
- * `PermissionPolicy.mode` in its own `capabilities().permissionModes` —
119
+ * `PermissionPolicy.mode` in its own `descriptor.capabilities.permissionModes` —
113
120
  * see `adapterSupportsMode`) wins. Unset defaults to
114
121
  * `DEFAULT_RUNTIME_PREFERENCE` (`task-runner.ts`): `['claude', 'codex',
115
122
  * 'pi']` — pi LAST, deliberately.
@@ -169,12 +176,19 @@ export interface DaemonConfig {
169
176
  * an id with no matching adapter is simply never looked up). `allow`
170
177
  * entries are exact variable names or `*`-suffixed prefixes, merged in
171
178
  * alongside that runtime adapter's own declared
172
- * `environmentRequirements()` — this can never override the hard
179
+ * `descriptor.environmentRequirements` — this can never override the hard
173
180
  * `BYOK_*` deny (see `environment.ts`'s own doc comment).
174
181
  */
175
182
  runtimeEnvironment?: Record<string, {
176
183
  allow?: string[];
177
184
  }>;
185
+ /**
186
+ * Device-local registry behind wire-level `requiredToolsets` ids. Only
187
+ * logical ids cross the SaaS wire; MCP executable definitions stay here.
188
+ * The first slice supports stdio servers (`command` + `args`) only and
189
+ * deliberately has no task-provided env/header/secret surface.
190
+ */
191
+ mcpToolsets?: Record<string, McpToolsetConfig>;
178
192
  /**
179
193
  * M5: explicit escape hatch for `url.ts`'s `assertServerUrlAllowed` — see
180
194
  * that function's own doc comment for the full allow/deny rule. Default
@@ -209,6 +223,48 @@ export interface DaemonConfig {
209
223
  * explicitly instead to opt out of enforcement altogether.
210
224
  */
211
225
  maxTaskOutputBytes?: number;
226
+ /**
227
+ * additive-minor (`task.complete.document`): the seam through which this
228
+ * product turns a finished task's final output text into the STRUCTURED
229
+ * terminal result the wire carries as `task.complete.document`, and the
230
+ * server projects into `TaskResult.document`.
231
+ *
232
+ * `extract(finalOutput, task)` is called exactly once per task, at the
233
+ * moment `task.complete` is built, with the same text that becomes
234
+ * `summary` (the concatenated `progress` events for that task) plus the
235
+ * task's `taskId`/`sessionRef`. Return `undefined` for "no structured
236
+ * result this time". Everything about the document's SHAPE is the
237
+ * product's business — the SDK never inspects, validates, or transforms
238
+ * it; extraction logic (prompting for JSON, parsing a fenced block,
239
+ * validating against the product's own schema) is product glue and belongs
240
+ * in this callback, not in the SDK.
241
+ *
242
+ * The SDK enforces exactly two wire rules, via the protocol's own
243
+ * `checkResultDocument`: the value must be JSON-serializable, and at most
244
+ * `RESULT_DOCUMENT_MAX_BYTES` (1 MiB) as canonical JSON. Stay under ~512
245
+ * KiB in practice (docs/protocol.md); a bigger result belongs in an
246
+ * artifact, not here.
247
+ *
248
+ * FAIL-CLOSED, never silent: if the extractor throws, returns a promise
249
+ * (the seam is synchronous and the runtime enforces it — an unawaited
250
+ * promise would be encoded as an empty document), produces something
251
+ * unsendable, or produces a document while the connected server never
252
+ * advertised the `result-document` capability (an old server would strip
253
+ * the field on arrival without a word), the task is reported as
254
+ * `task.fail` with `retryable: false` and a reason prefixed
255
+ * `result document undeliverable` — see
256
+ * `RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX` (`task-runner.ts`).
257
+ * Completing a task while quietly discarding the structured result it
258
+ * exists to produce is not an option this SDK offers.
259
+ *
260
+ * Omitted entirely by default, in which case the completion path is
261
+ * unchanged in every respect — no extractor call, no capability check, and
262
+ * a `task.complete` payload byte-identical to the one sent before this
263
+ * field existed.
264
+ */
265
+ resultDocument?: {
266
+ readonly extract: ResultDocumentExtractor;
267
+ };
212
268
  /**
213
269
  * M5 batch-3 (workstream 2): deadline bound on the graceful-shutdown
214
270
  * sequence's own wait for `TaskRunner.shutdownActiveTasks` to finish
@@ -221,6 +277,85 @@ export interface DaemonConfig {
221
277
  * existed.
222
278
  */
223
279
  shutdownGraceMs?: number;
280
+ /**
281
+ * Cadence for the `online` presence heartbeat (§12.3), when — and only when
282
+ * — the deployment's capability declaration contains `presence.hints` (see
283
+ * `capabilities-client.ts`; a deployment that declares nothing, or one this
284
+ * daemon could not read a declaration from, publishes nothing at all).
285
+ *
286
+ * Every field is optional and defaults to `presence-publisher.ts`'s own
287
+ * constants, which are chosen against the hosted defaults. `ttlMs` and
288
+ * `minimumIntervalMs` describe THE DEPLOYMENT's hint TTL and publication
289
+ * throttle as this operator understands them: the daemon never learns either
290
+ * from the wire, and uses them only to validate `intervalMs` sits strictly
291
+ * between them — a cadence outside that band is rejected synchronously here,
292
+ * the same way `maxTaskOutputBytes` is above, rather than degrading into a
293
+ * rate-limited or flickering hint nobody sees an error for.
294
+ */
295
+ presence?: PresenceConfig;
296
+ /**
297
+ * Plan `device-assertion-broker`: opt-in local assertion broker — lets a
298
+ * sibling process on this same machine (typically the host's own CLI,
299
+ * installed alongside this daemon) ask the daemon, over the already
300
+ * authenticated control socket, to mint a short-lived audience-scoped
301
+ * assertion signed with the paired device key. See
302
+ * {@link DeviceAssertionConfig}.
303
+ *
304
+ * OFF by default, and off is expressed two ways that mean the same thing: an
305
+ * absent section, or a present one with an empty `audiences` list. Both make
306
+ * `assertion.issue` answer `assertion_disabled` without looking at anything
307
+ * else. A new local authentication surface does not get to be on because
308
+ * someone left a config key behind.
309
+ */
310
+ deviceAssertion?: DeviceAssertionConfig;
311
+ }
312
+ /**
313
+ * Plan `device-assertion-broker`. Two fields, both about what this daemon will
314
+ * refuse.
315
+ *
316
+ * Every field is validated synchronously at construction, the same way
317
+ * `maxTaskOutputBytes` and the presence cadence are — a misconfigured
318
+ * authentication surface must fail when the daemon is built, not on the first
319
+ * call that needed it.
320
+ */
321
+ export interface DeviceAssertionConfig {
322
+ /**
323
+ * The EXACT audience strings this daemon will mint for. Matched with
324
+ * `Set.has` — exact string equality, never a prefix or suffix or subdomain
325
+ * rule.
326
+ *
327
+ * Prefix matching is the classic hole here: an allowlist entry of
328
+ * `salesko-api` under a `startsWith` rule also admits `salesko-api.evil.com`,
329
+ * and a suffix rule admits `evil-salesko-api`. There is no configuration
330
+ * that turns this into a pattern match, because there is no pattern-matching
331
+ * code to configure.
332
+ *
333
+ * An empty list means the feature is off (see
334
+ * `DaemonConfig.deviceAssertion`). Duplicate entries, empty entries, and
335
+ * entries over 256 UTF-8 bytes are construction errors — a duplicate is
336
+ * usually a copy-paste that hid a typo'd second entry, and silently
337
+ * de-duplicating it would hide it for good.
338
+ */
339
+ audiences: string[];
340
+ /**
341
+ * Assertion lifetime, ms. Default
342
+ * `DEVICE_ASSERTION_DEFAULT_TTL_MS` (120s), hard ceiling
343
+ * `DEVICE_ASSERTION_MAX_TTL_MS` (300s) — both from `@byok-sdk/core`, which
344
+ * enforces the same ceiling again at verification time, so a daemon patched
345
+ * to ignore this one still cannot get a longer-lived assertion accepted.
346
+ *
347
+ * Deliberately NOT caller-selectable over the control socket: a lifetime a
348
+ * caller can ask for is a lifetime every caller asks the maximum of.
349
+ */
350
+ ttlMs?: number;
351
+ }
352
+ export interface PresenceConfig {
353
+ /** Heartbeat cadence. Default 30s. */
354
+ intervalMs?: number;
355
+ /** The deployment's presence hint TTL. Default 90s (core §12.7.5 suggests 60-120s). */
356
+ ttlMs?: number;
357
+ /** The deployment's minimum interval between accepted publications. Default 5s. */
358
+ minimumIntervalMs?: number;
224
359
  }
225
360
  export interface DaemonStatus {
226
361
  paired: boolean;
@@ -340,7 +475,40 @@ export interface DaemonOverrides {
340
475
  pressureEngine?: LocalStoragePressureEngine;
341
476
  };
342
477
  }
478
+ /**
479
+ * Plan `device-assertion-broker` (codex round-2 F3): the internal, NON-public
480
+ * test seam for observing assertion issuance.
481
+ *
482
+ * This is NOT reachable through `DaemonConfig`/`DaemonOverrides`, and is NOT
483
+ * re-exported from the package `index.ts` — a test imports it straight from
484
+ * this module. That isolation is the point. The earlier `DaemonOverrides.
485
+ * deviceAssertion.mint` seam replaced the SIGNER, which meant a production
486
+ * embedder (`DaemonOverrides` is public API) could inject a callback that
487
+ * received the whole `DeviceRecord` — private key included — and exfiltrate it
488
+ * or forge claims.
489
+ *
490
+ * `onIssued` is a strict OBSERVER, called only AFTER a real, successful sign,
491
+ * with non-secret metadata ONLY (`jti`, `audience`). It cannot see the private
492
+ * key, cannot alter the signature, the claims, or the audit event, and cannot
493
+ * be reached from any public type. A test counts these calls to prove a gate
494
+ * rejection never reached the signer (a rejection never calls `onIssued`).
495
+ */
496
+ export interface AssertionIssueProbe {
497
+ onIssued(meta: {
498
+ jti: string;
499
+ audience: string;
500
+ }): void;
501
+ }
343
502
  export declare function createDaemonWithAdapters(config: DaemonConfig, adapters: RuntimeAdapter[], overrides?: DaemonOverrides): Daemon;
503
+ /**
504
+ * codex round-2 F3: the real builder. Exported from THIS module but NOT from
505
+ * the package `index.ts`, so the optional `assertionProbe` (an
506
+ * {@link AssertionIssueProbe} post-sign observer) is reachable only by tests
507
+ * importing this module directly — never through any public type. The public
508
+ * `createDaemonWithAdapters` above forwards without it, so production has no
509
+ * observer and no signer-injection surface at all.
510
+ */
511
+ export declare function buildDaemonWithAdapters(config: DaemonConfig, adapters: RuntimeAdapter[], overrides?: DaemonOverrides, assertionProbe?: AssertionIssueProbe): Daemon;
344
512
  /**
345
513
  * Public white-label entry point (M0-M3): the "5-line launcher" — a product
346
514
  * only needs a `DaemonConfig`, no hand-built adapter list. The bundled
@@ -11,6 +11,43 @@ interface OwnerRecord {
11
11
  export interface DaemonOwnerLease {
12
12
  release(): Promise<void>;
13
13
  }
14
+ /**
15
+ * Where this store's lock lives. Keyed by the CANONICAL storeDir alone — not
16
+ * by product id or OS user — because the store is the resource being
17
+ * serialized: a symlink alias and its target, or two products pointed at one
18
+ * store directory, must contend for the same endpoint (see
19
+ * `acquireDaemonOwner`'s own note on resolving aliases before deriving this).
20
+ *
21
+ * POSIX prefers `<storeDir>/mutex.sock`, keeping the lock beside the store
22
+ * state it guards in a directory `ensureSecureDir` has already made 0700. A
23
+ * storeDir can be long enough that no address under it fits
24
+ * {@link UNIX_SOCKET_PATH_SOFT_LIMIT} — a storeDir alone can exceed the whole
25
+ * `sun_path` budget — so the second candidate drops the storeDir from the path
26
+ * entirely and carries it as a hash under {@link STORE_MUTEX_FALLBACK_ROOT},
27
+ * nested one level deep so that directory can be created 0700 BEFORE anything
28
+ * binds inside it (the nesting convention is `controlSocketPath`'s).
29
+ *
30
+ * `controlSocketPath`'s fallback now binds under its own FIXED `/tmp` root
31
+ * (`control-protocol.ts`'s `CONTROL_SOCKET_FALLBACK_ROOT` — not
32
+ * `os.tmpdir()`); this mutex must not regress to `os.tmpdir()` for the same
33
+ * two independent reasons proven by CI job 94334133652. Correctness:
34
+ * `os.tmpdir()` is environment-derived, and a lock address that differs
35
+ * between two contending processes admits two writers. Reachability: the
36
+ * caller may have pointed `TMPDIR` INSIDE the very tree that made the natural
37
+ * path too long — there, `os.tmpdir()` yields an address LONGER than the one
38
+ * being escaped (152 bytes vs 126 in that job), so the "fallback" cannot bind
39
+ * at all. A fixed short root is immune to both.
40
+ *
41
+ * win32 has no socket file to place; a named pipe lives in one flat
42
+ * machine-wide namespace, so the name carries the store hash to keep two
43
+ * stores from colliding, and no length branch is needed.
44
+ *
45
+ * @internal Exported for the regression guard only (never re-exported from
46
+ * `index.ts`): the crashed-holder recovery path can only be exercised by
47
+ * planting a stale socket file at the exact address this derives, and a test
48
+ * recomputing the derivation itself would prove nothing about this one.
49
+ */
50
+ export declare function storeMutexEndpoint(canonicalStoreDir: string, identity: string, platform?: NodeJS.Platform): string;
14
51
  export declare class DaemonOwnerActiveError extends Error {
15
52
  readonly role: OwnerRecord['role'] | 'unknown';
16
53
  constructor(role: OwnerRecord['role'] | 'unknown');
@@ -0,0 +1,41 @@
1
+ import { type DeviceAssertionClaims, type DeviceAssertionEnvelopeV1 } from '@byok-sdk/core';
2
+ import type { DeviceRecord } from './store';
3
+ /**
4
+ * Mints one device assertion (plan `device-assertion-broker`).
5
+ *
6
+ * This module exists so there is exactly ONE place in the client that touches
7
+ * the device private key for this envelope, and so "the key is never cached"
8
+ * is a property of a small readable file rather than a claim about a
9
+ * 2000-line one. Two rules it enforces structurally:
10
+ *
11
+ * 1. **No module-level key state.** There is no cache, no memo, no
12
+ * module-scope variable of any kind here. The `KeyObject` is created inside
13
+ * {@link mintDeviceAssertion} from the record the caller just read off disk
14
+ * and becomes unreachable when the function returns — the same
15
+ * read-the-store-every-time discipline `StoredDeviceProofSigner` documents,
16
+ * for the same reason: clearing `device.json` must remove local signing
17
+ * authority immediately, not at the next process restart.
18
+ * 2. **The caller supplies the record.** This function does not load the
19
+ * store, check revocation, or consult an allowlist. All of that is the
20
+ * daemon's fail-closed gate sequence (`create-daemon.ts`), and duplicating
21
+ * any of it here would create a second, quieter authority on whether an
22
+ * assertion may be minted at all.
23
+ */
24
+ export interface MintDeviceAssertionInput {
25
+ /** The record just read from disk — never a cached one. */
26
+ readonly record: DeviceRecord;
27
+ /** The paired server's normalized origin (`url.ts`'s `toHttpBase` → `origin`). */
28
+ readonly issuer: string;
29
+ readonly productId: string;
30
+ /** Already checked against the configured allowlist by the caller. */
31
+ readonly audience: string;
32
+ /** Already range-checked at daemon construction time. */
33
+ readonly ttlMs: number;
34
+ readonly now: Date;
35
+ }
36
+ export interface MintedDeviceAssertion {
37
+ readonly envelope: DeviceAssertionEnvelopeV1;
38
+ readonly claims: DeviceAssertionClaims;
39
+ readonly expiresAt: string;
40
+ }
41
+ export declare function mintDeviceAssertion(input: MintDeviceAssertionInput): MintedDeviceAssertion;
@@ -24,22 +24,24 @@ export declare function exportPrivateKeyPem(privateKey: KeyObject): string;
24
24
  export declare function importPrivateKeyPem(pem: string): KeyObject;
25
25
  /**
26
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.
27
+ * challenge nonce. The device key is a long-lived identity key that later
28
+ * planes will also sign structured messages with; tagging the domain is what
29
+ * stops a signature made for one of those from being replayable as a
30
+ * token-renewal credential.
32
31
  *
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.
32
+ * The literal itself now lives in `@byok-sdk/core` (`src/pairing.ts`), which
33
+ * the daemon, the hosted surface, and the reference server all already depend
34
+ * on. It used to be three copies, each commented as byte-identical to the
35
+ * others — an agreement that holds only until someone edits one of them.
36
+ * Re-exported here so this module's public surface is unchanged.
36
37
  */
37
- export declare const NONCE_SIGNING_DOMAIN = "byok-nonce-v1\n";
38
+ export { NONCE_SIGNING_DOMAIN } from '@byok-sdk/core';
38
39
  /**
39
40
  * 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.
41
+ * `NONCE_SIGNING_DOMAIN` followed by `nonce` (UTF-8) core's
42
+ * {@link nonceSigningBytes} produces those bytes and the result is the raw
43
+ * 64-byte Ed25519 signature, base64url-encoded (protocol §6.2). A server on the
44
+ * domain-separated contract rejects the undomained form, so there is no variant
45
+ * of this that omits the prefix.
44
46
  */
45
47
  export declare function signNonce(privateKey: KeyObject, nonce: string): string;
@@ -2,7 +2,7 @@
2
2
  * M5: per-runtime environment allowlist for spawned agent child processes.
3
3
  *
4
4
  * Before this module existed, `task-runner.ts` built every task's
5
- * `TaskContext.env` as `process.env` verbatim — the daemon's OWN full
5
+ * `RuntimeOperationStartInput.env` as `process.env` verbatim — the daemon's OWN full
6
6
  * environment, unfiltered, handed to whichever runtime CLI (`pi`/`claude`/
7
7
  * `codex`) `pickAdapter` selected. Any credential-shaped variable sitting in
8
8
  * the daemon's own environment for a completely unrelated reason (an
@@ -20,10 +20,9 @@
20
20
  * behave sanely in a non-interactive shell.
21
21
  * 2. Whatever ADDITIONAL names the *specific* runtime adapter about to be
22
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.
23
+ * (`RuntimeAdapter.descriptor.environmentRequirements` — see
24
+ * `../types.ts`). A descriptor that declares no names gets the platform
25
+ * baseline only; descriptors are required and frozen before claim.
27
26
  * 3. A per-device, per-runtime operator override (`DaemonConfig
28
27
  * .runtimeEnvironment` — see `create-daemon.ts`) — a local escape hatch
29
28
  * for a product/operator that knows it needs one more variable forwarded
@@ -39,8 +38,8 @@
39
38
  */
40
39
  /**
41
40
  * What one runtime adapter declares it needs beyond the always-included
42
- * platform baseline. Returned from the optional
43
- * `RuntimeAdapter.environmentRequirements()` method (`../types.ts`).
41
+ * platform baseline. Declared in the required frozen
42
+ * `RuntimeAdapter.descriptor.environmentRequirements` (`../types.ts`).
44
43
  */
45
44
  export interface RuntimeEnvironmentRequirements {
46
45
  /**
@@ -69,8 +68,9 @@ export interface BuildRuntimeEnvOptions {
69
68
  ambient: NodeJS.ProcessEnv;
70
69
  /**
71
70
  * The selected runtime adapter's own declared requirements —
72
- * `undefined` (no `environmentRequirements()` implementation on that
73
- * adapter) means "platform baseline only," fail-closed.
71
+ * `undefined` means "platform baseline only" for this helper. The public
72
+ * RuntimeAdapter descriptor always supplies this object before TaskRunner
73
+ * invokes the helper.
74
74
  */
75
75
  requirements?: RuntimeEnvironmentRequirements;
76
76
  /**
@@ -2,6 +2,27 @@ export interface GitWorkspaceConfig {
2
2
  mode: 'local-checkpoints';
3
3
  }
4
4
  export type GitErrorCategory = 'git-unavailable' | 'git-timeout' | 'git-output-limit' | 'git-command-failed' | 'workspace-root-invalid' | 'workspace-root-conflict' | 'workspace-not-owned' | 'repository-root-mismatch' | 'repository-invalid' | 'lease-busy' | 'ledger-invalid';
5
+ /**
6
+ * Runtime projection of {@link GitErrorCategory}: every union member, once,
7
+ * in union order — the single source of truth the CLI's stable-output
8
+ * validators (`bin/format.ts`, `bin/audit-log.ts`, `bin/tasks-view.ts`,
9
+ * `bin/commands/workspaces.ts`) project from when deciding which category
10
+ * strings from ledger records are stable enough to render, so those filters
11
+ * can never drift from the union. The `satisfies` half rejects a string
12
+ * that isn't a union member; the `AssertExhaustive` proof below rejects a
13
+ * union member missing from this list — extending either side alone is a
14
+ * compile error. The runtime half (no duplicates, every consumer projects
15
+ * exactly this list) is `__tests__/git-category-drift.test.ts`.
16
+ */
17
+ export declare const GIT_ERROR_CATEGORIES: readonly ["git-unavailable", "git-timeout", "git-output-limit", "git-command-failed", "workspace-root-invalid", "workspace-root-conflict", "workspace-not-owned", "repository-root-mismatch", "repository-invalid", "lease-busy", "ledger-invalid"];
18
+ /**
19
+ * Runtime projection of `GitWorkspacePhase` (the type itself lives in
20
+ * `git-workspace-store.ts`; the projection lives here beside
21
+ * {@link GIT_ERROR_CATEGORIES} so both category/phase single sources ship
22
+ * from one module) — same exhaustiveness contract, consumed by
23
+ * `bin/tasks-view.ts`'s phase filter.
24
+ */
25
+ export declare const GIT_WORKSPACE_PHASES: readonly ["preparing", "active", "completed", "failed", "cancelled", "interrupted", "salvage"];
5
26
  export declare class GitWorkspaceError extends Error {
6
27
  readonly category: GitErrorCategory;
7
28
  constructor(category: GitErrorCategory, message?: string);