@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,178 @@
1
+ import type { AgentEvent, PermissionPolicy, TaskOfferPayload } from '@byok-sdk/protocol';
2
+ import type { RuntimeEnvironmentRequirements } from './daemon/environment';
3
+ export type { RuntimeEnvironmentRequirements } from './daemon/environment';
4
+ export interface GitWorkspaceConfig {
5
+ mode: 'local-checkpoints';
6
+ }
7
+ /**
8
+ * Result of probing whether a runtime is usable on this machine. `authPresent`
9
+ * is computed without ever reading the runtime's own credential storage (see
10
+ * the credential-isolation rule on {@link RuntimeAdapter}) — it only reflects
11
+ * whether a recognized environment variable name is set.
12
+ */
13
+ export interface RuntimeDetectResult {
14
+ present: boolean;
15
+ version?: string;
16
+ authPresent?: boolean;
17
+ }
18
+ /** What a runtime adapter can do, advertised so the daemon can pick/validate adapters. */
19
+ export interface RuntimeCapabilities {
20
+ steer: boolean;
21
+ resume: boolean;
22
+ /**
23
+ * Whether this adapter can genuinely pause a running session on
24
+ * `needs_approval` and resume it from an out-of-band decision — i.e.
25
+ * whether {@link Session.resolveApproval} really resolves rather than
26
+ * throwing. This is the ONLY source of truth for the wire's
27
+ * `RuntimeInfo.capabilities.approvalInteractive` (`daemon/
28
+ * create-daemon.ts`'s `toRuntimeInfoCapabilities`); the daemon no longer
29
+ * hardcodes a value.
30
+ *
31
+ * Required, deliberately: a new adapter (or a test fake) that forgets to
32
+ * declare it fails to compile rather than silently defaulting to a claim
33
+ * it cannot back.
34
+ */
35
+ approvalInteractive: boolean;
36
+ /** Subset of {@link PermissionPolicy}'s `mode` values this adapter can express without widening. */
37
+ permissionModes: string[];
38
+ }
39
+ /**
40
+ * M4 Phase 3: the out-of-band approval channel `TaskRunner` (`daemon/
41
+ * task-runner.ts`) hands to an adapter's `start()` via `TaskContext
42
+ * .approvalChannel`, for a runtime whose approval mechanism genuinely needs
43
+ * to reach back into the daemon from OUTSIDE the adapter's own process — the
44
+ * claude adapter's concrete case: `claude`'s `--permission-prompt-tool`
45
+ * resolves a pending permission entirely inside a SEPARATE MCP-server child
46
+ * process claude itself spawns (see `bin/byok-approval-mcp.ts`), which has
47
+ * no in-process handle to this task's `Session` at all and must instead call
48
+ * back into the SAME daemon over its control socket. `storeDir`/`productId`
49
+ * are exactly what that out-of-process helper needs to find and authenticate
50
+ * against this daemon's control socket (`daemon/control-protocol.ts`
51
+ * `controlEndpointPath`/`controlTokenPath`); `taskId` is how its request gets
52
+ * correlated back to THIS task once it arrives. `resolve()` is the
53
+ * daemon-side counterpart: it resolves the single most-recently-registered
54
+ * pending approval for this task (via `TaskRunner.requestApproval`'s own
55
+ * `ApprovalRegistry` entry — see `daemon/approvals.ts`), and rejects if none
56
+ * is currently pending, mirroring `Session.resolveApproval`'s own
57
+ * no-notion-of-approval-pending fail-closed contract one level up.
58
+ *
59
+ * Optional and adapter-agnostic on purpose: only an adapter whose runtime
60
+ * genuinely supports an out-of-band pause (claude, today) ever reads this;
61
+ * every other adapter (pi, codex) ignores it exactly as before this field
62
+ * existed.
63
+ */
64
+ export interface ApprovalChannel {
65
+ taskId: string;
66
+ storeDir: string;
67
+ productId: string;
68
+ /** Default wait (ms) before the daemon force-resolves an unanswered approval request as a fail-closed rejection — see `TaskRunner.requestApproval`. */
69
+ timeoutMs: number;
70
+ /** Resolve the single currently-pending out-of-band approval for this task. Rejects if none is pending right now. */
71
+ resolve(approved: boolean, reason?: string): Promise<void>;
72
+ }
73
+ /**
74
+ * Per-task execution context handed to {@link RuntimeAdapter.start}. `policy`
75
+ * is the already fail-closed-checked *effective* policy (offer policy merged
76
+ * against the daemon's configured ceiling) — the adapter must obey this, not
77
+ * whatever the raw task offer's own `policy` field said.
78
+ */
79
+ export interface TaskContext {
80
+ workspaceDir: string;
81
+ policy: PermissionPolicy;
82
+ env: NodeJS.ProcessEnv;
83
+ /** Prepared local checkpoint repository metadata; absent for legacy plain workspaces. */
84
+ gitWorkspace?: {
85
+ workspaceId: string;
86
+ baseline?: string;
87
+ };
88
+ /** M4 Phase 3 — see {@link ApprovalChannel}. Optional/adapter-agnostic: unset for every adapter that never requests an out-of-band approval. */
89
+ approvalChannel?: ApprovalChannel;
90
+ }
91
+ /**
92
+ * A running (or resumable) unit of work on a runtime. One `Session` maps to
93
+ * one underlying runtime process/session for the lifetime of a task.
94
+ */
95
+ export interface Session {
96
+ /** Opaque runtime session id, reported back to the server via `task.complete.sessionRef`. */
97
+ sessionRef: string;
98
+ /** Normalized events for this session; the daemon batches these into `task.progress`. */
99
+ events: AsyncIterable<AgentEvent>;
100
+ /** Inject steering text into a running turn (mid-stream). */
101
+ steer(text: string): Promise<void>;
102
+ /** Send a new instruction on the same session after it has gone idle. */
103
+ followUp(task: TaskOfferPayload): Promise<void>;
104
+ /** Best-effort abort of the current turn (used for `task.cancel`). */
105
+ interrupt(): Promise<void>;
106
+ /** Tear down the underlying runtime process/session. Idempotent. */
107
+ close(): Promise<void>;
108
+ /**
109
+ * Resolve a session paused on `needs_approval` (protocol §5). The
110
+ * server's own state has already moved by the time this is called (§4 —
111
+ * `task.approve`/`task.reject` are best-effort notifications, not
112
+ * requests awaiting a reply): `approved: true` must make the session
113
+ * resume producing events (`task.progress` continuing is the proof);
114
+ * `approved: false` means the caller will immediately follow up with
115
+ * `interrupt()` + `close()` and report `task.fail` — an adapter that has
116
+ * no notion of `needs_approval` at all (i.e. never emits one) should
117
+ * throw a descriptive error here rather than silently no-op, since a
118
+ * caller receiving `task.approve`/`task.reject` for one of its tasks
119
+ * implies something upstream expected approval support that isn't there.
120
+ */
121
+ resolveApproval(approved: boolean, reason?: string): Promise<void>;
122
+ }
123
+ /**
124
+ * Uniform seam every concrete runtime (pi now; claude/codex in M2) implements.
125
+ *
126
+ * Credential-isolation rule: an adapter spawns only the runtime's official
127
+ * binary. It never reads, proxies, or forwards that runtime's own credential
128
+ * storage (OAuth tokens, API keys on disk, `~/.claude`, `~/.codex`, `~/.pi`
129
+ * auth state, etc). Presence checks are limited to environment variable
130
+ * *names* (see {@link RuntimeDetectResult.authPresent}).
131
+ *
132
+ * M5: separately, {@link RuntimeAdapter.environmentRequirements} below
133
+ * declares which environment variable NAMES (never values inspected here
134
+ * either) this adapter's runtime needs forwarded into its own spawned
135
+ * process — see that method's own doc comment and `daemon/environment.ts`.
136
+ */
137
+ export interface RuntimeAdapter {
138
+ id: string;
139
+ detect(): Promise<RuntimeDetectResult>;
140
+ capabilities(): RuntimeCapabilities;
141
+ start(task: TaskOfferPayload, ctx: TaskContext): Promise<Session>;
142
+ /**
143
+ * M5: declares which environment variable names (exact, or `*`-suffixed
144
+ * prefix) this runtime's own CLI needs beyond the always-included
145
+ * platform baseline (`daemon/environment.ts`'s `buildRuntimeEnv`) —
146
+ * `task-runner.ts` builds each task's `TaskContext.env` from this instead
147
+ * of ever handing a spawned agent the daemon's raw `process.env` again.
148
+ * Optional and fail-closed by omission: an adapter that doesn't implement
149
+ * this gets the platform baseline ONLY, never an implicit "everything."
150
+ */
151
+ environmentRequirements?(): RuntimeEnvironmentRequirements;
152
+ }
153
+ /**
154
+ * Thrown by `RuntimeAdapter.start` when the task can never succeed on this
155
+ * adapter as offered — an unsupported `PermissionPolicy` (fail-closed) or an
156
+ * instruction shape the adapter can't handle (e.g. a blob-ref in M0) — as
157
+ * opposed to a transient/environmental failure (spawn error, missing
158
+ * credentials) that might succeed on a later retry. The daemon uses this
159
+ * distinction to set `task.fail`'s `retryable` flag correctly instead of
160
+ * treating every `start()` failure the same way.
161
+ */
162
+ export declare class PolicyUnsupportedError extends Error {
163
+ constructor(message: string);
164
+ }
165
+ /**
166
+ * Thrown by {@link Session.steer} on an adapter whose runtime has no
167
+ * mid-turn steering channel at all (`capabilities().steer === false`) — a
168
+ * permanent property of the runtime, never a transient failure. Typed
169
+ * rather than a bare `Error` so the daemon can classify an inbound
170
+ * `task.steer` for such a runtime as non-retryable (record + ack, cursor
171
+ * advances) instead of stalling the cursor on it forever, without matching
172
+ * on message strings.
173
+ */
174
+ export declare class SteerUnsupportedError extends Error {
175
+ /** The `RuntimeAdapter.id` that cannot steer (e.g. `claude`, `codex`). */
176
+ readonly runtimeId: string;
177
+ constructor(runtimeId: string, message: string);
178
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * A minimal push/pull async queue: producers call `push`/`end`, consumers
3
+ * `for await` over the queue itself. Shared by the pi RPC client's event
4
+ * stream, the claude/codex adapters' own event streams, and test fixtures
5
+ * (stub sessions) so all of them get the same order-preserving semantics
6
+ * without duplicating it.
7
+ *
8
+ * M4 hardening: this used to be genuinely back-pressure-free — an unbounded
9
+ * internal `buffered` array, so a producer that outpaces its consumer (a
10
+ * runaway/misbehaving runtime adapter, or simply nobody ever reading
11
+ * `session.events`) could grow it without limit. Bounded now: `capacity`
12
+ * (default {@link DEFAULT_ASYNC_QUEUE_CAPACITY}) caps how many items may sit
13
+ * in `buffered` unconsumed. On overflow this fails fast rather than either of
14
+ * the two worse alternatives — silently dropping the newest/oldest event (a
15
+ * consumer would never know its view of the stream has a hole in it) or
16
+ * blocking `push()` (every producer in this codebase calls `push()`
17
+ * synchronously from an I/O callback; blocking there would stall reading the
18
+ * underlying process/socket entirely). Instead, once `capacity` is exceeded,
19
+ * the queue transitions into a permanent error state: every pending and
20
+ * future consumer `next()` call rejects with an {@link AsyncQueueOverflowError}
21
+ * naming the capacity, and all further `push()` calls are silently ignored
22
+ * (mirrors the pre-existing `ended` no-op precedent — once terminal, always
23
+ * terminal). The error state is checked before `ended`, so it wins even if
24
+ * `end()` is called afterward — a caller must learn the stream broke, not
25
+ * see a quiet, ordinary completion.
26
+ */
27
+ export declare class AsyncQueueOverflowError extends Error {
28
+ readonly capacity: number;
29
+ constructor(capacity: number);
30
+ }
31
+ /** Default `capacity` (see {@link AsyncQueue}'s own doc comment) when the constructor isn't given one — generous for every producer in this codebase (adapter event streams a consumer reads turn-by-turn), while still catching a genuinely runaway/unconsumed stream instead of growing forever. */
32
+ export declare const DEFAULT_ASYNC_QUEUE_CAPACITY = 10000;
33
+ export declare class AsyncQueue<T> implements AsyncIterable<T> {
34
+ private readonly capacity;
35
+ private readonly buffered;
36
+ private readonly waiters;
37
+ private ended;
38
+ private failure;
39
+ constructor(capacity?: number);
40
+ push(item: T): void;
41
+ end(): void;
42
+ /** Transitions the queue into its permanent overflow state: rejects every currently-pending waiter and, from then on, every `next()` call (checked ahead of `ended` — see the class doc comment on why the error must win). */
43
+ private fail;
44
+ [Symbol.asyncIterator](): AsyncIterator<T>;
45
+ }
@@ -0,0 +1,86 @@
1
+ /** Options for {@link atomicWriteFile}. */
2
+ export interface AtomicWriteOptions {
3
+ /**
4
+ * POSIX file mode (e.g. `0o600`) to apply to the written file. Applied to
5
+ * the temp file at creation (via `fs.open`'s own `mode` argument, which
6
+ * *is* honored there since the temp file is always freshly created — see
7
+ * point 3 below) and re-asserted with an explicit `chmod` both right
8
+ * after and again once more after the rename. Omit to leave the file at
9
+ * the platform's default create mode.
10
+ */
11
+ mode?: number;
12
+ /**
13
+ * fsync(2) the temp file's contents before renaming it onto the target,
14
+ * then fsync the published target and, on POSIX, its parent directory after
15
+ * the rename. Windows' `FlushFileBuffers` requires a writable file handle
16
+ * and has no directory-flush surface through Node/libuv, so that platform
17
+ * flushes the target through `r+` and relies on the atomic rename for the
18
+ * directory entry. Off by default: this is the strongest platform-native
19
+ * durability form needed by state whose directory entry itself is
20
+ * load-bearing (for example the operational-health run marker). It is not
21
+ * needed for the core no-torn-read guarantee, which comes from rename.
22
+ */
23
+ fsync?: boolean;
24
+ }
25
+ /**
26
+ * Write `data` to `filePath` such that:
27
+ *
28
+ * 1. **No torn reads.** A concurrent reader opening `filePath` at any point
29
+ * during the write either sees the file's prior complete contents or the
30
+ * new complete contents — never a truncated/partial write. A bare
31
+ * `fs.writeFile` does NOT give you this: it truncates the target in
32
+ * place before writing the new bytes, so a reader racing it can observe
33
+ * (or fail to `JSON.parse`) a half-written file. This helper instead
34
+ * writes to a private temp file in the same directory as `filePath`
35
+ * (same directory is required for the rename below to be atomic — it
36
+ * must stay on one filesystem) and `fs.rename`s it onto the target;
37
+ * POSIX guarantees that rename-onto-an-existing-path is an atomic
38
+ * directory-entry swap.
39
+ * 2. **No corruption on crash.** If the process dies between the write and
40
+ * the rename, `filePath` is untouched — the half-written data only ever
41
+ * existed at the temp path. This does not, by itself, order two
42
+ * concurrent writers deterministically — the last rename to land wins,
43
+ * silently, with no lost-update detection. Callers that need that on
44
+ * top of atomicity (e.g. an in-memory map persisted across many
45
+ * concurrent callers) still need their own serialization, the way
46
+ * `session-workspace-store.ts`'s own per-instance queue does.
47
+ * 3. **Mode preserved across a replace.** POSIX rename replaces the target
48
+ * path's directory entry wholesale — the resulting file's mode is the
49
+ * temp file's mode, not whatever the previously-existing target's mode
50
+ * was. Passing `{ mode }` sets that on the temp file before the rename
51
+ * so callers like `DeviceStore` (0600, holds a private key) get a
52
+ * guaranteed mode on every write, not just the one that first creates
53
+ * the file.
54
+ * 4. **Works on win32 — without ever risking the live target.** POSIX
55
+ * `rename(2)` atomically replaces an existing `filePath` in one step.
56
+ * Windows' rename (`MoveFileEx` under libuv) can refuse to replace an
57
+ * existing target — most commonly `EPERM`, and on some Node/libuv
58
+ * versions `EEXIST` — when something transient (an antivirus scanner or
59
+ * search indexer briefly holding a read handle open on the destination
60
+ * is the usual culprit) is touching it. An earlier version of this
61
+ * helper "fixed" that by `unlink`-ing the stale target and retrying the
62
+ * rename once — which is exactly backwards for a helper whose whole job
63
+ * is protecting `filePath` from ever being left corrupt or missing: if
64
+ * the retried rename ALSO failed (the lock wasn't transient after all),
65
+ * the catch block deleted the temp file too, leaving `filePath` gone
66
+ * entirely — worse than the torn write this helper exists to prevent,
67
+ * and fatal for a caller like `DeviceStore` (the device's JWT + private
68
+ * key live at that path with no other copy). Fixed: `renameOnto` below
69
+ * NEVER unlinks `targetPath`. On `EPERM`/`EEXIST` it retries the plain
70
+ * rename with a few short, bounded backoff waits (the lock is normally
71
+ * gone within milliseconds); if every attempt still fails, it throws
72
+ * with `filePath` left completely untouched — the caller keeps the
73
+ * last-good file on disk instead of losing it, at the cost of that one
74
+ * write not landing. Still strictly better than the plain
75
+ * `fs.writeFile` this replaces, which truncates the live file in place
76
+ * unconditionally (so a crash or a racing reader can see a torn file on
77
+ * every platform, every time) rather than only failing-safe on one
78
+ * platform under a transient lock.
79
+ *
80
+ * The caller is responsible for ensuring the parent directory of `filePath`
81
+ * already exists (e.g. via `fs.mkdir(dir, { recursive: true })`) — this
82
+ * helper does not create it, mirroring `DeviceStore`/`CursorStore`/
83
+ * `SessionWorkspaceStore`, which all do that themselves with their own
84
+ * directory mode.
85
+ */
86
+ export declare function atomicWriteFile(filePath: string, data: string | Uint8Array, options?: AtomicWriteOptions): Promise<void>;
@@ -0,0 +1,124 @@
1
+ import { type Runner } from '../lifecycle/exec-runner';
2
+ /**
3
+ * Pure command-construction seam (finding F7) — kept separate from the
4
+ * actual `icacls` invocation below so it's unit-testable on ANY host OS,
5
+ * not just win32 (this whole SDK is developed on darwin/linux — see
6
+ * `templates/service/winsw/smoke-test.mjs`'s own header comment on the
7
+ * identical constraint for WinSW itself).
8
+ *
9
+ * Removes inherited ACEs (`/inheritance:r`) and grants FULL CONTROL,
10
+ * recursively (`(OI)(CI)F` — Object Inherit, Container Inherit, Full
11
+ * control — so anything created under `dir` afterward inherits the SAME
12
+ * restriction without needing to be re-ACL'd individually) to exactly
13
+ * three principals:
14
+ *
15
+ * - the current user, via `os.userInfo().username` — deliberately NOT the
16
+ * `%USERNAME%` environment variable a hand-typed reference command might
17
+ * use: an env var can be stale, unset, or (in an unusual but real
18
+ * embedding) spoofed by whatever set up this process's environment;
19
+ * querying the OS directly cannot be. This one genuinely has to be a
20
+ * NAME (icacls has no "current user" SID shorthand), but it's the
21
+ * account's real name, not a translated built-in label.
22
+ * - `SYSTEM` and `Administrators` — both needed for a Windows-SERVICE
23
+ * topology, where the daemon runs as `SYSTEM` (a WinSW-installed
24
+ * service's default account) while an operator's interactive CLI
25
+ * invocation runs as a normal user against the SAME `storeDir` — see
26
+ * `control-protocol.ts`'s `controlPipeName` doc comment for the
27
+ * identical service-account rationale on the pipe-naming side. Finding
28
+ * R4: referenced by their WELL-KNOWN SIDs ({@link SYSTEM_SID} /
29
+ * {@link ADMINISTRATORS_SID}), not the display names `SYSTEM`/
30
+ * `Administrators` — those two names are LOCALIZED (e.g. a
31
+ * French-language Windows renders the Administrators group as
32
+ * "Administrateurs"), so `icacls ... /grant Administrators:...` would
33
+ * silently fail to resolve (and thus fail the whole hardening step) on
34
+ * any non-English install. The SIDs themselves are invariant across
35
+ * every locale/edition; `icacls` accepts SID form directly when prefixed
36
+ * with `*` (its own documented syntax).
37
+ *
38
+ * Returns a plain ARGV array with NO manually-embedded quote characters —
39
+ * this is meant for `child_process.execFile`'s array form (this codebase's
40
+ * own established convention for every external command it ever runs; see
41
+ * `lifecycle/exec-runner.ts`'s `defaultRunner`), which is never a shell and
42
+ * so has no shell-quoting hazard for a `dir`/username containing spaces —
43
+ * Node's own Windows argv encoding (used internally by `execFile`/`spawn`
44
+ * when given an array) already quotes/escapes each element correctly
45
+ * regardless of embedded whitespace. Hand-rolling literal `"` characters
46
+ * into an array element bound for that API would risk DOUBLE-quoting
47
+ * instead of fixing anything — the icacls reference command sometimes
48
+ * quoted as `"%USERNAME%":(OI)(CI)F` is a shell/cmd.exe-level concern that
49
+ * simply does not apply once argv is passed as an array with no shell
50
+ * involved.
51
+ */
52
+ export declare function buildIcaclsArgs(dir: string, username: string): string[];
53
+ /** Restrictive ACL for one already-created file; no inheritance flags because the target cannot contain children. */
54
+ export declare function buildIcaclsFileArgs(filePath: string, username: string): string[];
55
+ export interface EnsureSecureDirOptions {
56
+ /** DI for tests — see `lifecycle/exec-runner.ts`'s identical `Runner` seam. Defaults to `defaultRunner` (real `execFile`, never a shell). */
57
+ run?: Runner;
58
+ /** DI for tests — lets the win32 branch below be exercised (with a fake `run`) from any host OS, mirroring `control-protocol.ts`'s `controlEndpointPath` platform-override convention. Defaults to `process.platform`. */
59
+ platform?: NodeJS.Platform;
60
+ }
61
+ /**
62
+ * Finding R4 (cross-model re-review — F7 residual): thrown by
63
+ * {@link ensureSecureDir} on win32 when `icacls` either could not be run at
64
+ * all (e.g. missing binary, or a restricted service account lacking
65
+ * permission to spawn it) or ran and exited non-zero (e.g. it couldn't
66
+ * resolve a principal, or was itself denied). This directory's contents —
67
+ * `device.json` (an Ed25519 private key + access token) or `control.token`
68
+ * (the control socket's HMAC secret) — would otherwise be protected by
69
+ * nothing but the OS's own default ACL, typically readable by any local
70
+ * user; see `docs/security.md`'s own note on why this is now fail-closed
71
+ * rather than a logged-and-ignored warning. See {@link ensureSecureDir}'s
72
+ * own doc comment for how each caller (`DeviceStore.save`,
73
+ * `control-server.ts`'s `startControlServer`) reacts to this.
74
+ */
75
+ export declare class SecureDirHardeningError extends Error {
76
+ readonly dir: string;
77
+ constructor(dir: string, reason: string);
78
+ }
79
+ export declare class SecureFileHardeningError extends Error {
80
+ readonly filePath: string;
81
+ constructor(filePath: string, reason: string);
82
+ }
83
+ /**
84
+ * Creates (if needed) and secures `dir`: POSIX `{mode: 0o700}` plus a
85
+ * best-effort `chmod` re-assertion on every platform (unchanged from
86
+ * before this fix — this is what actually restricts access on
87
+ * darwin/linux), PLUS — win32 only — a restrictive DACL via `icacls` (see
88
+ * `buildIcaclsArgs`'s own doc comment for exactly what it grants/removes).
89
+ *
90
+ * Finding R4 (cross-model re-review): the win32 `icacls` step is now
91
+ * FAIL-CLOSED — it used to be best-effort (logged via `console.warn`,
92
+ * never thrown), which meant a host where `icacls` genuinely can't run
93
+ * (missing binary, a locked-down service account) would silently create
94
+ * `storeDir` with NO Windows-side ACL protection at all and carry on as if
95
+ * nothing were wrong. Now it THROWS {@link SecureDirHardeningError}
96
+ * instead, on both failure shapes (the spawn itself failing, or `icacls`
97
+ * running and exiting non-zero). Each real caller already has (or gets,
98
+ * via this fix) an appropriate reaction:
99
+ *
100
+ * - `control-server.ts`'s `startControlServer` calls this as the very
101
+ * FIRST thing, before any socket/pipe exists — a thrown error here
102
+ * propagates out of `startControlServer` with nothing to clean up (no
103
+ * F9-style orphan-listener risk), straight into `create-daemon.ts`'s
104
+ * `start()`'s EXISTING "any non-`AnotherControlServerRunningError` bind
105
+ * failure degrades non-fatally" catch block: logs a loud
106
+ * `console.warn` naming the reason (this error's own message) and
107
+ * continues the rest of the daemon WITHOUT a control socket — the
108
+ * correct "graceful path" for a control-IPC-layer failure, unchanged
109
+ * code, already exactly right once this function starts throwing.
110
+ * - `DeviceStore.save()` calls this before ever writing `device.json` —
111
+ * a thrown error here propagates directly out of `AuthManager.pair()`
112
+ * (called during `pair()`, before any credential is persisted) as a
113
+ * clear, typed, actionable rejection — pairing simply fails rather than
114
+ * silently leaving an unprotected device keypair/access token on disk.
115
+ *
116
+ * Non-win32 (darwin/linux) behavior is completely unchanged — the POSIX
117
+ * `mkdir`/`chmod` calls above are the only enforcement there, and never
118
+ * throw on their own best-effort `chmod` failure (that one stays
119
+ * genuinely benign — an `EPERM` against a directory this process doesn't
120
+ * own — see the inline comment on it).
121
+ */
122
+ export declare function ensureSecureDir(dir: string, opts?: EnsureSecureDirOptions): Promise<void>;
123
+ /** Re-asserts 0600 on POSIX and a non-inherited owner/SYSTEM/Admin DACL on Windows. */
124
+ export declare function ensureSecureFile(filePath: string, opts?: EnsureSecureDirOptions): Promise<void>;
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@byok-sdk/client",
3
+ "version": "0.1.0",
4
+ "description": "BYOK SDK client daemon: runs on the end user's machine, pairs with a SaaS server, and drives a local coding-agent runtime",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Ancienttwo/byok-sdk.git",
10
+ "directory": "packages/client"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/Ancienttwo/byok-sdk/issues"
14
+ },
15
+ "homepage": "https://github.com/Ancienttwo/byok-sdk#readme",
16
+ "engines": {
17
+ "node": ">=20"
18
+ },
19
+ "sideEffects": false,
20
+ "main": "./dist/index.js",
21
+ "module": "./dist/index.js",
22
+ "types": "./dist/index.d.ts",
23
+ "bin": {
24
+ "byok-agent": "./dist/bin/byok-agent.js",
25
+ "byok-approval-mcp": "./dist/bin/byok-approval-mcp.js"
26
+ },
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "import": "./dist/index.js"
31
+ },
32
+ "./package.json": "./package.json"
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "README.md",
37
+ "LICENSE"
38
+ ],
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "dependencies": {
43
+ "ws": "^8.21.1",
44
+ "@byok-sdk/core": "0.1.0",
45
+ "@byok-sdk/protocol": "0.1.0"
46
+ },
47
+ "optionalDependencies": {
48
+ "@earendil-works/pi-coding-agent": "^0.74.2"
49
+ },
50
+ "devDependencies": {
51
+ "@types/ws": "^8.18.1",
52
+ "@hono/node-server": "^2.0.10",
53
+ "@byok-sdk/server": "0.1.0"
54
+ },
55
+ "scripts": {
56
+ "build": "tsup && tsc -p tsconfig.build.json",
57
+ "dev": "tsup --watch",
58
+ "test": "vitest run",
59
+ "test:watch": "vitest",
60
+ "typecheck": "tsc --noEmit",
61
+ "smoke:adapters": "node scripts/adapter-task-smoke.mjs",
62
+ "audit:adapter-task-smoke": "node scripts/adapter-task-smoke.mjs",
63
+ "audit:credentials": "node scripts/linux-credential-audit.mjs",
64
+ "clean": "rm -rf dist"
65
+ }
66
+ }