@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ancienttwo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,11 @@
1
+ # @byok-sdk/client
2
+
3
+ The local BYOK daemon. It pairs a device, durably journals tasks, connects over
4
+ WebSocket or long poll, dispatches to local Claude Code, Codex, or pi adapters,
5
+ and exposes authenticated local diagnostics/control commands.
6
+
7
+ The package installs `byok-agent` and `byok-approval-mcp` binaries. Provider
8
+ credentials are not read by the dispatch plane; `@byok-sdk/keys` is separate.
9
+
10
+ MIT licensed. Node.js 20 or newer; SQLite-backed journaling requires a runtime
11
+ with `node:sqlite` support.
@@ -0,0 +1,155 @@
1
+ import type { TaskOfferPayload } from '@byok-sdk/protocol';
2
+ import { type RuntimeAdapter, type RuntimeCapabilities, type RuntimeDetectResult, type RuntimeEnvironmentRequirements, type Session, type TaskContext } from '../../types';
3
+ import { type ResolvedBin } from './resolve-bin';
4
+ import { type ResolvedApprovalMcpBin } from './resolve-approval-mcp-bin';
5
+ import { type SpawnFn } from './process-client';
6
+ export interface ClaudeAdapterOptions {
7
+ /** Override bin resolution — tests substitute the fake-claude fixture script. */
8
+ resolveBin?: () => ResolvedBin;
9
+ /** Override process spawning — tests substitute a fake spawn. */
10
+ spawnFn?: SpawnFn;
11
+ /** M4 Phase 3: override `byok-approval-mcp` bin resolution — tests substitute a fixture script instead of computing a real dist path. Mirrors `resolveBin` above. */
12
+ resolveApprovalMcpBin?: () => ResolvedApprovalMcpBin;
13
+ }
14
+ /**
15
+ * Claude Code runtime adapter (`claude -p --input-format stream-json
16
+ * --output-format stream-json`) — the M2-a counterpart to `../pi/pi-adapter.ts`.
17
+ * Every behavioral claim in this file's own doc comments and its sibling
18
+ * modules (`events.ts`, `permission-mapping.ts`, `process-client.ts`) was
19
+ * empirically reproduced against the real installed `claude` 2.1.212 binary
20
+ * on a logged-in machine (per this task's own "do NOT trust docs over the
21
+ * real binary" mandate — `claude --help` was actively wrong/misleading for
22
+ * `--allowedTools`, see `permission-mapping.ts`) — not inferred from
23
+ * training-data recall or the Claude API/Agent-SDK docs, which describe a
24
+ * DIFFERENT product surface (the Messages API, not this CLI's headless
25
+ * wire format).
26
+ *
27
+ * ## The central finding: claude's headless approval model has no
28
+ * `needs_approval` pause, at all
29
+ *
30
+ * This is the first real use of the `needs_approval` /
31
+ * `Session.resolveApproval` seam any adapter in this codebase has
32
+ * implemented (pi never emits `needs_approval` — see `PiSession
33
+ * .resolveApproval`'s own doc comment) — so this finding directly informs
34
+ * the M2-c protocol-freeze decision on that seam.
35
+ *
36
+ * Empirically (see the M2-a report for the full live-capture evidence):
37
+ * spawning `claude -p` **non-interactively** with a tool call that would
38
+ * normally prompt a human is resolved **synchronously, before the turn
39
+ * continues** — there is no pause, no wait, no later resumption point:
40
+ *
41
+ * - Under `--permission-mode default` (or no flag at all — headless has no
42
+ * TTY to interactively ask), an unapproved tool call is immediately
43
+ * AUTO-DENIED with a synthesized `tool_result`
44
+ * (`"Claude requested permissions to write to <path>, but you haven't
45
+ * granted it yet."`, `is_error:true`) and the run continues normally to
46
+ * its own `result` frame — no hang, and nothing this adapter could ever
47
+ * resume later even if it wanted to.
48
+ * - Under a permissive `--permission-mode` (`acceptEdits`/`bypassPermissions`),
49
+ * the call is auto-GRANTED, again synchronously, again with nothing to
50
+ * pause on.
51
+ *
52
+ * There is consequently no claude stream-json frame this adapter could
53
+ * ever map to the protocol's `needs_approval` `AgentEvent` — the decision
54
+ * is always already made by the time any frame reaches this adapter at
55
+ * all. `resolveApproval()` below throws a descriptive error rather than
56
+ * silently no-op'ing, mirroring `PiSession.resolveApproval`'s own
57
+ * documented reasoning exactly: a caller that ever receives
58
+ * `task.approve`/`task.reject` for one of this adapter's tasks implies
59
+ * something upstream expected approval support this adapter genuinely does
60
+ * not have.
61
+ *
62
+ * `PermissionPolicy.mode: 'confirm'` — the policy mode whose whole point is
63
+ * "ask a human, then proceed" — was therefore rejected outright at
64
+ * `start()` through M2/M3 (fail-closed, see `permission-mapping.ts`), never
65
+ * silently downgraded to auto-accept or auto-deny.
66
+ *
67
+ * ## M4 Phase 3 update: a genuine out-of-band pause DOES exist — it is
68
+ * just invisible to everything written above
69
+ *
70
+ * `--permission-prompt-tool` (a DIFFERENT flag from `--permission-mode`,
71
+ * undocumented in `claude --help`'s own output on the installed 2.1.216
72
+ * binary but empirically confirmed accepted — an unrecognized flag is
73
+ * rejected outright with `error: unknown option`, this one is not) makes
74
+ * claude block a turn on a real MCP round-trip to a server it spawns
75
+ * itself, waiting for that server to answer allow/deny before continuing —
76
+ * genuinely pausing, for real wall-clock time (live-verified: an instant
77
+ * allow/deny, AND a deliberate multi-second delayed answer, both worked
78
+ * identically; only a permission-prompt-tool call that never answers AT
79
+ * ALL was found to make claude abandon the turn on its own, after roughly
80
+ * 1.5s — never actually reachable by this design, since the bundled
81
+ * `bin/byok-approval-mcp.ts` always eventually answers within its own
82
+ * configured ceiling).
83
+ *
84
+ * Everything above this section remains true and is NOT superseded by
85
+ * this: claude's own stream-json output still emits nothing while this
86
+ * pause is in progress — the gap between a `tool_use` frame and its
87
+ * `tool_result` is indistinguishable from ordinary model latency on the
88
+ * wire, and there is still no `needs_approval`-shaped frame this adapter's
89
+ * event mapper could ever produce. The pause is real, but it is invisible
90
+ * to `ClaudeSession.events` and to `task-runner.ts`'s `pump()` entirely —
91
+ * it is only ever observable from OUTSIDE this adapter's own process, by
92
+ * the separate MCP-server child process claude itself spawns. This is why
93
+ * `confirm` mode's daemon-side wiring (`task-runner.ts`'s `requestApproval`,
94
+ * `types.ts`'s `ApprovalChannel`) is driven from the control socket, not
95
+ * from any `AgentEvent` — see those files' own doc comments for the full
96
+ * design this finding drove. `confirm` is now SUPPORTED (see
97
+ * `permission-mapping.ts` and `resolveApproval()` below), still fail-closed
98
+ * whenever no approval channel was actually wired up for this session.
99
+ *
100
+ * ## Steering was also found unsupported (a second, related finding)
101
+ *
102
+ * Live-probed via a persistent `--input-format stream-json` process:
103
+ * writing a second `{"type":"user",...}` message to stdin WHILE a turn is
104
+ * still generating does NOT redirect that in-flight turn — it QUEUES as a
105
+ * separate, subsequent turn, processed only after the first one reaches
106
+ * its own `result`. This is genuinely useful for `followUp()` (a new turn
107
+ * "after [the session] has gone idle" — exactly the queued-after-result
108
+ * case), but it is not what `Session.steer`'s "inject steering text into a
109
+ * running turn (mid-stream)" contract promises. `capabilities().steer` is
110
+ * therefore `false`, and `steer()` throws rather than silently behaving
111
+ * like a queued follow-up under a name that implies live redirection.
112
+ */
113
+ export declare class ClaudeAdapter implements RuntimeAdapter {
114
+ private readonly options;
115
+ readonly id = "claude";
116
+ constructor(options?: ClaudeAdapterOptions);
117
+ detect(): Promise<RuntimeDetectResult>;
118
+ capabilities(): RuntimeCapabilities;
119
+ /**
120
+ * M5: deliberate product-boundary decision, not an oversight — byok's
121
+ * current ToS posture for claude is login-state-only (`claude auth
122
+ * login`'s own OAuth session — see `probeAuthPresent` below), so this
123
+ * adapter declares NO credential env vars at all; env-based API-key
124
+ * passthrough for claude is a separate, still-pending product decision.
125
+ * A product that genuinely needs it can opt in locally per-device via
126
+ * `DaemonConfig.runtimeEnvironment.claude.allow` (`create-daemon.ts`).
127
+ * `baseNames` is empty too: nothing in this adapter reads a
128
+ * claude-specific config-discovery variable (e.g. `CLAUDE_CONFIG_DIR`)
129
+ * today — if a future version of this adapter starts reading one, it
130
+ * belongs here, not left to rely on the platform baseline alone.
131
+ */
132
+ environmentRequirements(): RuntimeEnvironmentRequirements;
133
+ start(task: TaskOfferPayload, ctx: TaskContext): Promise<Session>;
134
+ /**
135
+ * `claude auth status --json` is claude's OWN non-secret login-state
136
+ * signal (see the credential-isolation rule on `RuntimeAdapter` in
137
+ * `../../types.ts`) — empirically confirmed live on this logged-in
138
+ * machine to report `{"loggedIn":true,"authMethod":"claude.ai",
139
+ * "apiProvider":"firstParty","email":"...","orgId":"...","orgName":"...",
140
+ * "subscriptionType":"max"}`, with no token/key material anywhere in it.
141
+ * This spawns the binary and parses ONLY its own reported status — it
142
+ * never reads `~/.claude` or any credential file itself, matching pi's
143
+ * `authPresent` computation being limited to environment-variable
144
+ * *names* (`../pi/pi-adapter.ts`'s `KNOWN_PROVIDER_ENV_VARS`), just via
145
+ * claude's own equivalent non-secret probe instead (claude's auth is
146
+ * OAuth-session-based via `claude auth login`, not primarily an env var,
147
+ * so pi's env-var-presence approach doesn't apply here the same way).
148
+ * A failed/unparseable probe (binary present but not logged in, a future
149
+ * claude release changing this output shape, etc.) fails closed to
150
+ * `false` — this never affects `present`, which is solely about whether
151
+ * `--version` itself succeeded.
152
+ */
153
+ private probeAuthPresent;
154
+ private resolveBin;
155
+ }
@@ -0,0 +1,91 @@
1
+ import type { AgentEvent } from '@byok-sdk/protocol';
2
+ /**
3
+ * A raw parsed line from `claude --output-format stream-json`. Shapes vary
4
+ * a lot by `type` (and, for `system`, by `subtype`) — see the doc comments
5
+ * on the individual mapping functions below for the concrete shapes this
6
+ * was empirically captured against. Kept as a loose bag rather than a full
7
+ * discriminated union for the same reason pi's `PiRpcMessage` is: this
8
+ * module only needs a handful of fields off of each frame.
9
+ */
10
+ export interface ClaudeStreamMessage {
11
+ type: string;
12
+ [key: string]: unknown;
13
+ }
14
+ /**
15
+ * Cross-message correlation state a single {@link ClaudeSession} (in
16
+ * `../claude-adapter.ts`) owns for its whole lifetime.
17
+ *
18
+ * Unlike pi's `tool_execution_start`/`tool_execution_end` frames (which
19
+ * both carry `toolName` directly, so `../pi/events.ts` can stay a pure,
20
+ * stateless function), claude's Messages-API-shaped transcript splits a
21
+ * tool call across two DIFFERENT frame types: the `assistant` frame's
22
+ * `tool_use` content block carries `{id, name, input}`, but the later
23
+ * `user` frame's `tool_result` content block carries only `{tool_use_id,
24
+ * content, is_error}` — no tool name at all. Since this protocol's own
25
+ * `AgentEvent` schema requires `tool_result.tool: string`, this mapper has
26
+ * no choice but to remember `tool_use_id -> name` from the `tool_use` block
27
+ * and look it up when the matching `tool_result` arrives later. This is a
28
+ * genuine, disclosed structural difference from pi, not an arbitrary
29
+ * design choice — see the M2-a report for the full reasoning.
30
+ */
31
+ export interface ToolUseCorrelation {
32
+ readonly toolNameByUseId: Map<string, string>;
33
+ }
34
+ export declare function createToolUseCorrelation(): ToolUseCorrelation;
35
+ export interface MapClaudeMessageOptions {
36
+ /** `ctx.workspaceDir` for the task — used only to compute a workspace-relative `name` for a possible `artifact` AgentEvent (see `tryBuildArtifactEvent`'s doc comment). */
37
+ workspaceDir: string;
38
+ }
39
+ export interface MapClaudeMessageResult {
40
+ events: AgentEvent[];
41
+ /**
42
+ * Set when this exact frame (or, for `assistant`/`user` frames, one
43
+ * content block inside it) was genuinely unrecognized — a frame/subtype/
44
+ * block-type this adapter has never been told to expect — as opposed to
45
+ * routine bookkeeping this mapper deliberately ignores (see
46
+ * `ROUTINE_CLAUDE_SYSTEM_SUBTYPES` and the `thinking`/`redacted_thinking`
47
+ * cases below). The caller (`ClaudeSession`'s event iterator) is
48
+ * responsible for actually recording it (via
49
+ * `ClaudeProcessClient.recordUnmappedFrame`) — mirrors pi's
50
+ * `ROUTINE_PI_EVENT_TYPES` check living in `PiSession`'s iterator rather
51
+ * than inside `mapPiMessageToAgentEvent` itself. At most one label per
52
+ * call even if a frame has several unmapped things in it — sufficient
53
+ * for the "did this regress" self-diagnosing purpose this exists for,
54
+ * without needing a list.
55
+ */
56
+ unmappedLabel?: string;
57
+ }
58
+ /**
59
+ * `system` frame subtypes empirically observed on real stream-json output
60
+ * from the installed claude 2.1.212 binary that carry no `AgentEvent`
61
+ * equivalent — routine bookkeeping, deliberately ignored:
62
+ *
63
+ * - `init`: session/turn start. Carries `session_id`, `tools`, `cwd`,
64
+ * `permissionMode`, etc. `session_id` specifically is NOT read here —
65
+ * `ClaudeProcessClient.waitForInit()` (`process-client.ts`) captures it
66
+ * directly off the raw line as part of this adapter's own
67
+ * `start()`/sessionRef bookkeeping, since it's needed before any
68
+ * `AgentEvent` mapping is even relevant.
69
+ * - `hook_started` / `hook_response`: fired when the user's own Claude Code
70
+ * installation has configured lifecycle hooks (e.g. `SessionStart`) —
71
+ * machine/config-specific, not part of this protocol's surface at all.
72
+ * - `thinking_tokens`: periodic token-count-estimate bookkeeping emitted
73
+ * while the model is reasoning; no user-visible content.
74
+ *
75
+ * A `system` frame whose `subtype` is NOT in this set is treated as
76
+ * genuinely unmapped (see `mapClaudeMessageToAgentEvents`'s `system` case)
77
+ * rather than silently folded into "system frames are always routine" —
78
+ * this is deliberately finer-grained than lumping the whole `system` type
79
+ * together, so a future/unobserved subtype (e.g. something compaction- or
80
+ * budget-related) shows up as a one-time warning instead of disappearing
81
+ * the way the pi adapter's own root-cause hang (a real settle event with no
82
+ * mapping, silently swallowed) did before that bug was found.
83
+ */
84
+ export declare const ROUTINE_CLAUDE_SYSTEM_SUBTYPES: ReadonlySet<string>;
85
+ /**
86
+ * Map one raw claude stream-json line to zero or more normalized
87
+ * `AgentEvent`s (a single `user` frame can produce two: `tool_result` plus
88
+ * a derived `artifact`). See the per-`type` mapping functions above for the
89
+ * concrete, empirically-captured shapes each branch handles.
90
+ */
91
+ export declare function mapClaudeMessageToAgentEvents(msg: ClaudeStreamMessage, correlation: ToolUseCorrelation, options: MapClaudeMessageOptions): MapClaudeMessageResult;
@@ -0,0 +1,142 @@
1
+ import type { PermissionPolicy } from '@byok-sdk/protocol';
2
+ export interface ClaudePermissionMapping {
3
+ ok: boolean;
4
+ /** CLI args to append to `claude -p ...`. Only meaningful when `ok` is true. */
5
+ args: string[];
6
+ /** Present when `ok` is false. */
7
+ reason?: string;
8
+ /**
9
+ * M4 Phase 3: set only for `policy.mode === 'confirm'` — tells
10
+ * `claude-adapter.ts`'s `start()` to additionally spin up the out-of-band
11
+ * approval channel (write the temp `--mcp-config` file pointing at
12
+ * `bin/byok-approval-mcp.ts`, then append `--permission-prompt-tool`) on
13
+ * top of the base `args` returned here. Kept out of `args` itself because
14
+ * generating that config is a real filesystem side effect (a temp
15
+ * directory + file write) that has no business happening inside this
16
+ * otherwise-pure, I/O-free mapping function — see `claude-adapter.ts`'s
17
+ * `start()` for where it actually happens.
18
+ */
19
+ needsApprovalMcp?: boolean;
20
+ }
21
+ /**
22
+ * Map an effective {@link PermissionPolicy} to `claude -p ...` CLI args,
23
+ * fail-closed. Empirically grounded against the real installed `claude`
24
+ * 2.1.212 binary (see the M2-a report) — every claim below was reproduced
25
+ * live, not inferred from `--help` text or training-data recall (`--help`
26
+ * text alone was actively misleading in at least one case: `--allowedTools`
27
+ * reads like a hard allowlist but is NOT one — see the finding below).
28
+ *
29
+ * ## Two DIFFERENT, easily-confused tool-control flags (the central finding)
30
+ *
31
+ * `claude --help` documents two separate flag families that look
32
+ * interchangeable but are not:
33
+ *
34
+ * - `--allowedTools` / `--disallowedTools`: a PERMISSION pre-grant/deny
35
+ * list. Empirically, this only affects whether a tool call needs an
36
+ * interactive prompt — it does NOT reliably restrict what the model can
37
+ * do once a broadly-permissive `--permission-mode` (acceptEdits,
38
+ * bypassPermissions, dontAsk) is also in effect. Confirmed two ways: (a)
39
+ * `--permission-mode acceptEdits --allowedTools Read` still let a Write
40
+ * call succeed (zero `permission_denials`) — the allowlist was silently
41
+ * ignored once the broad grant was in effect; (b) `--permission-mode
42
+ * acceptEdits --disallowedTools Write` correctly blocked the *named*
43
+ * `Write` tool call (`<tool_use_error>Error: No such tool available:
44
+ * Write...`), but the model then simply used `Bash` instead (still
45
+ * enabled) to write the same file — `(Bash completed with no output)`,
46
+ * file created anyway. A single denied tool name is not a security
47
+ * boundary when a general-purpose Bash tool remains available.
48
+ * - `--tools`: REPLACES the entire active built-in tool set (pi's own
49
+ * `--tools` flag works the same way — this is the one place claude and pi
50
+ * share an identical convention). Confirmed: `--tools Read` shrinks
51
+ * `system/init`'s reported `tools` array to just `['Read', ...MCP
52
+ * tools]`; the model then cannot even attempt Write (no tool definition
53
+ * to call), regardless of `--permission-mode`. `--tools ""` disables
54
+ * every built-in tool outright (confirmed: only the always-on MCP tool
55
+ * remained). This is the ONLY mechanism this mapper trusts to actually
56
+ * restrict the model's capability surface — never `--allowedTools`/
57
+ * `--disallowedTools` for that purpose.
58
+ *
59
+ * ## Mode mapping
60
+ *
61
+ * - `auto` (no `allowTools`): `--permission-mode acceptEdits`. Empirically
62
+ * confirmed to auto-accept BOTH file edits (Write) and Bash execution
63
+ * with zero `permission_denials` — broader than the interactive-mode
64
+ * folklore that acceptEdits only covers file edits. Verified for a
65
+ * benign `echo ... > file` Bash command specifically; not exhaustively
66
+ * verified across every possible Bash command shape.
67
+ * - `auto` with `allowTools` (no `denyTools`): adds `--tools
68
+ * <allowTools.join(',')>` (the replacive flag) on top of `acceptEdits`,
69
+ * mirroring pi's own `allowTools`-only branch.
70
+ * - `readonly`: ALWAYS `--permission-mode default` (never acceptEdits/
71
+ * bypassPermissions/dontAsk — see the finding above: a permissive mode
72
+ * defeats any restriction) plus `--tools
73
+ * <intersect(allowTools ?? READONLY_TOOLS, READONLY_TOOLS) - denyTools>`.
74
+ * An empty resulting set emits `--tools ""` explicitly — never an absent
75
+ * `--tools` flag, which would default to claude's full active set and
76
+ * silently widen a readonly request (the exact class of bug pi's own
77
+ * `--no-tools` fallback exists to prevent).
78
+ * - `plan`: `--permission-mode plan`. Empirically confirmed to never
79
+ * execute the requested mutating tool call against its real target — the
80
+ * model instead writes a plan document and stops. **Caveat, flagged for
81
+ * the M2-c freeze decision, not silently hidden**: it writes that plan
82
+ * file to `~/.claude/plans/<slug>.md` — the real user's home directory,
83
+ * OUTSIDE `ctx.workspaceDir` — unconditionally, regardless of cwd. This
84
+ * is a genuine, confirmed workspace-confinement gap specific to plan
85
+ * mode's own bookkeeping (the path is fixed/product-owned by Claude Code
86
+ * itself, not attacker/model-directed, and no destructive action runs
87
+ * against the actual task target) — mapped as supported rather than
88
+ * failed-closed because refusing would make an entire policy mode whose
89
+ * name and semantics match this protocol's own `plan` mode 1:1
90
+ * completely unusable over a relatively minor, fixed-path side effect,
91
+ * but this is a judgment call for a human to weigh in on, not a fact.
92
+ * - `confirm`: SUPPORTED as of M4 Phase 3 — `--permission-mode default`
93
+ * (deny-by-default baseline, same as `readonly`'s own choice above) plus
94
+ * `allowTools`/`denyTools` mapped exactly like `auto` does (finding F2,
95
+ * fixed same session as the confirm-mode support itself first shipped —
96
+ * see the function body below): an explicit `--tools <allowTools>` when
97
+ * only `allowTools` is set, and a fail-closed refusal whenever `denyTools`
98
+ * is non-empty, since confirm — like `auto`/`plan` and unlike
99
+ * `readonly` — has no bounded, known-safe base tool list to subtract
100
+ * from; the only trustworthy restriction mechanism (`--tools`) is
101
+ * replacive, not subtractive. This composes with, rather than replaces,
102
+ * `--permission-prompt-tool` pointed at a small bundled MCP server
103
+ * (`bin/byok-approval-mcp.ts`) that forwards the pending permission
104
+ * decision to this device's own daemon over its control socket and blocks
105
+ * until a human (or the daemon's own timeout) resolves it. This directly
106
+ * supersedes the M2-a finding that `confirm` was inexpressible: that
107
+ * finding was empirically true for `--permission-mode` ALONE (every
108
+ * decision resolved synchronously, no pause) — `--permission-prompt-tool`
109
+ * is a DIFFERENT flag, undocumented in `claude --help`'s output on the
110
+ * installed 2.1.216 binary (confirmed accepted anyway — an unrecognized
111
+ * flag is rejected outright with `error: unknown option`, this one is
112
+ * not), that genuinely pauses the turn on a real MCP round-trip while
113
+ * claude waits for an answer (M4 Phase 3 STEP 0: live-verified allow,
114
+ * deny, and multi-second-delayed-then-allow, against the real installed
115
+ * binary — see `../../bin/byok-approval-mcp.ts`'s own doc comment for the
116
+ * full empirical writeup, including the one caveat found: a permission-
117
+ * prompt-tool call that NEVER answers at all triggers claude's own
118
+ * internal abandonment of the turn after roughly 1.5s — never actually hit
119
+ * by this design, since `byok-approval-mcp` always eventually answers
120
+ * within its own configured ceiling, but disclosed rather than silently
121
+ * assumed away).
122
+ * - `denyTools` non-empty under `auto`: FAILS CLOSED. Given the
123
+ * `--allowedTools`/`--disallowedTools`-under-a-permissive-mode escape
124
+ * hatch above, the only mechanism this mapper trusts (`--tools`) is
125
+ * REPLACIVE, not subtractive — pi can resolve `denyTools` to an
126
+ * equivalent allowlist because pi's own default active tool set is
127
+ * fixed and known from its installed source; claude's active tool
128
+ * surface is NOT reliably known ahead of time (empirically, this exact
129
+ * dev machine's own installed build exposes a bespoke, non-vanilla tool
130
+ * set — see `READONLY_TOOLS`'s doc comment), so there is no reliable
131
+ * "default set minus these" this mapper can construct. Refusing is the
132
+ * fail-closed choice over guessing a set that might not match reality.
133
+ *
134
+ * `network: false` fails closed for the same reason as pi: no verified
135
+ * network sandbox exists for claude's Bash tool either (`claude --help`
136
+ * exposes no network/sandbox flag at all) — this was not independently
137
+ * re-verified against real network traffic the way the tool-restriction
138
+ * findings above were (doing so would require an actual network probe this
139
+ * task didn't run), but is the same conservative, precedent-consistent
140
+ * default pi already applies for an unverifiable constraint.
141
+ */
142
+ export declare function mapPermissionPolicyToClaudeArgs(policy: PermissionPolicy): ClaudePermissionMapping;
@@ -0,0 +1,103 @@
1
+ import { spawn } from 'node:child_process';
2
+ import type { ClaudeStreamMessage } from './events';
3
+ export type SpawnFn = typeof spawn;
4
+ export interface ClaudeProcessClientOptions {
5
+ command: string;
6
+ args: string[];
7
+ cwd: string;
8
+ env: NodeJS.ProcessEnv;
9
+ spawnFn?: SpawnFn;
10
+ }
11
+ /**
12
+ * NDJSON process transport for `claude -p --input-format stream-json
13
+ * --output-format stream-json`.
14
+ *
15
+ * Structurally simpler than pi's `PiRpcClient` in one real way, and
16
+ * different (not simpler) in another:
17
+ *
18
+ * - No request/response correlation. pi's RPC mode replies to each command
19
+ * with a `{type:"response", id, success, ...}` — claude's stream-json has
20
+ * no such acknowledgement at all; writing a `{"type":"user",...}` line
21
+ * just starts (or queues) a turn, and the ONLY confirmation is the
22
+ * ordinary event stream itself (starting with a `system/init` frame).
23
+ * There is therefore no `pending` id->resolver map here.
24
+ * - `waitForInit()` exists specifically to compensate for that missing
25
+ * ack: pi's `start()` fails fast because a bad flag/auth error rejects
26
+ * the pending `send()` promise for the first command. Claude's own
27
+ * `AsyncQueue.end()` (used for the `events` stream) is a CLEAN,
28
+ * non-throwing end — a process that crashes before ever emitting a line
29
+ * would otherwise look, from the async-iteration protocol alone, exactly
30
+ * like a session that legitimately produced zero events, silently
31
+ * swallowing the real failure. `waitForInit()` is a dedicated promise
32
+ * that resolves with the real `session_id` once claude's own
33
+ * `system/init` frame arrives, or rejects with the same enriched
34
+ * exit-error `events` would otherwise swallow — this is what lets
35
+ * `ClaudeAdapter.start()` fail loudly and immediately for a bad
36
+ * `--resume` target etc., mirroring pi's own fail-fast contract with a
37
+ * mechanism suited to claude's ack-less protocol instead of copying pi's
38
+ * request/response one verbatim.
39
+ *
40
+ * Framing, stderr-ring, and unmapped-frame-tally-in-exit-error all mirror
41
+ * `../pi/rpc-client.ts`'s already-proven design directly (LF-delimited
42
+ * JSONL, `node:readline` avoided for the same U+2028/U+2029 reason pi's
43
+ * doc comment explains, `close` not `exit` for the same complete-stderr
44
+ * guarantee) — these are generic, sound patterns, not pi-specific logic,
45
+ * so re-implementing them independently here (rather than importing from
46
+ * `../pi/`) keeps this adapter fully self-contained, matching this repo's
47
+ * existing per-adapter isolation.
48
+ */
49
+ export declare class ClaudeProcessClient {
50
+ private readonly child;
51
+ private buffer;
52
+ private readonly eventQueue;
53
+ private closed;
54
+ private exitError;
55
+ private readonly stderrRing;
56
+ private readonly unmappedFrameCounts;
57
+ private sessionId;
58
+ private initWaiter;
59
+ constructor(options: ClaudeProcessClientOptions);
60
+ /**
61
+ * Write a new user turn onto stdin (`--input-format stream-json`'s wire
62
+ * shape: `{"type":"user","message":{"role":"user","content":[{"type":
63
+ * "text","text":...}]}}`). Used identically for the very first turn
64
+ * (`ClaudeAdapter.start()`) and any later same-session turn
65
+ * (`ClaudeSession.followUp()`) — empirically confirmed live that claude
66
+ * keeps a `--input-format stream-json` process alive across multiple
67
+ * sequential turns on ONE persistent process/session (same `session_id`
68
+ * reported on each turn's own `system/init` and `result` frames), only
69
+ * exiting when stdin is closed or the process is killed. This is the
70
+ * mechanism `followUp()` relies on instead of spawning a fresh
71
+ * `--resume`'d process per follow-up.
72
+ */
73
+ writeUserMessage(text: string): void;
74
+ /**
75
+ * Resolves with claude's own `session_id` once its `system/init` frame
76
+ * arrives (see this class's doc comment for why this exists at all).
77
+ * Idempotent: once resolved, further calls resolve immediately with the
78
+ * same id; if the process already closed before init ever arrived,
79
+ * every call rejects with that same exit error.
80
+ */
81
+ waitForInit(): Promise<string>;
82
+ /** Every parsed stream-json line — `system/init` is consumed internally (see `waitForInit`) but is also forwarded here like any other frame, so routine-frame accounting in `ClaudeSession`'s mapper stays uniform. */
83
+ get events(): AsyncIterable<ClaudeStreamMessage>;
84
+ /**
85
+ * Record a claude stream-json frame/subtype/content-block label that
86
+ * `ClaudeSession`'s event iterator (`../claude-adapter.ts`) decided has
87
+ * no `AgentEvent` mapping and isn't routine bookkeeping (see
88
+ * `events.ts`'s `MapClaudeMessageResult.unmappedLabel` doc comment) —
89
+ * i.e. genuinely unexpected traffic. Mirrors pi's
90
+ * `PiRpcClient.recordUnmappedFrame` exactly: logs once per distinct
91
+ * label, folds the running tally into a later exit error for a
92
+ * post-mortem without separate log scraping.
93
+ */
94
+ recordUnmappedFrame(label: string): void;
95
+ /** Best-effort teardown. SIGTERM on POSIX; `taskkill /T /F` on Windows to also reap child processes claude itself spawned (e.g. Bash) — mirrors pi's cross-platform `kill()` exactly. Empirically confirmed on this (POSIX) machine: a running claude process exits cleanly within ~1s of SIGTERM (observed exit code 143 = 128+SIGTERM, i.e. claude catches and handles the signal itself rather than needing a harder kill). */
96
+ kill(): void;
97
+ private onData;
98
+ private onLine;
99
+ private onStderr;
100
+ /** Mirrors pi's `buildExitError` exactly — stderr tail + unmapped-frame tally folded into one self-diagnosing message. */
101
+ private buildExitError;
102
+ private onClosed;
103
+ }
@@ -0,0 +1,45 @@
1
+ export interface ResolvedApprovalMcpBin {
2
+ command: string;
3
+ args: string[];
4
+ source: 'env' | 'dist';
5
+ }
6
+ /**
7
+ * Resolve `byok-approval-mcp` — the small stdio MCP server
8
+ * (`bin/byok-approval-mcp.ts`) `claude`'s own `--permission-prompt-tool`
9
+ * spawns as ITS child process (see that file's doc comment, and
10
+ * `permission-mapping.ts`'s `confirm`-mode doc comment, for the full design).
11
+ *
12
+ * Unlike `resolveClaudeBin` (the end user's own separately-installed,
13
+ * separately-authenticated CLI, resolved via bare-name PATH lookup),
14
+ * `byok-approval-mcp` is a script THIS SAME `@byok-sdk/client` package ships —
15
+ * bare-name PATH lookup is NOT safe for it: `@byok-sdk/client` is typically a
16
+ * project-local dependency, so its `node_modules/.bin/byok-approval-mcp`
17
+ * symlink is only on PATH for processes that inherit THAT project's own
18
+ * shell/PATH, not reliably for a background OS service (launchd/systemd
19
+ * often run with a stripped-down PATH that omits project-local
20
+ * `node_modules/.bin` entirely — see `templates/service/**`). Resolving an
21
+ * ABSOLUTE path to this package's own compiled bin avoids depending on PATH
22
+ * at all.
23
+ *
24
+ * `BYOK_APPROVAL_MCP_BIN` overrides everything when set — the injectable
25
+ * seam for tests (mirrors `BYOK_CLAUDE_BIN`/`BYOK_PI_BIN`), letting a test
26
+ * substitute a fixture script instead of computing any real path. The
27
+ * override is a single command string with no separate args (tests don't
28
+ * need to invoke it any differently than `node <script>`); the real default
29
+ * below is `node <absolute-path-to-the-built-bin>`.
30
+ *
31
+ * The default computation is deliberately anchored to THIS module's own
32
+ * `import.meta.url`, resolved once at the real production entry point: when
33
+ * `@byok-sdk/client` is built (`tsup.config.ts`), this file's code ends up
34
+ * bundled into `dist/index.js` at the package root, with `dist/bin/
35
+ * byok-approval-mcp.js` as its direct sibling (same layout `byok-agent.js`
36
+ * already uses) — `path.join(path.dirname(fileURLToPath(import.meta.url)),
37
+ * 'bin', 'byok-approval-mcp.js')` is therefore correct for that one real
38
+ * shape. It is NOT correct for this file's own unbundled TypeScript source
39
+ * location (`src/adapters/claude/` is two directories deeper than `src/`),
40
+ * but nothing in this codebase ever reaches this fallback unbundled — every
41
+ * test that exercises `confirm` mode sets `BYOK_APPROVAL_MCP_BIN` explicitly
42
+ * (see `claude-adapter.test.ts`), exactly like `BYOK_CLAUDE_BIN` already
43
+ * does for the real `claude` binary.
44
+ */
45
+ export declare function resolveApprovalMcpBin(): ResolvedApprovalMcpBin;
@@ -0,0 +1,27 @@
1
+ export interface ResolvedBin {
2
+ command: string;
3
+ source: 'env' | 'path';
4
+ }
5
+ /**
6
+ * Resolve the `claude` (Claude Code) CLI executable.
7
+ *
8
+ * Unlike pi (`../pi/resolve-bin.ts`), this package does NOT bundle a
9
+ * matched `claude` build as an optionalDependency. Claude Code is the end
10
+ * user's own globally-installed, individually-authenticated CLI (`claude
11
+ * auth login`, tied to their Anthropic/claude.ai account) — there is
12
+ * nothing useful to vendor: a bundled copy could never carry the user's own
13
+ * login state, and the credential-isolation rule (see `../../types.ts`'s
14
+ * `RuntimeAdapter` doc comment — this adapter must never read, proxy, or
15
+ * forward `~/.claude`'s own auth storage) means this adapter has no
16
+ * business managing a claude install at all, only spawning whatever `claude`
17
+ * the user already has authenticated on their PATH.
18
+ *
19
+ * Resolution is therefore deliberately two-tier, not three like pi's:
20
+ * `BYOK_CLAUDE_BIN` overrides everything when set (the injectable seam for
21
+ * in-process tests — mirrors `BYOK_PI_BIN` and substitutes the
22
+ * `fake-claude.mjs` fixture ahead of a real claude install, exactly as pi's
23
+ * own override does), otherwise this falls back to the literal command name
24
+ * `claude`, resolved via the child process's own PATH lookup — there is no
25
+ * optionalDependency tier in between.
26
+ */
27
+ export declare function resolveClaudeBin(): ResolvedBin;