@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.
- package/LICENSE +21 -0
- package/README.md +11 -0
- package/dist/adapters/claude/claude-adapter.d.ts +155 -0
- package/dist/adapters/claude/events.d.ts +91 -0
- package/dist/adapters/claude/permission-mapping.d.ts +142 -0
- package/dist/adapters/claude/process-client.d.ts +103 -0
- package/dist/adapters/claude/resolve-approval-mcp-bin.d.ts +45 -0
- package/dist/adapters/claude/resolve-bin.d.ts +27 -0
- package/dist/adapters/codex/codex-adapter.d.ts +97 -0
- package/dist/adapters/codex/events.d.ts +41 -0
- package/dist/adapters/codex/permission-mapping.d.ts +107 -0
- package/dist/adapters/codex/process-runner.d.ts +86 -0
- package/dist/adapters/codex/resolve-bin.d.ts +25 -0
- package/dist/adapters/pi/events.d.ts +58 -0
- package/dist/adapters/pi/permission-mapping.d.ts +46 -0
- package/dist/adapters/pi/pi-adapter.d.ts +29 -0
- package/dist/adapters/pi/resolve-bin.d.ts +44 -0
- package/dist/adapters/pi/rpc-client.d.ts +101 -0
- package/dist/bin/approval-mcp-server.d.ts +69 -0
- package/dist/bin/audit-log.d.ts +212 -0
- package/dist/bin/byok-agent.d.ts +2 -0
- package/dist/bin/byok-agent.js +12347 -0
- package/dist/bin/byok-agent.js.map +1 -0
- package/dist/bin/byok-approval-mcp.d.ts +2 -0
- package/dist/bin/byok-approval-mcp.js +498 -0
- package/dist/bin/byok-approval-mcp.js.map +1 -0
- package/dist/bin/commands/approvals.d.ts +31 -0
- package/dist/bin/commands/approve-reject.d.ts +29 -0
- package/dist/bin/commands/doctor.d.ts +15 -0
- package/dist/bin/commands/pair.d.ts +7 -0
- package/dist/bin/commands/runtimes.d.ts +7 -0
- package/dist/bin/commands/service.d.ts +54 -0
- package/dist/bin/commands/start.d.ts +21 -0
- package/dist/bin/commands/status.d.ts +25 -0
- package/dist/bin/commands/support-bundle.d.ts +7 -0
- package/dist/bin/commands/tasks.d.ts +27 -0
- package/dist/bin/commands/unpair.d.ts +124 -0
- package/dist/bin/commands/workspaces.d.ts +16 -0
- package/dist/bin/config.d.ts +41 -0
- package/dist/bin/control-client.d.ts +50 -0
- package/dist/bin/format.d.ts +67 -0
- package/dist/bin/runtime-probe.d.ts +34 -0
- package/dist/bin/tasks-view.d.ts +49 -0
- package/dist/daemon/approvals.d.ts +87 -0
- package/dist/daemon/auth-manager.d.ts +52 -0
- package/dist/daemon/blob-client.d.ts +22 -0
- package/dist/daemon/connection-manager.d.ts +502 -0
- package/dist/daemon/control-protocol.d.ts +299 -0
- package/dist/daemon/control-server.d.ts +61 -0
- package/dist/daemon/create-daemon.d.ts +364 -0
- package/dist/daemon/cursor-store.d.ts +30 -0
- package/dist/daemon/daemon-owner.d.ts +24 -0
- package/dist/daemon/deterministic-jitter.d.ts +20 -0
- package/dist/daemon/device-keys.d.ts +45 -0
- package/dist/daemon/device-proof-signer.d.ts +41 -0
- package/dist/daemon/environment.d.ts +96 -0
- package/dist/daemon/git-workspace-store.d.ts +49 -0
- package/dist/daemon/git-workspace.d.ts +80 -0
- package/dist/daemon/http-client.d.ts +8 -0
- package/dist/daemon/journal/journal.d.ts +342 -0
- package/dist/daemon/journal/sqlite-journal.d.ts +173 -0
- package/dist/daemon/journal/sqlite-support.d.ts +96 -0
- package/dist/daemon/journal/storage-policy.d.ts +368 -0
- package/dist/daemon/long-poll-transport.d.ts +142 -0
- package/dist/daemon/observer.d.ts +345 -0
- package/dist/daemon/operational-health.d.ts +87 -0
- package/dist/daemon/policy.d.ts +16 -0
- package/dist/daemon/progress-batcher.d.ts +31 -0
- package/dist/daemon/runtime-capabilities.d.ts +36 -0
- package/dist/daemon/session-workspace-store.d.ts +75 -0
- package/dist/daemon/store.d.ts +54 -0
- package/dist/daemon/task-runner.d.ts +862 -0
- package/dist/daemon/truth-memory-client.d.ts +94 -0
- package/dist/daemon/url.d.ts +60 -0
- package/dist/daemon/ws-transport.d.ts +110 -0
- package/dist/diagnostics/diagnostics.d.ts +114 -0
- package/dist/diagnostics/support-bundle.d.ts +81 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +10056 -0
- package/dist/index.js.map +1 -0
- package/dist/lifecycle/create-service-lifecycle.d.ts +59 -0
- package/dist/lifecycle/exec-runner.d.ts +141 -0
- package/dist/lifecycle/launchd.d.ts +55 -0
- package/dist/lifecycle/service-types.d.ts +177 -0
- package/dist/lifecycle/systemd.d.ts +46 -0
- package/dist/lifecycle/winsw.d.ts +64 -0
- package/dist/types.d.ts +178 -0
- package/dist/util/async-queue.d.ts +45 -0
- package/dist/util/atomic-write.d.ts +86 -0
- package/dist/util/secure-dir.d.ts +124 -0
- package/package.json +66 -0
package/dist/types.d.ts
ADDED
|
@@ -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
|
+
}
|