@byok-sdk/client 0.3.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 (38) hide show
  1. package/README.md +13 -0
  2. package/dist/adapters/claude/claude-adapter.d.ts +4 -20
  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 -16
  6. package/dist/adapters/codex/process-runner.d.ts +4 -1
  7. package/dist/adapters/index.d.ts +3 -1
  8. package/dist/adapters/index.js +923 -258
  9. package/dist/adapters/index.js.map +1 -1
  10. package/dist/adapters/pi/pi-adapter.d.ts +3 -16
  11. package/dist/adapters/pi/rpc-client.d.ts +9 -1
  12. package/dist/adapters/process-tree.d.ts +19 -0
  13. package/dist/bin/audit-log.d.ts +12 -0
  14. package/dist/bin/byok-agent.js +1293 -484
  15. package/dist/bin/byok-agent.js.map +1 -1
  16. package/dist/bin/byok-approval-mcp.js.map +1 -1
  17. package/dist/bin/commands/workspaces.d.ts +11 -0
  18. package/dist/bin/format.d.ts +13 -0
  19. package/dist/bin/runtime-probe.d.ts +1 -1
  20. package/dist/bin/tasks-view.d.ts +13 -0
  21. package/dist/daemon/approvals.d.ts +2 -2
  22. package/dist/daemon/connection-manager.d.ts +4 -2
  23. package/dist/daemon/control-server.d.ts +18 -1
  24. package/dist/daemon/create-daemon.d.ts +2 -2
  25. package/dist/daemon/daemon-owner.d.ts +4 -2
  26. package/dist/daemon/environment.d.ts +9 -9
  27. package/dist/daemon/git-workspace.d.ts +21 -0
  28. package/dist/daemon/observer.d.ts +13 -0
  29. package/dist/daemon/presence-publisher.d.ts +29 -0
  30. package/dist/daemon/runtime-capabilities.d.ts +1 -1
  31. package/dist/daemon/task-runner.d.ts +27 -34
  32. package/dist/daemon/ws-transport.d.ts +3 -1
  33. package/dist/index.d.ts +4 -2
  34. package/dist/index.js +1254 -432
  35. package/dist/index.js.map +1 -1
  36. package/dist/runtime-failure.d.ts +64 -0
  37. package/dist/types.d.ts +100 -73
  38. package/package.json +4 -4
@@ -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);
@@ -188,6 +188,13 @@ export type DaemonEvent = {
188
188
  conflicted: number;
189
189
  };
190
190
  errorCategory?: string;
191
+ } | {
192
+ kind: 'runtime-disposal-failed';
193
+ ts: string;
194
+ taskId: string;
195
+ runtimeId: string;
196
+ stage: 'signal' | 'quiescence' | 'cleanup';
197
+ reason: string;
191
198
  }
192
199
  /**
193
200
  * Plan `device-assertion-broker`: one `assertion.issue` control call
@@ -350,6 +357,12 @@ export declare class DaemonObserver {
350
357
  };
351
358
  errorCategory?: string;
352
359
  }): void;
360
+ noteRuntimeDisposalFailure(event: {
361
+ taskId: string;
362
+ runtimeId: string;
363
+ stage: 'signal' | 'quiescence' | 'cleanup';
364
+ reason: string;
365
+ }): void;
353
366
  /**
354
367
  * Finding F4: wired from `TaskRunnerDeps.onApprovalDispatched`, called
355
368
  * synchronously by `TaskRunner.dispatchApproval` BEFORE its own
@@ -1,3 +1,30 @@
1
+ /**
2
+ * Presence producer (§12.3): a periodic `{ level: 'online' }` heartbeat to
3
+ * `PUT /byok/presence`, started only when the deployment's capability
4
+ * declaration contains `presence.hints` (ADR-010 — see `capabilities-client.ts`).
5
+ *
6
+ * Three properties this module deliberately holds:
7
+ *
8
+ * - **Only `online`.** The five levels exist in core, but nothing downstream
9
+ * consumes a `thinking`/`working`/`error` mapping yet, so none is invented
10
+ * here. That also keeps the rule below trivially true.
11
+ * - **It never reads or writes task state.** Presence is a lossy, unsigned,
12
+ * TTL-bounded hint that must never become coordination or execution state
13
+ * (`@byok-sdk/core`'s `presence.ts` module doc). This publisher's only input
14
+ * is a clock.
15
+ * - **Stopping IS the offline signal.** No explicit `offline` publish on
16
+ * shutdown: the hosted hint carries a TTL and expiry means *absence*, so a
17
+ * daemon that stops beating disappears on its own. Publishing `offline` too
18
+ * would express the same fact through a second channel that a crashed daemon
19
+ * could never use anyway.
20
+ *
21
+ * Auth reuses the daemon's one device-token lifecycle: {@link authedFetch}
22
+ * attaches the current bearer and, on a 401, renews once through `AuthManager`
23
+ * and retries exactly once. A revoked device (`DeviceRevokedError`) stops this
24
+ * publisher permanently — there is no recourse but a fresh `pair()`, so
25
+ * retrying would be a pure spin.
26
+ */
27
+ import { type ToolsetId } from '@byok-sdk/protocol';
1
28
  import type { AuthManager } from './auth-manager';
2
29
  /**
3
30
  * Client-side defaults, chosen against the hosted defaults
@@ -30,6 +57,8 @@ export declare function assertPresenceHeartbeatCadence(cadence: {
30
57
  export interface PresencePublisherOptions {
31
58
  serverUrl: string;
32
59
  auth: AuthManager;
60
+ /** Sorted logical IDs only. Executable MCP definitions and credentials remain device-local. */
61
+ configuredToolsets?: readonly ToolsetId[];
33
62
  /** Heartbeat cadence. Must sit strictly between {@link PresencePublisherOptions.minimumIntervalMs} and {@link PresencePublisherOptions.ttlMs}. */
34
63
  intervalMs?: number;
35
64
  /** The deployment's presence hint TTL, as this daemon understands it. Only used to validate the cadence. */
@@ -1,7 +1,7 @@
1
1
  import type { RuntimeCapabilities as ProtocolRuntimeCapabilities } from '@byok-sdk/protocol';
2
2
  import type { RuntimeCapabilities } from '../types';
3
3
  /**
4
- * Maps a `RuntimeAdapter`'s own internal `capabilities()` result
4
+ * Maps a frozen `RuntimeAdapterDescriptor`'s internal `capabilities` value
5
5
  * (`../types.ts`'s `RuntimeCapabilities` — `{steer, resume,
6
6
  * approvalInteractive, permissionModes}`, always-required fields) onto the
7
7
  * wire's `RuntimeCapabilities` shape (`@byok-sdk/protocol` — the same field names,
@@ -1,5 +1,6 @@
1
1
  import { type Envelope, type PermissionPolicy, type RuntimeId, type TaskOfferPayload, type TaskOfferWithToolsetsPayload } from '@byok-sdk/protocol';
2
2
  import { type McpToolsetConfig, type RuntimeAdapter } from '../types';
3
+ import { type RuntimeDisposalStage } from '../runtime-failure';
3
4
  import { type ApprovalDecision, type ApprovalOrigin, type ApprovalRegistry } from './approvals';
4
5
  import type { BlobResolver } from './blob-client';
5
6
  import type { TaskQueueWatermark } from './control-protocol';
@@ -205,13 +206,20 @@ export interface TaskRunnerDeps {
205
206
  observation?: GitWorkspaceObservation;
206
207
  errorCategory?: string;
207
208
  }) => void;
209
+ /** Local-only evidence that a semantic terminal outcome could not yet release its runtime ownership. */
210
+ onRuntimeDisposalFailure?: (event: {
211
+ taskId: string;
212
+ runtimeId: string;
213
+ stage: RuntimeDisposalStage;
214
+ reason: string;
215
+ }) => void;
208
216
  /**
209
217
  * M4 Phase 3: this daemon's control-socket identity + the shared registry
210
218
  * backing the control socket's own `approvals.list`/`approvals.resolve`
211
219
  * methods (`create-daemon.ts` constructs ONE `ApprovalRegistry` and passes
212
220
  * the SAME instance here) — see `requestApproval`'s own doc comment for
213
221
  * why `TaskRunner` needs a handle on all three. `storeDir`/`productId` are
214
- * copied verbatim into every task's `TaskContext.approvalChannel`.
222
+ * copied verbatim into every prepared operation's approval channel.
215
223
  */
216
224
  approvalRegistry: ApprovalRegistry;
217
225
  storeDir: string;
@@ -244,7 +252,7 @@ export interface TaskRunnerDeps {
244
252
  * to supply one — mirrors `onStaleApprovalDecision`'s own contract.
245
253
  */
246
254
  onApprovalDispatched?: (taskId: string, approvalId: string) => void;
247
- /** Finding F5(a): overrides {@link DEFAULT_SHUTDOWN_INTERRUPT_TIMEOUT_MS} — see `teardownActiveTask`'s own doc comment. Also the grace window `teardownActiveTask`'s hard-kill escalation (`session.close()`) gets once `session.interrupt()` itself fails to settle in time — see that method's own doc comment for why the same window is reused for both steps. */
255
+ /** Overrides the bounded soft-interrupt window before authoritative `Session.close()` disposal begins. */
248
256
  shutdownInterruptTimeoutMs?: number;
249
257
  /**
250
258
  * M5 batch-3 (workstream 2): overrides {@link DEFAULT_MAX_TASK_OUTPUT_BYTES}
@@ -319,8 +327,8 @@ export type AdmissionGuardDecision = {
319
327
  };
320
328
  type AcceptedOfferPayload = TaskOfferPayload | TaskOfferWithToolsetsPayload;
321
329
  /**
322
- * Per-connection task orchestration: offer -> (decline | claim -> adapter
323
- * session -> started) -> seq-ordered progress batches -> complete/fail/
330
+ * Per-connection task orchestration: offer -> (decline | prepare -> seal ->
331
+ * claim -> prepared operation -> started) -> seq-ordered progress batches -> complete/fail/
324
332
  * cancelled, plus approve/reject/cancel/steer handling.
325
333
  *
326
334
  * M1 rework (docs/protocol.md §3, §5, §10 — `packages/protocol` is frozen,
@@ -340,13 +348,13 @@ export declare class TaskRunner {
340
348
  * Finding F4 (cancel lost during the offer-processing window): a
341
349
  * `task.cancel` for a taskId that hasn't finished `handleOffer` yet (still
342
350
  * awaiting adapter detection / instruction resolution / workspace setup /
343
- * `adapter.start()`) has no `this.tasks` entry to land on — it used to be
351
+ * prepared operation `start()`) has no `this.tasks` entry to land on — it used to be
344
352
  * silently dropped, and the runtime session `handleOffer` was about to
345
353
  * register would then run an unsupervised ("zombie") turn nobody asked
346
354
  * for anymore. Recording the taskId here lets `handleOffer` consult it at
347
355
  * the two points where it can still safely react (see its body): before
348
356
  * claiming at all (decline instead of ever starting a session), and right
349
- * after `adapter.start()` resolves but before this task is registered as
357
+ * after the prepared operation resolves but before this task is registered as
350
358
  * active (tear the just-started session down immediately, before its
351
359
  * event loop ever pumps a single event). Consumed (deleted) at whichever
352
360
  * checkpoint handles it; a cancel for a taskId that's already active,
@@ -372,12 +380,12 @@ export declare class TaskRunner {
372
380
  * checkpoint-2 cancel-teardown, or successful registration into
373
381
  * `this.tasks`). Bounded eviction on `pendingCancelled` (below) must never
374
382
  * remove an entry for a taskId in this set: doing so is exactly the bug —
375
- * block task A in `adapter.start()`, deliver A's own `task.cancel` (so
383
+ * block task A in prepared-operation `start()`, deliver A's own `task.cancel` (so
376
384
  * `pendingCancelled` gets an entry for A while A is still in-flight),
377
385
  * then deliver `MAX_TRACKED_TASK_IDS` more cancels for unrelated taskIds
378
386
  * nobody ever offered — under naive oldest-wins eviction, A's entry (the
379
387
  * single oldest) gets evicted purely because of unrelated churn, so when
380
- * `adapter.start()` finally resolves, checkpoint 2 finds no cancel marker
388
+ * the prepared operation finally resolves, checkpoint 2 finds no cancel marker
381
389
  * and the already-cancelled task starts a real session. See
382
390
  * `evictPendingCancelled` below for the fix, and
383
391
  * `task-runner-bounded-collections.test.ts` for a test mirroring this
@@ -397,7 +405,7 @@ export declare class TaskRunner {
397
405
  * explicitly relies on redelivered handlers being idempotent for exactly
398
406
  * this reason). `handleOffer` must treat a redelivered offer for a taskId
399
407
  * that's already active (`this.tasks`) or already finished (this set) as
400
- * a no-op — never a second `adapter.start()` call, which would orphan the
408
+ * a no-op — never a second prepared-operation `start()` call, which would orphan the
401
409
  * first session.
402
410
  *
403
411
  * M3-B: unbounded otherwise — a long-lived daemon that's finished many
@@ -455,10 +463,9 @@ export declare class TaskRunner {
455
463
  /** M4 Phase 2: stop claiming any FUTURE `task.offer` — see `stoppingOffers`'s own doc comment. Idempotent. */
456
464
  stopAcceptingOffers(): void;
457
465
  /**
458
- * M4 Phase 2: best-effort shutdown of every currently ACTIVE task, for the
459
- * control socket's `shutdown` RPC. Mirrors `handleCancel`'s best-effort
460
- * `session.interrupt()` style (an interrupt failure is swallowed; the
461
- * terminal message is sent either way) but reports `task.fail` rather than
466
+ * Shutdown of every currently ACTIVE task for the control socket's
467
+ * `shutdown` RPC. Soft interrupt remains bounded, but each task's
468
+ * authoritative close receipt must settle successfully. Reports `task.fail` rather than
462
469
  * `task.cancelled` — these tasks aren't ending because the SERVER
463
470
  * cancelled them, they're ending because this device is shutting down.
464
471
  * `retryable: true` throughout: nothing about the task/policy itself was
@@ -509,28 +516,13 @@ export declare class TaskRunner {
509
516
  * unconditionally, so a hung `interrupt()` (a misbehaving adapter) can
510
517
  * never block `task.fail` from being sent at all.
511
518
  *
512
- * New in this batch hard-kill escalation: when `interrupt()` does NOT
513
- * settle within that same grace window, `session.close()` is tried next
514
- * (ALSO raced against `timeoutMs`, for the identical reason: a hung
515
- * `close()` must not be able to block this forever either — which matters
516
- * far more here than it used to for the pre-existing graceful-shutdown-only
517
- * caller, since THAT path is additionally bounded by an outer deadline
518
- * (`SHUTDOWN_TASK_TEARDOWN_DEADLINE_MS`/`DaemonConfig.shutdownGraceMs`,
519
- * `create-daemon.ts`), while resource-limit enforcement fires during
520
- * ordinary operation with no such outer bound watching it). `close()` is
521
- * every adapter's harder teardown primitive — an actual process-level kill
522
- * (SIGTERM, or `taskkill /F` on Windows — see e.g.
523
- * `ClaudeProcessClient.kill()`/`PiRpcClient.kill()`) as opposed to pi's own
524
- * soft in-band `interrupt()` (an RPC `abort` message that leaves the
525
- * process alive and resumable) — so escalating to it is the closest thing
526
- * to a "hard kill" the `Session` interface exposes. `finish()` below calls
527
- * `session.close()` again regardless (documented idempotent) — this isn't
528
- * a substitute for that, only an earlier, bounded attempt at actually
529
- * stopping a stuck runtime before this method gives up and reports failure
530
- * anyway.
519
+ * After the bounded soft interrupt, `finish()` always awaits the authoritative
520
+ * `Session.close()` receipt. A failed receipt retains active/Git ownership;
521
+ * shutdown surfaces the rejection while resource enforcement leaves local
522
+ * evidence for a later retry.
531
523
  *
532
524
  * Re-checks task identity (`this.tasks.get(...) === active`) immediately
533
- * before sending `task.fail`: the interrupt/hard-kill race above has await
525
+ * before sending `task.fail`: the interrupt race above has await
534
526
  * points during which a DIFFERENT path (a racing `task.cancel`/
535
527
  * `task.reject`, or the session completing normally on its own) may have
536
528
  * already finished this exact task and sent its own terminal message.
@@ -623,7 +615,7 @@ export declare class TaskRunner {
623
615
  *
624
616
  * `inFlightOffers` is naturally tiny (bounded by this device's real
625
617
  * concurrent-offer-processing count — normally single digits, driven by
626
- * how many `task.offer`s are simultaneously mid-`adapter.start()` — nowhere
618
+ * how many `task.offer`s are simultaneously mid-prepared-operation start() — nowhere
627
619
  * near `MAX_TRACKED_TASK_IDS`), so this scan is cheap in practice: it
628
620
  * finds a safe entry at or near the front almost always. The only case
629
621
  * where NO entry is safe to evict is every single tracked cancel
@@ -952,6 +944,7 @@ export declare class TaskRunner {
952
944
  private observeGit;
953
945
  private updateGitPhaseBestEffort;
954
946
  private finish;
947
+ private reserveSemanticTerminal;
955
948
  /** M3-B: bounded insert for `finishedTaskIds` — see its class-level doc comment and `MAX_TRACKED_TASK_IDS`. Evicts the oldest (first-inserted) entry once over cap, same idiom as `ConnectionHub.checkAndRecordDuplicate` (packages/server/src/hub.ts). */
956
949
  private addFinishedTaskId;
957
950
  /** `reuseDir`, when set (a known sessionRef's recorded workspace), is used verbatim instead of a fresh `workspaceRoot/<taskId>` directory — `mkdir recursive` is idempotent either way, so ensuring-exists is safe to do unconditionally. */
@@ -1,4 +1,4 @@
1
- import { type CapabilityFlag, type Envelope, type RuntimeInfo } from '@byok-sdk/protocol';
1
+ import { type CapabilityFlag, type Envelope, type RuntimeInfo, type ToolsetId } from '@byok-sdk/protocol';
2
2
  export type ConnectionState = 'connecting' | 'open' | 'closed' | 'degraded' | 'revoked';
3
3
  /** The WS upgrade itself was rejected with a non-101 HTTP status (e.g. 401 for an expired/invalid bearer token). Surfaced via `onConnectOutcome` so `ConnectionManager` can force a reactive token renewal before the next attempt (protocol §6.2, "reactively on 401"). */
4
4
  export declare class WsUnexpectedStatusError extends Error {
@@ -25,6 +25,8 @@ export interface WsTransportOptions {
25
25
  capabilities: CapabilityFlag[];
26
26
  /** Detected runtimes, sent on every `conn.hello` (protocol §10 gap #4/#11). */
27
27
  runtimes?: RuntimeInfo[];
28
+ /** Sorted logical IDs configured locally; no MCP executable definition crosses the wire. */
29
+ configuredToolsets?: readonly ToolsetId[];
28
30
  /** The redelivery cursor to send as `conn.hello.cursor` (protocol §9) — read fresh on every connect so a value learned mid-connection is used on the next reconnect. */
29
31
  getCursor?: () => number | undefined;
30
32
  onEnvelope: (envelope: Envelope) => void;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
- export type { RuntimeAdapter, RuntimeCapabilities, RuntimeDetectResult, Session, TaskContext, GitWorkspaceConfig, McpStdioServerConfig, McpToolsetConfig, } from './types';
2
- export { PolicyUnsupportedError, SteerUnsupportedError } from './types';
1
+ export type { RuntimeAdapter, RuntimeAdapterDescriptor, RuntimeAdapterPrepareInput, RuntimeAdapterPrepareResult, RuntimeAdapterRejectedOperation, RuntimeAdapterPreparedOperation, PreparedRuntimeOperation, RuntimeOperationManifest, RuntimeOperationStartInput, RuntimeCapabilities, RuntimeDetectResult, Session, GitWorkspaceConfig, McpStdioServerConfig, McpToolsetConfig, } from './types';
2
+ export { PolicyUnsupportedError, SteerUnsupportedError, freezeRuntimeAdapterDescriptor, sealRuntimeOperationManifest } from './types';
3
3
  export type { RuntimeEnvironmentRequirements } from './daemon/environment';
4
+ export { RuntimeExecutionFailure, RuntimeDisposalFailure, RUNTIME_ADAPTER_CONTRACT_VIOLATION_REASON, isRuntimeDisposalFailure, isRuntimeExecutionFailure, projectRuntimeBoundaryFailure, projectRuntimeExecutionFailure, } from './runtime-failure';
5
+ export type { RuntimeExecutionFailureInput, RuntimeDisposalFailureInput, RuntimeDisposalStage, RuntimeFailureCategory, RuntimeFailurePhase, RuntimeFailureProjection, RuntimeRetryDisposition, } from './runtime-failure';
4
6
  export { GitWorkspaceManager, GitWorkspaceError, isGitWorkspaceConfig, prependGitWorkspaceGuidance } from './daemon/git-workspace';
5
7
  export type { GitWorkspaceObservation, GitWorkspaceLease, GitWorkspaceOptions, GitErrorCategory } from './daemon/git-workspace';
6
8
  export { GitWorkspaceStore } from './daemon/git-workspace-store';