@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
|
@@ -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
|
+
}
|