@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,97 @@
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 SpawnFn } from './process-runner';
5
+ export interface CodexAdapterOptions {
6
+ /** Override bin resolution — tests substitute the fake-codex fixture script. */
7
+ resolveBin?: () => ResolvedBin;
8
+ /** Override process spawning — tests substitute a fake spawn. */
9
+ spawnFn?: SpawnFn;
10
+ }
11
+ /**
12
+ * `RuntimeAdapter` for the OpenAI Codex CLI (`codex exec --json`), the M2-b
13
+ * counterpart to `../pi/pi-adapter.ts`. Every empirical claim in this file
14
+ * and its sibling modules (`events.ts`, `permission-mapping.ts`,
15
+ * `process-runner.ts`) was driven live against the real installed `codex-cli
16
+ * 0.144.5` in a scratch directory before being encoded — repeating the pi
17
+ * adapter's own M0-3 discipline ("docs lied and shipped a nonexistent flag")
18
+ * independently found the exact same bug class on codex:
19
+ *
20
+ * - `codex exec --help` documents `-a`/`--ask-for-approval`; the real
21
+ * parser rejects it outright on `codex exec` ("unexpected argument").
22
+ * - `-s`/`--sandbox` works on a fresh `codex exec` but is rejected outright
23
+ * on `codex exec resume` (whose own --help correctly omits it).
24
+ * - `codex exec resume` does NOT auto-inherit the sandbox mode a session
25
+ * was originally started with — a read-only-started session's write
26
+ * SUCCEEDED on a bare resume with no sandbox override re-passed,
27
+ * silently falling back to this machine's own ambient config default.
28
+ * - This task's own brief assumed SIGINT for `interrupt()`; empirically,
29
+ * `codex exec` ignores SIGINT entirely (a 60s `sleep` ran to completion
30
+ * despite SIGINT at t=4s) — SIGTERM is used instead (confirmed to work:
31
+ * immediate exit, no orphaned children, thread stays resumable after).
32
+ *
33
+ * See `./permission-mapping.ts` and `./process-runner.ts` for the full
34
+ * per-finding writeups (sandbox scope, network, approval model, resume
35
+ * mechanics, stdin handling).
36
+ *
37
+ * Architecture, and how it differs from pi: pi is one long-lived `pi --mode
38
+ * rpc` process for a whole session's lifetime, driven by a bidirectional
39
+ * JSONL request/response protocol (`../pi/rpc-client.ts`). `codex exec` has
40
+ * no such thing — it's a one-shot batch process per turn, prompt in via
41
+ * argv, JSONL out via stdout, process exits. `CodexSession` here instead
42
+ * spawns a fresh `CodexProcessRunner` for every turn (the initial `start()`
43
+ * and every later `followUp()`), and forwards each one's mapped events into
44
+ * one shared, session-lifetime `AsyncQueue` — the thing `Session.events`
45
+ * actually exposes. `sessionRef` is codex's own `thread_id`, learned from
46
+ * `thread.started`, which is reliably the first JSONL line codex ever prints
47
+ * (confirmed across every empirical capture, fresh starts and resumes
48
+ * alike) — `runCodexTurn` below awaits specifically for that line before
49
+ * resolving, mirroring pi's own "resolve a real session id before
50
+ * constructing the Session, fail closed if you can't" discipline
51
+ * (`../pi/pi-adapter.ts`'s `resolveFreshSessionId`, finding F8).
52
+ */
53
+ export declare class CodexAdapter implements RuntimeAdapter {
54
+ private readonly options;
55
+ readonly id = "codex";
56
+ constructor(options?: CodexAdapterOptions);
57
+ detect(): Promise<RuntimeDetectResult>;
58
+ /**
59
+ * `authPresent` without ever reading `~/.codex/auth.json` (credential-
60
+ * isolation rule, `../../types.ts`): spawns codex's OWN `login status`
61
+ * subcommand and interprets its human-readable report — the exact
62
+ * "non-secret signal" this adapter is required to use, and cleaner than
63
+ * pi's env-var-name check since codex's real credential model (on the
64
+ * reference machine) is a ChatGPT OAuth session, not an env var.
65
+ *
66
+ * Two independently-verified channel gotchas apply here, the "pi lesson"
67
+ * yet again:
68
+ * - `codex login status`'s human-readable "Logged in using ChatGPT"
69
+ * message prints on STDERR, not stdout (the opposite-channel
70
+ * counterpart of pi's own `--version`-goes-to-stderr surprise) — both
71
+ * streams are checked here for exactly that reason.
72
+ * - The NOT-logged-in message/exit-code shape was deliberately never
73
+ * empirically tested: this machine has a real, live ChatGPT login, and
74
+ * running `codex logout` to observe the negative case would have
75
+ * broken that login for the rest of this session/machine. The match
76
+ * below is intentionally conservative (`/logged in (using|with)/i`,
77
+ * not a bare `"logged in"` substring) specifically because a bare
78
+ * substring check would false-positive on a plausible negative message
79
+ * like "Not logged in" (itself containing the substring "logged in").
80
+ * This is a documented, known gap — flagged for M2-c / a follow-up
81
+ * empirical pass on a logged-out machine, not asserted as verified.
82
+ */
83
+ private probeAuthPresent;
84
+ capabilities(): RuntimeCapabilities;
85
+ /**
86
+ * M5: same deliberate posture as the claude adapter (see its own doc
87
+ * comment) — codex authenticates via its own `codex login`-managed
88
+ * ChatGPT OAuth session (`probeAuthPresent` above), not an env var, so
89
+ * there is no credential env var this adapter needs forwarded; env-based
90
+ * API-key passthrough remains a separate, pending product decision. No
91
+ * `baseNames` either: nothing in this adapter reads a codex-specific
92
+ * config-discovery variable (e.g. `CODEX_HOME`) today.
93
+ */
94
+ environmentRequirements(): RuntimeEnvironmentRequirements;
95
+ start(task: TaskOfferPayload, ctx: TaskContext): Promise<Session>;
96
+ private resolveBin;
97
+ }
@@ -0,0 +1,41 @@
1
+ import type { AgentEvent } from '@byok-sdk/protocol';
2
+ import type { CodexRawEvent } from './process-runner';
3
+ /**
4
+ * `codex exec --json` event catalog, empirically captured (raw JSONL frames,
5
+ * not inferred from docs) driving the real installed `codex-cli 0.144.5`
6
+ * across: plain chat, file writes (`apply_patch`-style), shell commands,
7
+ * network attempts, sandbox denials, an invalid-model API error, and a
8
+ * SIGTERM-killed-then-resumed thread. Two nesting levels: top-level frames
9
+ * (`thread.started`, `turn.started`, `item.started`, `item.completed`,
10
+ * `turn.completed`, `turn.failed`, and a top-level `error` distinct from any
11
+ * item), and — inside `item.started`/`item.completed` — a nested `item.type`
12
+ * (`agent_message`, `command_execution`, `file_change`, `error`).
13
+ *
14
+ * `thread.started` (always the first line codex ever prints, both for a
15
+ * fresh `codex exec` and for `codex exec resume`) is deliberately NOT
16
+ * handled here — it's consumed directly by `../codex-adapter.ts`'s
17
+ * `runCodexTurn` to resolve `Session.sessionRef` before this mapper ever
18
+ * sees a line, mirroring how pi's adapter resolves a fresh session id via
19
+ * `get_state` before constructing its `Session` (`../pi/pi-adapter.ts`'s
20
+ * `resolveFreshSessionId`). It's still listed in
21
+ * {@link ROUTINE_CODEX_EVENT_TYPES} for defensiveness, in case it were ever
22
+ * unexpectedly seen again mid-stream.
23
+ *
24
+ * No case here ever produces `{type: 'needs_approval'}` — deliberately, not
25
+ * an oversight. `codex exec` (this build) has no wire-visible signal for it
26
+ * at all: a sandbox-denied action under any approval policy resolves
27
+ * internally with no pause an external caller could ever answer — confirmed
28
+ * empirically (a `sandbox_mode=read-only` write attempt, and separately an
29
+ * `approval_policy=untrusted` shell command outside the sandbox's own
30
+ * trusted-command allowlist, both either just ran when the sandbox allowed
31
+ * it or were silently auto-denied when it didn't, narrated only as a normal
32
+ * `agent_message` — never a distinct event this mapper could hook). See
33
+ * `../codex-adapter.ts`'s `resolveApproval`, which throws rather than
34
+ * pretending to support a resume path that cannot exist on the wire.
35
+ */
36
+ export declare function mapCodexEventToAgentEvents(evt: CodexRawEvent, workspaceDir: string): AgentEvent[];
37
+ /** Top-level codex event types with no `AgentEvent` equivalent, by design (not "not yet handled") — kept in sync with the switch cases above sharing this comment. Used only for observability, mirroring pi's `ROUTINE_PI_EVENT_TYPES`: `codex-adapter.ts`'s per-line pump only records a frame as genuinely unmapped (`unmappedFrameKey` + a one-time console.warn) when it maps to an empty array AND its type isn't in this set. */
38
+ export declare const ROUTINE_CODEX_EVENT_TYPES: ReadonlySet<string>;
39
+ export declare function isRoutineCodexEvent(evt: CodexRawEvent): boolean;
40
+ /** Diagnostic key for unmapped-frame accounting: for `item.*` frames, folds in the nested `item.type` (e.g. `"item.completed:reasoning"`) so a never-seen item shape is distinguishable from a never-seen top-level type. */
41
+ export declare function unmappedFrameKey(evt: CodexRawEvent): string;
@@ -0,0 +1,107 @@
1
+ import type { PermissionPolicy } from '@byok-sdk/protocol';
2
+ export interface CodexPermissionMapping {
3
+ ok: boolean;
4
+ /** `-c key=value` args (as separate argv entries, e.g. `['-c', 'sandbox_mode=read-only']`) to append to `codex exec`/`codex exec resume`. Only meaningful when `ok` is true. */
5
+ args: string[];
6
+ /** Present when `ok` is false. */
7
+ reason?: string;
8
+ }
9
+ /**
10
+ * Map an effective {@link PermissionPolicy} to `codex exec`/`codex exec
11
+ * resume` CLI args, fail-closed. Empirically grounded against the real
12
+ * installed `codex exec --json` (`codex-cli 0.144.5`) — every claim below was
13
+ * driven live in a scratch directory before being encoded here, exactly like
14
+ * the pi adapter's own M0-3 findings (`../pi/permission-mapping.ts`), and the
15
+ * same class of "the CLI's own --help lies" bug showed up again independently
16
+ * (see the confirm/plan case below).
17
+ *
18
+ * - Uses `-c sandbox_mode=<value>` (a config-override key), NEVER the `-s`/
19
+ * `--sandbox` CLI flag, and NEVER `-a`/`--ask-for-approval`:
20
+ * - `-s`/`--sandbox` works on a fresh `codex exec` but is REJECTED
21
+ * outright ("error: unexpected argument '-s' found") on `codex exec
22
+ * resume` — confirmed against the real binary, not inferred from
23
+ * --help (which lists `-s` for `exec` but, correctly for once, omits
24
+ * it entirely from `resume`'s own --help).
25
+ * - `-a`/`--ask-for-approval` is documented in `codex exec --help`
26
+ * itself, yet REJECTED outright by `codex exec`'s real arg parser
27
+ * ("error: unexpected argument '-a' found") — a second, independent
28
+ * instance of the exact doc/binary mismatch class the pi adapter's own
29
+ * `--session-id`/`--exclude-tools` findings already hit. Never used.
30
+ * - `-c sandbox_mode=...` was confirmed empirically to produce the
31
+ * IDENTICAL restriction as `-s` on a fresh `exec` (a read-only-sandbox
32
+ * write attempt fails the same way either way), and to also work on
33
+ * `resume` (where `-s` cannot be used at all) — so this one mechanism
34
+ * is used everywhere, for both a fresh start and every later resume,
35
+ * instead of switching mechanisms depending on call site.
36
+ * - CRITICAL: `codex exec resume` does NOT auto-inherit the sandbox mode
37
+ * the session was originally started with. Empirically confirmed: a
38
+ * session started with `sandbox_mode=read-only` (which correctly
39
+ * denied a write on its first turn) had that SAME write SUCCEED on a
40
+ * later `codex exec resume` with no sandbox flag re-passed — silently
41
+ * falling back to this machine's own ambient `~/.codex/config.toml`
42
+ * default. This is exactly the "silently widens" failure this whole
43
+ * mapping exists to prevent, so this adapter re-pins
44
+ * `-c sandbox_mode=...` (and `-c approval_policy=never`, see below) on
45
+ * *every* invocation — the initial `start()` AND every `followUp()` —
46
+ * never relying on "it was already set once."
47
+ * - `auto` -> `sandbox_mode=workspace-write`. Empirically, this permits
48
+ * writes inside the given cwd (the task workspace) AND the OS temp dir
49
+ * (e.g. `/tmp`/`$TMPDIR`) — NOT strictly confined to the workspace, a
50
+ * real, non-obvious scope worth knowing about (confirmed: a shell write to
51
+ * `/tmp/...` succeeds even though it's outside `ctx.workspaceDir`; a write
52
+ * to `$HOME` is correctly denied with a clean "operation not permitted",
53
+ * not a hang or silent no-op).
54
+ * - `readonly` -> `sandbox_mode=read-only`. Empirically denies every write —
55
+ * both `apply_patch`-style file edits and shell-redirection writes — with
56
+ * an immediate, clean, non-interactive denial (real stderr: "writing is
57
+ * blocked by read-only sandbox; rejected by user approval settings").
58
+ * Never a hang, never a silent bypass.
59
+ * - `confirm` / `plan` are NOT expressible and fail closed. `codex exec` has
60
+ * no interactive approval channel at all in this build — confirmed two
61
+ * independent ways: (1) the flag that would request one (`-a`/
62
+ * `--ask-for-approval`) is rejected outright by the real parser (see
63
+ * above); (2) even reaching the underlying setting via the `-c
64
+ * approval_policy=...` config-key path (which IS accepted), a
65
+ * sandbox-denied action under a non-`never` policy resolves the "ask"
66
+ * *internally* with no wire-visible pause and no way for an external
67
+ * caller to answer it: it just auto-denies, and the model narrates the
68
+ * denial as a normal `agent_message` — there is no `needs_approval`-
69
+ * equivalent signal anywhere in the `--json` stream to map to (see
70
+ * `../codex/events.ts`'s module doc comment). `plan` has no dedicated
71
+ * codex equivalent either (no plan-only/no-exec CLI mode exists on this
72
+ * build). Rather than pretend either is supported, both fail closed here —
73
+ * the same posture as pi's own `confirm`/`plan` rejection.
74
+ * - `network: true` is NOT expressible and fails closed. The one config key
75
+ * that should enable network under `workspace-write`
76
+ * (`sandbox_workspace_write.network_access=true`) was tested three times
77
+ * against the real binary — including a pure `-c`-only invocation with no
78
+ * `-s` flag mixed in, ruling out CLI-flag/config-table interference as the
79
+ * cause — and never restored real network access: a `curl` to a live host
80
+ * still failed every time, though its failure mode shifted from a
81
+ * DNS-level block (exit 6, "Could not resolve host") to a data-level block
82
+ * (exit 56, "Failure receiving network data"), hinting the override does
83
+ * *something* internally but not enough to trust as a real grant. A
84
+ * control run under `sandbox_mode=danger-full-access` confirmed network
85
+ * genuinely works from this environment (HTTP 200), so the gap is in this
86
+ * specific override on this codex build, not the test methodology. This
87
+ * adapter never silently proceeds without the grant it was asked for, so
88
+ * `network: true` is rejected rather than quietly running with no network
89
+ * and no explanation. `network: false`/unset needs no special handling:
90
+ * both sandbox modes this adapter ever selects have NO network by default
91
+ * (confirmed empirically for both `read-only` and plain `workspace-write`)
92
+ * — the safe case is also the default case, so nothing to enforce.
93
+ * - `allowTools`/`denyTools` are NOT expressible and fail closed when
94
+ * non-empty. `codex exec` has no verified per-tool allow/deny surface —
95
+ * only the coarse `sandbox_mode` dial — unlike pi's real `--tools`/
96
+ * `--no-tools`. Rather than silently drop a requested tool restriction,
97
+ * this rejects it outright.
98
+ * - `approval_policy=never` is pinned unconditionally on every `ok: true`
99
+ * result, for the same "never trust the ambient default" reason as
100
+ * `sandbox_mode` above: a fresh `codex exec` invoked with NO sandbox/
101
+ * approval flags at all on the reference machine successfully ran a live
102
+ * network `curl` with zero prompting — proof this machine's own
103
+ * `~/.codex/config.toml` default is more permissive than anything this
104
+ * adapter should ever grant implicitly. Every invocation pins both keys
105
+ * explicitly so behavior never depends on the end user's own codex config.
106
+ */
107
+ export declare function mapPermissionPolicyToCodexArgs(policy: PermissionPolicy): CodexPermissionMapping;
@@ -0,0 +1,86 @@
1
+ import { spawn } from 'node:child_process';
2
+ export type SpawnFn = typeof spawn;
3
+ /**
4
+ * One parsed line of `codex exec --json` / `codex exec resume --json`
5
+ * output. Field shapes vary by `type` (see `./events.ts`'s module doc
6
+ * comment for the empirically-captured catalog), so this stays a loose bag
7
+ * rather than a full discriminated union, mirroring `PiRpcMessage` in
8
+ * `../pi/rpc-client.ts`.
9
+ */
10
+ export interface CodexRawEvent {
11
+ type: string;
12
+ [key: string]: unknown;
13
+ }
14
+ export interface CodexProcessOptions {
15
+ command: string;
16
+ args: string[];
17
+ cwd: string;
18
+ env: NodeJS.ProcessEnv;
19
+ spawnFn?: SpawnFn;
20
+ /** Called once per parsed JSONL line, in arrival order. */
21
+ onEvent: (evt: CodexRawEvent) => void;
22
+ }
23
+ /**
24
+ * Spawns and streams ONE `codex exec` / `codex exec resume` invocation — i.e.
25
+ * exactly one turn.
26
+ *
27
+ * Unlike pi (a single long-lived RPC server process for a whole session's
28
+ * lifetime — see `../pi/rpc-client.ts`), `codex exec` is a one-shot batch
29
+ * process per turn with no persistent request/response channel: it takes its
30
+ * prompt as an argv positional, streams JSONL to stdout for the one turn
31
+ * it's running, and exits. `../codex-adapter.ts`'s `CodexSession` constructs
32
+ * a fresh `CodexProcessRunner` for every turn (the initial `start()` and
33
+ * every later `followUp()`), forwarding each one's lines into the same
34
+ * long-lived event queue.
35
+ *
36
+ * stdin is deliberately never piped to the child (`stdio: ['ignore', 'pipe',
37
+ * 'pipe']`): `codex exec --help` documents that a piped, non-TTY stdin is
38
+ * read and appended to the prompt as a `<stdin>` block even when a prompt was
39
+ * ALSO given as an argv positional, and empirically every single real
40
+ * invocation made while building this adapter logged "Reading additional
41
+ * input from stdin..." on stderr regardless of whether a prompt argument was
42
+ * given. Leaving `stdio: ['pipe', ...]` open for stdin and never closing it
43
+ * risks codex blocking on that read forever — exactly the hang class this
44
+ * task was built to avoid (the pi adapter's own `agent_end`/`agent_settled`
45
+ * mismatch left a task stuck `Running` forever in the M0/M1 GLM run).
46
+ * `'ignore'` presents immediate EOF instead, which was verified live with a
47
+ * dedicated Node `child_process` probe before this was written: no hang,
48
+ * clean completion at normal model latency. This adapter never needs to
49
+ * SEND codex anything over stdin — there is no in-band steer/approval
50
+ * protocol (see `../codex-adapter.ts`'s `steer`/`resolveApproval`).
51
+ */
52
+ export declare class CodexProcessRunner {
53
+ private readonly child;
54
+ private readonly onEvent;
55
+ private buffer;
56
+ private readonly stderrRing;
57
+ private closed;
58
+ private exitCode;
59
+ private exitSignal;
60
+ private readonly closedPromise;
61
+ private resolveClosed;
62
+ constructor(options: CodexProcessOptions);
63
+ private finishClosing;
64
+ /** Resolves once the child process has fully exited (both exit and stdio-flush guaranteed — see the `close` listener above). Never rejects. */
65
+ waitClosed(): Promise<void>;
66
+ get isClosed(): boolean;
67
+ /**
68
+ * Best-effort teardown. SIGTERM on POSIX: SIGINT was empirically confirmed
69
+ * to be silently ignored by `codex exec` (a real, direct test — a 60s
70
+ * shell `sleep` ran to full, unaffected completion despite SIGINT sent at
71
+ * t=4s) — a genuine, evidence-based correction to this task's own initial
72
+ * assumption ("interrupt: SIGINT — POSIX here"). SIGTERM was separately
73
+ * confirmed to terminate the process immediately (exit code 143) with no
74
+ * orphaned child processes left behind (the shell command it was running
75
+ * died with it), and — critically — the underlying codex thread remained
76
+ * cleanly resumable afterward via `codex exec resume` (no corruption from
77
+ * killing mid-turn). `taskkill /T /F` on Windows, mirroring
78
+ * `../pi/rpc-client.ts`'s own cross-platform convention.
79
+ */
80
+ kill(): void;
81
+ /** Builds a descriptive error folding in the exit code/signal and the stderr tail — mirrors `PiRpcClient.buildExitError`'s reasoning: a post-mortem on a failed start/resume should never need separately re-running codex by hand with a raw JSONL logger to learn why. */
82
+ buildExitError(context: string): Error;
83
+ private onData;
84
+ private parseLine;
85
+ private onStderr;
86
+ }
@@ -0,0 +1,25 @@
1
+ export interface ResolvedBin {
2
+ command: string;
3
+ source: 'env' | 'path';
4
+ }
5
+ /**
6
+ * Resolve the codex CLI executable.
7
+ *
8
+ * Unlike pi (bundled as an npm optionalDependency — see
9
+ * `../pi/resolve-bin.ts`), the real OpenAI Codex CLI (empirically `codex-cli
10
+ * 0.144.5` on the machine this adapter was built/verified against, installed
11
+ * at a plain PATH location — not inside this repo's `node_modules`) is not
12
+ * published as an npm package this SDK could sensibly depend on: it's a
13
+ * standalone global install (native installer / `npm i -g @openai/codex` /
14
+ * homebrew, depending on platform and version). There is no package-relative
15
+ * resolution to attempt, so this is simpler than pi's version: an explicit
16
+ * override for tests, else a bare PATH lookup.
17
+ *
18
+ * `BYOK_CODEX_BIN` overrides PATH lookup — substituting the fake-codex test
19
+ * fixture, exactly like `BYOK_PI_BIN` does for pi. The `byok-agent` CLI bin
20
+ * only ever constructs `new CodexAdapter()` with no options (mirroring
21
+ * `createDaemon`'s pi wiring), so an out-of-process substitution (e.g. a
22
+ * future e2e harness swapping in a fake binary ahead of a real codex install)
23
+ * has no other seam to use.
24
+ */
25
+ export declare function resolveCodexBin(): ResolvedBin;
@@ -0,0 +1,58 @@
1
+ import type { AgentEvent } from '@byok-sdk/protocol';
2
+ import type { PiRpcMessage } from './rpc-client';
3
+ /**
4
+ * ROOT CAUSE of the 2026-07-16 live GLM run's "finding #2" hang (task stuck
5
+ * `Running` forever after pi streamed its final answer; pi process alive,
6
+ * idle, no sockets, only stdio pipes): this mapper used to listen for a pi
7
+ * event type `agent_settled` to know a whole run had finished. That type
8
+ * does not exist anywhere in the real, installed
9
+ * `@earendil-works/pi-coding-agent@0.74.2` package — not in its bundled
10
+ * `docs/rpc.md`, not in `dist/modes/rpc/rpc-types.d.ts`, not in
11
+ * `pi-agent-core`'s own `AgentEvent` union (`dist/types.d.ts`), and not
12
+ * observed even once across live probes against real GLM traffic (raw JSONL
13
+ * frame capture). The real "whole run is done, pi is idle again" signal is
14
+ * `agent_end` — confirmed both by `pi-agent-core`'s own doc comment ("
15
+ * `agent_end` is the last event emitted for a run... The agent becomes idle
16
+ * only after those listeners finish") and empirically: a live run's frame
17
+ * sequence ended `...turn_end (tool call), ..., turn_end (final text),
18
+ * agent_end` with nothing further arriving even after an 8s idle-grace
19
+ * window. Because this switch had no `agent_end` case, it fell to
20
+ * `default: return undefined` — silently dropped — so `task-runner.ts`'s
21
+ * `pump()` loop never saw a `turn_end` `AgentEvent` and blocked forever on
22
+ * the next one. This repo's own fixture (`fake-pi.mjs`) masked the bug for
23
+ * the whole M0/M1 test suite by emitting a fictional `agent_settled` frame
24
+ * of its own alongside the real `agent_end` one — fixed alongside this
25
+ * change (see fake-pi.mjs's doc comment).
26
+ *
27
+ * `agent_end` (not pi's own per-LLM-turn `turn_end`) is what maps to our
28
+ * `turn_end`: a single pi prompt can produce several internal turns (tool
29
+ * round-trips, each ending its own real `turn_end`); only once the whole
30
+ * run is settled (no auto-retry, compaction-retry, or queued continuation
31
+ * left) is the task actually done. Forwarding pi's own `turn_end` 1:1 would
32
+ * emit multiple confusing `turn_end`s for what the daemon must treat as one
33
+ * task — empirically confirmed: a single-tool-call prompt produced two real
34
+ * `turn_end` frames (one after the tool call, one after the final text) and
35
+ * exactly one `agent_end`.
36
+ *
37
+ * Returns undefined for pi messages with no protocol equivalent (session/
38
+ * compaction/retry bookkeeping — see `ROUTINE_PI_EVENT_TYPES` below —
39
+ * extension UI dialogs, which never reach here at all in production since
40
+ * `PiRpcClient` answers them itself before they'd ever be queued as an
41
+ * event; see rpc-client.ts).
42
+ */
43
+ export declare function mapPiMessageToAgentEvent(msg: PiRpcMessage): AgentEvent | undefined;
44
+ /**
45
+ * Pi RPC message types that are routine, expected, and deliberately have no
46
+ * `AgentEvent` equivalent (kept in sync with the switch cases above sharing
47
+ * this comment). Used only for observability: `PiSession`'s event iterator
48
+ * (pi-adapter.ts) calls `PiRpcClient.recordUnmappedFrame` for any message
49
+ * type that maps to `undefined` AND isn't in this set — i.e. traffic nobody
50
+ * has ever told this adapter to expect. That distinction is what makes a
51
+ * regression like this file's root-cause bug (`agent_end` going unhandled)
52
+ * self-diagnosing: a warning fires the first time the new/renamed settle
53
+ * event shows up, instead of the daemon just quietly hanging. `default`-only
54
+ * unknowns (a type not listed in the switch at all) are equally "not
55
+ * routine" and get flagged the same way — this set exists so *routine*
56
+ * traffic doesn't also trip that alarm on every single task.
57
+ */
58
+ export declare const ROUTINE_PI_EVENT_TYPES: ReadonlySet<string>;
@@ -0,0 +1,46 @@
1
+ import type { PermissionPolicy } from '@byok-sdk/protocol';
2
+ export interface PiPermissionMapping {
3
+ ok: boolean;
4
+ /** CLI args to append to `pi --mode rpc ...`. Only meaningful when `ok` is true. */
5
+ args: string[];
6
+ /** Present when `ok` is false. */
7
+ reason?: string;
8
+ }
9
+ /**
10
+ * Map an effective {@link PermissionPolicy} to `pi --mode rpc` CLI args,
11
+ * fail-closed. Empirically grounded (see the M0-3 report):
12
+ *
13
+ * - `auto` / `readonly` are expressible via `--tools` / `--no-tools`.
14
+ * - `confirm` and `plan` are NOT expressible: pi ships no built-in per-call
15
+ * approval gate and explicitly "skips ... plan mode" (README); both exist
16
+ * only as example third-party extensions (`examples/extensions/
17
+ * permission-gate.ts`, `examples/extensions/plan-mode/`), which is
18
+ * out-of-scope, speculative surface for M0.
19
+ * - `network: false` is NOT expressible: pi has no network sandbox for its
20
+ * bash tool ("Pi does not include a built-in sandbox" — docs/security.md).
21
+ * `network: true` or unset proceeds, since nothing needs enforcing then.
22
+ *
23
+ * Workspace confinement is NOT a pi flag — the caller spawns pi with
24
+ * `cwd: ctx.workspaceDir`, the daemon-created per-task directory.
25
+ *
26
+ * SECOND SELF-DISCOVERED FINDING, same class as the pi-adapter.ts
27
+ * `--session-id` bug (this task, while hardening argv validation):
28
+ * `--exclude-tools` — what `denyTools` used to map to — is ALSO not a real
29
+ * pi CLI flag. Confirmed against real pi 0.74.2: `pi --mode rpc
30
+ * --exclude-tools bash` → `Error: Unknown option: --exclude-tools`, exit
31
+ * code 1, before any model call — and it's absent from both `pi --help`'s
32
+ * own listing and the CLI's actual arg parser (`dist/cli/args.js`), which
33
+ * recognizes only `--tools`/`-t`, `--no-tools`/`-nt`, `--no-builtin-tools`/
34
+ * `-nbt` for tool control. This crashed EVERY real pi invocation for any
35
+ * policy with a non-empty `denyTools`, unconditionally — never caught by
36
+ * this repo's own test suite for the exact same reason as the
37
+ * `--session-id` bug (`fake-pi.mjs` never validated argv; fixed alongside
38
+ * this change). pi has no "start from the default set, minus these" flag at
39
+ * all — `--tools` always REPLACES the active set wholesale — so `denyTools`
40
+ * is now resolved to an equivalent allowlist in-process: readonly mode
41
+ * intersects with `READONLY_TOOLS` (as before), any other mode starts from
42
+ * an explicit `allowTools` or pi's own `DEFAULT_ACTIVE_TOOLS`, and
43
+ * `denyTools` is subtracted from that resolved set before it's ever handed
44
+ * to pi as a single `--tools`/`--no-tools` pair of args.
45
+ */
46
+ export declare function mapPermissionPolicyToPiArgs(policy: PermissionPolicy): PiPermissionMapping;
@@ -0,0 +1,29 @@
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 SpawnFn } from './rpc-client';
5
+ export interface PiAdapterOptions {
6
+ /** Override bin resolution — tests substitute the fake-pi fixture script. */
7
+ resolveBin?: () => ResolvedBin;
8
+ /** Override process spawning — tests substitute a fake spawn. */
9
+ spawnFn?: SpawnFn;
10
+ }
11
+ export declare class PiAdapter implements RuntimeAdapter {
12
+ private readonly options;
13
+ readonly id = "pi";
14
+ constructor(options?: PiAdapterOptions);
15
+ detect(): Promise<RuntimeDetectResult>;
16
+ capabilities(): RuntimeCapabilities;
17
+ /**
18
+ * M5: pi authenticates to its ~30 supported providers via env-var API
19
+ * keys — `detect()`'s own `authPresent` probe above checks this identical
20
+ * list — so these MUST keep flowing into pi's spawned process or pi auth
21
+ * breaks entirely. `KNOWN_PROVIDER_ENV_VARS` above is the single source
22
+ * of truth, reused here rather than duplicated. No `baseNames`: nothing
23
+ * in this adapter or `rpc-client.ts` reads a pi-specific config-discovery
24
+ * variable beyond the platform baseline (`daemon/environment.ts`).
25
+ */
26
+ environmentRequirements(): RuntimeEnvironmentRequirements;
27
+ start(task: TaskOfferPayload, ctx: TaskContext): Promise<Session>;
28
+ private resolveBin;
29
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The pi coding-agent CLI's real npm package name.
3
+ *
4
+ * IMPORTANT (empirically verified 2026-07-16, see the M0-3 report): the name
5
+ * `@mariozechner/pi` — the identifier this task was originally briefed with —
6
+ * is NOT the coding agent. On npm it resolves to an unrelated "CLI tool for
7
+ * managing vLLM deployments on GPU pods" (bin: `pi-pods`). The real coding
8
+ * agent was `@mariozechner/pi-coding-agent`, which is now itself deprecated
9
+ * in favor of this package (same maintainers: badlogic, mitsuhiko), as of
10
+ * literally the day before this was written. `package.json` pins the
11
+ * `legacy-node20` dist-tag (0.74.2) rather than `latest` (0.80.7), because
12
+ * `latest` requires Node >=22.19 while this SDK's baseline is Node >=20;
13
+ * both versions were empirically confirmed to speak the identical RPC-mode
14
+ * frame shapes this adapter depends on.
15
+ */
16
+ export declare const PI_PACKAGE_NAME = "@earendil-works/pi-coding-agent";
17
+ export interface ResolvedBin {
18
+ command: string;
19
+ source: 'package' | 'path';
20
+ }
21
+ /**
22
+ * Resolve the pi CLI executable. Prefers the optionalDependency installed
23
+ * alongside @byok-sdk/client (guarantees a known-good, version-matched build);
24
+ * falls back to whatever `pi` is on PATH so users with a pre-existing global
25
+ * pi install still work even when the optionalDependency didn't install.
26
+ *
27
+ * `BYOK_PI_BIN` overrides both of the above when set: `PiAdapterOptions.resolveBin`
28
+ * is the injectable seam for in-process tests, but the `byok-agent` CLI bin
29
+ * only ever constructs `new PiAdapter()` with no options (see `createDaemon`),
30
+ * so an out-of-process substitution (e.g. examples/basic's e2e run swapping
31
+ * in the fake-pi fixture ahead of a real pi install) has no other seam to use.
32
+ *
33
+ * Deliberately does NOT use `createRequire(...).resolve()`: this package is
34
+ * pure ESM with no `require` export condition (`exports["."]` only offers
35
+ * `import`), so CJS-style resolution fails outright with
36
+ * `ERR_PACKAGE_PATH_NOT_EXPORTED` — empirically confirmed, not a hypothetical.
37
+ * It also does NOT resolve the `./package.json` subpath directly (also not
38
+ * exported by this package); instead it resolves the package's main entry
39
+ * via `import.meta.resolve` and walks up parent directories to find the
40
+ * package root (identified by a `package.json` whose `name` matches), which
41
+ * is robust to `dist/` layout changes and to whether `./package.json` is
42
+ * ever exported.
43
+ */
44
+ export declare function resolvePiBin(): ResolvedBin;
@@ -0,0 +1,101 @@
1
+ import { spawn } from 'node:child_process';
2
+ export type SpawnFn = typeof spawn;
3
+ /**
4
+ * A pi RPC-mode message: either a `{type:"response", id, success, ...}` reply
5
+ * to a command we sent, or an unsolicited event/extension-UI-request. Field
6
+ * shapes vary by `type` (see docs.md / this task's live probes), so this
7
+ * stays a loose bag rather than a full discriminated union — M0 only needs
8
+ * a handful of fields off of each.
9
+ */
10
+ export interface PiRpcMessage {
11
+ type: string;
12
+ id?: string;
13
+ [key: string]: unknown;
14
+ }
15
+ export interface PiRpcClientOptions {
16
+ command: string;
17
+ args: string[];
18
+ cwd: string;
19
+ env: NodeJS.ProcessEnv;
20
+ spawnFn?: SpawnFn;
21
+ }
22
+ /**
23
+ * JSONL request/response + event-stream client for `pi --mode rpc`.
24
+ *
25
+ * Framing per pi's own docs/rpc.md: strict JSONL, LF (`\n`) as the only
26
+ * record delimiter (a trailing `\r` is stripped; this deliberately does NOT
27
+ * use `node:readline`, which pi's docs call out as non-compliant because it
28
+ * also splits on U+2028/U+2029 — valid inside JSON strings).
29
+ *
30
+ * Responses are correlated by `id`, never by arrival order: empirically,
31
+ * pi's responses do not preserve request order (an immediate parse-failure
32
+ * response can overtake a slower in-flight command's response).
33
+ */
34
+ export declare class PiRpcClient {
35
+ private readonly child;
36
+ private buffer;
37
+ private nextId;
38
+ private readonly pending;
39
+ private readonly eventQueue;
40
+ private closed;
41
+ private exitError;
42
+ /** Bounded tail of recent stderr lines — pi discarded this entirely before (nothing ever read `child.stderr`), which is exactly why finding #1 (`Error: Unknown option: --session-id`, exit 1) had to be root-caused by hand instead of reading it off a thrown error. See `buildExitError`. */
43
+ private readonly stderrRing;
44
+ /** Count of pi RPC message types `PiSession` (pi-adapter.ts) has told us have no `AgentEvent` mapping and aren't routine bookkeeping — see `recordUnmappedFrame`. */
45
+ private readonly unmappedFrameCounts;
46
+ constructor(options: PiRpcClientOptions);
47
+ /** Send a command, resolved with its correlated `response` message. */
48
+ send(command: Record<string, unknown> & {
49
+ type: string;
50
+ id?: string;
51
+ }): Promise<PiRpcMessage>;
52
+ /** Every non-response, non-`extension_ui_request` line — the latter is answered directly by this client (see `respondToExtensionUiRequest`) and never enqueued. */
53
+ get events(): AsyncIterable<PiRpcMessage>;
54
+ /**
55
+ * Record a pi RPC message `type` that `PiSession` (pi-adapter.ts) decided
56
+ * has no `AgentEvent` mapping and isn't routine bookkeeping (see
57
+ * `events.ts`'s `ROUTINE_PI_EVENT_TYPES`) — i.e. genuinely unexpected
58
+ * traffic. Logs once per distinct type (not per occurrence, so a
59
+ * repeating unmapped type can't spam stdout); the running tally is also
60
+ * folded into this client's exit-time error message (`buildExitError`) so
61
+ * a post-mortem on a failed/hung task has it without needing separate log
62
+ * scraping. This is the exact mechanism that would have turned this
63
+ * task's root-cause hang (`agent_end` arriving with no mapping) into a
64
+ * one-line, immediate warning instead of a silent stall.
65
+ */
66
+ recordUnmappedFrame(type: string): void;
67
+ /** Best-effort teardown. SIGTERM on POSIX; `taskkill /T /F` on Windows to also reap child processes pi itself spawned (e.g. bash). */
68
+ kill(): void;
69
+ private onData;
70
+ private onLine;
71
+ /**
72
+ * Answer pi's extension-UI blocking protocol headlessly (rpc.md's
73
+ * "Extension UI Protocol"). Fail-closed policy, stated explicitly because
74
+ * it's a security-relevant default, not an incidental one: this NEVER
75
+ * approves or picks a value on the caller's behalf — every dialog method
76
+ * (`select`/`confirm`/`input`/`editor`) gets `{cancelled: true}`, the one
77
+ * response shape rpc.md documents as valid for all four uniformly
78
+ * ("Dismiss any dialog method... the extension receives `undefined` (for
79
+ * select/input/editor) or `false` (for confirm)"). An extension asking
80
+ * e.g. `confirm("Delete everything?")` gets a firm decline, never a
81
+ * guessed approval — this adapter has no human in the loop to ask, and
82
+ * silently approving would defeat any extension that uses these dialogs
83
+ * specifically as a permission gate. Fire-and-forget methods
84
+ * (`notify`/`setStatus`/`setWidget`/`setTitle`/`set_editor_text`) get no
85
+ * reply at all — sending one would itself violate rpc.md ("Responses are
86
+ * sent for dialog methods only").
87
+ */
88
+ private respondToExtensionUiRequest;
89
+ private onStderr;
90
+ /**
91
+ * finding #1 ("bad flag → instant exit", e.g. the `--session-id`/
92
+ * `--exclude-tools` bugs this task fixes): the daemon used to report only
93
+ * `pi process exited (code=1, signal=null)` — accurate but useless for
94
+ * diagnosing *why* without re-running pi by hand with a raw JSONL logger,
95
+ * exactly as this task's own root-cause investigation had to. Folding in
96
+ * the stderr tail and any unmapped-frame tally makes that self-diagnosing
97
+ * from the thrown error alone.
98
+ */
99
+ private buildExitError;
100
+ private onClosed;
101
+ }