npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@byok-sdk/client

v0.25.0

Published

BYOK SDK client daemon: runs on the end user's machine, pairs with a SaaS server, and drives a local coding-agent runtime

Downloads

2,223

Readme

@byok-sdk/client

Diagnostics and recovery integration

Use the SDK-maintained downstream guide to build diagnostic UI and local repair flows. byok-agent doctor --json is the current read-only CLI entrypoint; --fix --yes only quarantines confirmed-corrupt operational health state with the daemon stopped. It does not repair an Agent or rebuild its journal. The guide includes integration limits and acceptance scenarios; qualify them against the exact SDK artifact shipped by your product.

The public diagnoseDevice API accepts the host's actual adapters. repairDeviceEnrollmentMetadata (or the named CLI --repair restore-enrollment-metadata action) restores missing/valid-stale non-secret enrollment metadata from its existing OS authority, with confirmation, exact expected tenant/device and exclusive store ownership. It does not renew credentials or prove Agent readiness.

Exact provider-profile admission

When DaemonConfig.piByokLauncher is configured, the daemon advertises the additive provider-profile-binding capability. A byok-profile dispatch selection carries only an opaque local profileRef, exact revision/hash, model, and bounded required capabilities. PiAdapter.prepare() asks the keys launcher to validate that binding before claim. Missing or stale local state, model mismatch, and unsupported capabilities decline without workspace or runtime side effects. The immutable operation manifest seals the nested binding, and the launcher revalidates it before credential access and spawn.

The task offer and manifest never contain the provider Base URL or secret.

The local BYOK daemon. It pairs a device, durably journals tasks, connects over WebSocket or long poll, dispatches to local Claude Code, Codex, or pi adapters, and exposes authenticated local diagnostics/control commands.

The package installs byok-agent and the SDK-reserved byok-agent-message-mcp task helper. The message helper exposes only bounded plain text/Markdown; authenticated task, Agent, session, device, tenant, and destination facts remain daemon/server authority and are never model input. The helper receives only a daemon-issued single-task sealed context token; it cannot select a task id or product destination. Provider credentials are not read by the dispatch plane; @byok-sdk/keys is a separate install and keeps a zero dependency edge to this package.

Single-file Bun/SEA products must explicitly re-enter SDK-reserved helpers before their own CLI parser. The SDK owns the reserved subcommand and helper implementation; the product does not resolve dist/bin paths:

import { createDaemon, runSdkReservedHelperCommand } from '@byok-sdk/client';

if (await runSdkReservedHelperCommand()) process.exit(0);

const daemon = createDaemon({
  // ...normal device, Agent-home, and egress configuration
  sdkHelperHost: { mode: 'self-executable' },
});

Pi uses the same re-entry: each Pi launch starts <executable> [<entry>] __byok_sdk_helper <pi-rpc|pi-prepared|pi-durable>. Set PI_PACKAGE_DIR to the product's Pi asset root; a Bun-compiled executable may keep those assets beside itself instead.

Normal Node/Bun source hosts omit sdkHelperHost and continue to use the package's installed helper scripts. A required-message offer performs an exact stdio MCP initialize/tools-list handshake before adapter preparation; an unwired or unstartable single-file helper is declined before runtime execution.

Pi is a required exact npm dependency and runs as an external Node subprocess. For an authoritative BYOK dispatchSelection, configure piByokLauncher with the separately installed byok-pi-provider-launcher, the local non-secret profile database path, and a stable Pi session directory. The client passes only those paths plus provider/model ids; the launcher alone reads the OS credential when required and spawns Pi. Both custody paths must be absolute; missing launcher configuration fails closed. A macOS host running under an isolated HOME can additionally set piByokLauncher.macosKeychainPath to one absolute keychain file. The client projects it as the launcher's reserved --macos-keychain-path flag; it does not search a second credential authority or widen the Pi child environment.

Claude Code and Codex remain user-installed runtimes and use their own login state. Each runtime child inherits the daemon environment, provider API keys included. The daemon removes only CLAUDECODE and its own BYOK_* names. Loader names such as NODE_OPTIONS reach the child as the user set them. Claude loads the user's own MCP configuration, settings, deny rules and hooks. The SDK adds its task servers with --mcp-config. Ordinary Pi loads the user's own extensions and skills from its agent directory. Codex runs with sandbox danger-full-access by default. Set DaemonConfig.codexSandbox to read-only or workspace-write for a stricter mode, or to inherit to apply the user's own config.toml. Any other value makes createDaemon throw a TypeError.

Hosts that only need runtime detection/composition can import the transport-free adapter surface:

import { PiAdapter, ClaudeAdapter, CodexAdapter } from '@byok-sdk/client/adapters';

Version 0.4.0 intentionally breaks custom adapters: they expose a frozen descriptor and side-effect-free prepare() that returns one prepared operation; the old direct start() surface is removed. A published Session.close() is a bounded quiescent-disposal receipt. It resolves only after the adapter-owned process tree and task resources are gone, or rejects with RuntimeDisposalFailure. The daemon keeps active/Git ownership after a rejection and never rewrites the task's already-established terminal result.

Claude, Codex and Pi tasks can select operator-owned local stdio MCP servers by logical id. The toolset selector carries no MCP command or connector credential:

import { createDaemon } from '@byok-sdk/client';

createDaemon({
  // ...normal device and transport configuration
  mcpToolsets: {
    'salesko.prospecting': {
      mcpServers: {
        'salesko-connectors': {
          command: '/opt/salesko/bin/connector-mcp',
          args: ['--profile', 'default'],
        },
      },
    },
  },
});

The map accepts only command and args; put OAuth tokens, cookies, and other secrets behind the local MCP process's own credential broker.

The SDK grants no per-tool permission (ADR-037). Sessions run YOLO and each tool call follows the agent's own guardrails:

  • Claude: the selected servers go into a task-scoped --mcp-config, next to the user's own MCP configuration. Claude starts with --dangerously-skip-permissions.
  • Codex: the selected servers go in the thread/start or thread/resume config (mcp_servers, as OAR mcpServers), next to the user's config.toml. Codex is qualified against 0.160.0, needs app-server support and uses approval_policy=never with sandbox danger-full-access by default (DaemonConfig.codexSandbox). Detection refuses an unavailable app-server. It never refuses a version: an auto-updated Codex is admitted with a runtime_version_unqualified advisory, shown by byok-agent runtimes.
  • Pi: before admission, the daemon starts each projected server and reads its own tools/list answer. Pi registers one tool per observed tool.

For Pi, a projected server that cannot start, or that lists no tools, is declined pre-claim and retryably, rather than claimed and handed a toolset the model can list but never call. A server that answers with a tool name that cannot be registered is declined permanently (retryable: false), with the server and the offending tool named in the decline.

The daemon derives one sorted configuredToolsets snapshot from this validated registry. Only those logical IDs are advertised in conn.hello and hosted presence; command, args, environment, headers, and credentials remain local.

Hosted deployments that enforce an activity-ingress byte ceiling should inject the same ceiling into the daemon. The byte count is the UTF-8 length of JSON.stringify(events); it does not include envelope or transport overhead. One event that cannot fit fails the task locally without truncation or network delivery.

createDaemon({
  // ...normal device and transport configuration
  progressBatch: {
    maxBatchBytes: 64 * 1024,
  },
});

The value is intentionally host-owned and has no SDK default because it is a deployment/read-model policy, not a frozen protocol limit.

Durable Agent homes

An Agent-capable daemon receives one absolute branded storage root. The SDK, not the host, composes agents/<agentId>, validates canonical containment, creates missing MEMORY.md and notes/ without overwriting existing bytes, and binds the resulting Agent home as runtime cwd.

import { createAgentHomeProjection, createDaemon } from '@byok-sdk/client';

createDaemon({
  // ...normal device and transport configuration
  agentHome: {
    hostStorageRoot: '/Users/alice/.salesko',
    projection: createAgentHomeProjection(async ({ agentRef, cwd }) => {
      // Host code receives the canonical home. It supplies redacted profile
      // content but never joins `agents/<agentId>` and never writes secrets.
      await profileProjection.write({ agentRef, canonicalAgentHome: cwd });
    }),
  },
});

For task-free desired-state projection, use createAgentHomeProjectionConsumer. Its hook must atomically and idempotently ensure its opaque product bytes. BYOK may invoke it again under the same canonical-home writer lease when a new request carries the exact current revision/hash; the terminal outcome remains idempotent. This permits repair of locally lost derived files without giving the SDK product path or schema knowledge. Stale and same-revision/different-hash requests do not invoke it.

Startup materializes and write-probes the canonical root before publishing agent-home-contract. agentHome and gitWorkspace are mutually exclusive; strict Agent execution has one workspace authority and never falls back to a task-scoped Git workspace.

Successful startup with this configuration advertises agent-home-contract. Agent offers are distinct from legacy task offers and fail closed when identity, profile revision, or session/runtime/cwd evidence does not match. Within one daemon process, execution leases are scoped to (agentId, sessionRef): different sessions of one Agent may run concurrently in the same canonical home, while the same session remains serialized. Fresh tasks bind their task-scoped admission lease to the runtime-created session before the SDK exposes that session. Shared .byok metadata mutations use a short per-home gate. Agent-memory hosted projection serializes the complete close-time outbox transaction per home because its durable outbox is one CAS authority; the publish wait remains timeout-bounded and does not serialize the sessions' runtime execution. The process-owned home activity marker remains held until the final active session exits so relocation stays fail-closed. A second daemon process remains excluded by that marker; cross-process session multiplexing is not provided. Agent files other than the SDK-reserved .byok namespace are opaque; there is no required artifacts/ directory and the client does not parse or index their contents.

Embedded Agent memory

A product that embeds this SDK rather than running the daemon owns its own Agent home, its own lease, and — on macOS — the absolute signed and notarized helper binary. It still must not own the memory authority itself: the sha256 compare-and-swap, the audit record, the platform gate, and the exact set of paths a model may name stay in the SDK. @byok-sdk/client/agent-memory is that authority without the daemon.

import {
  AgentMemoryService,
  captureAgentMemorySnapshot,
  isAgentMemorySecureFilesystemAvailable,
  openAgentMemoryFilesystemHelper,
  prependAgentMemoryGuidance,
  serveAgentMemoryMcpOverStdio,
} from '@byok-sdk/client/agent-memory';

if (!isAgentMemorySecureFilesystemAvailable(helperBin !== undefined)) return;

const context = {
  taskId, tenantId, deviceId, agentRef, sessionRef, runtimeId, leaseId,
  canonicalHome: lease.canonicalHome,
  homeIdentity: lease.homeIdentity,
  // macOS only: the host's own helper binary, admitted by absolute path.
  ...(helperBin === undefined ? {} : {
    filesystem: await openAgentMemoryFilesystemHelper({
      helperBin, canonicalHome: lease.canonicalHome, homeIdentity: lease.homeIdentity,
    }),
  }),
};

const service = new AgentMemoryService(context);
serveAgentMemoryMcpOverStdio({ deps: service });
const instruction = prependAgentMemoryGuidance(agentInstruction);
// After the session closes, while the lease still exists:
const snapshot = await captureAgentMemorySnapshot(context);

Platform behavior is inherited from the daemon path, not restated: Linux uses the native descriptor-relative backend, macOS requires the external helper, and Windows stays fail-closed with or without one.

This entry deliberately reaches no transport, no daemon composition, and no control socket — importing the same symbols from the package root pulls all three in. connectControlClient is not public anywhere in this package and must not become reachable here; src/__tests__/agent-memory-entry-constraints.test.ts pins the source module graph and scripts/check-agent-memory-entry.mjs pins the built bundle.

Hosted projection is not on this entry. An embedded host gets the local snapshot and no way to send it anywhere from this package.

Because each entry is bundled separately, AgentMemoryError imported from @byok-sdk/client/agent-memory and from @byok-sdk/client are distinct constructors. Discriminate on error.name, not instanceof, if a host mixes both entries.

Daemon-free assertion requests

A Host toolset server is a short-lived stdio process the daemon spawns for one task. Its whole job is to trade the BYOK_HOST_TOOLSET_CONTEXT nonce it was started with for one short-lived task assertion and present it to the product's cloud; it runs no daemon, drives no runtime, and touches no transport. Importing requestTaskAssertion from the package root handed it all three anyway — the root entry composes createDaemon, which reaches @earendil-works/pi-coding-agent and through it @modelcontextprotocol/sdk and ajv, and the root graph statically imports @modelcontextprotocol/client, whose published dist embeds an ajv provider built on new Function. Under a Content-Security-Policy or any runtime that refuses code generation, the call a toolset server needed was unreachable because of code it never ran. @byok-sdk/client/assertion-client is the same two functions without that graph.

import { requestTaskAssertion } from '@byok-sdk/client/assertion-client';

const result = await requestTaskAssertion({
  productId, contextToken, audience: 'https://api.example.com',
});
if (!result.ok) return refuse(result.code, result.reason);
presentToCloud(result.assertion, result.expiresAt);

The entry exports exactly requestTaskAssertion, requestDeviceAssertion and their option/result types. connectControlClient stays unreachable here for the same reason it is unreachable everywhere else in this package: that socket also carries shutdown, approval resolution, and the raw task-event stream. The root entry keeps exporting both functions; this is a narrower door to the same authority, not a replacement. src/__tests__/dist-subpath-closure.test.ts walks the emitted bundle for runtime code generation and for any import specifier that is not a node builtin, @byok-sdk/core, @byok-sdk/protocol, or a relative path, and runs the same checker against dist/index.js as a control that must report hits.

Serving an MCP toolset over stdio

@byok-sdk/client/mcp-server is the server counterpart to this package's MCP client authority: a tools-only stdio MCP server, transport and baseline only, with no product semantics in it. The four SDK-reserved helpers (byok-agent-message-mcp, byok-agent-memory-mcp, byok-agent-team-mcp) are served through it, and a host that spawns its own toolset server can use the same entry.

import { McpServerToolError, serveMcpOverStdio } from '@byok-sdk/client/mcp-server';

serveMcpOverStdio({
  serverInfo: { name: 'acme-toolset', version: '1.0.0' },
  tools: [{ name: 'lookup', description: 'Look one record up.', inputSchema: LOOKUP_SCHEMA }],
  callTool: async ({ name, arguments: args, signal }) => {
    if (name !== 'lookup') throw new McpServerToolError(-32602, `unknown tool "${name}"`);
    return { content: [{ type: 'text', text: await lookup(args, { signal }) }] };
  },
});

What the core decides is the protocol and nothing else. It answers initialize by SELECTING from MCP_SERVER_SUPPORTED_PROTOCOL_VERSIONS (['2025-11-25', '2025-06-18', '2024-11-05']) and never by echoing what the peer offered — echoing asserts support for any string a peer sends, including revisions the server does not implement. It authors the advertised capabilities itself ({tools:{}}, or {tools:{listChanged:true}} only when you supply a real toolsListChanged emitter): a client routes requests on that advertisement, so a capability the core does not implement must never appear in it, and there is no option through which you can add one. Both frame directions are bounded at 1 MiB — the same ceiling @byok-sdk/client's MCP client applies — and an over-cap outbound frame is dropped whole rather than truncated, with the session closing fail-closed. A notifications/cancelled aborts the call's AbortSignal and no response is ever written for that id afterwards; a late answer is an observable protocol violation, not a nicety. Malformed envelopes, unusable or duplicate ids and batch arrays are refused before your handler is reachable.

What the core never decides is anything about a TOOL. Your callTool owns which names exist, what an invalid argument is, and whether a domain failure is an error or a successful result carrying a refusal: a handler that returns normally always produces a result, and only a thrown McpServerToolError becomes an error. That is what lets the approval helper answer an unreachable daemon with a successful {behavior:'deny'} payload, which is the behaviour that keeps claude from abandoning the turn.

The entry adds no dependency. @modelcontextprotocol/sdk is not required to run an SDK-reserved MCP server; the emitted bundle reaches node builtins only, and src/__tests__/dist-subpath-closure.test.ts keeps that true.

The sub-path is an unreleased candidate: it is not in a published artifact yet, so consume it from the workspace until the release that ships it.

Agent egress and explicit content reads

agentEgress is consumed policy configuration, not a profile or tenant projection. The host selects one exact policy revision. The daemon obtains its tenant binding only from the authenticated pair response persisted in the atomic local DeviceRecord; there is no agentEgress.tenantId setting and no Profile/config, deviceId, or access-token fallback. Runtime activity, results and artifacts go to the Host as is; the SDK does not filter, redact or omit them. Reliable events are fsynced under the canonical Agent home and retire only after an exact ack.

Device credentials belong to the OS user and productId. All storeDir values for that product share one OS credential entry. A new directory does not create an unpaired device. Before replacing server state, stop all daemons for that product and unpair with the old configuration (byok-agent unpair --config <path>). Alternatively, use a new productId for an independent enrollment. Unpair clears the shared credential for every directory of that product.

Hosts that need cold setup or diagnostic state use readDeviceEnrollmentStatus({ productId, storeDir }). It validates the complete SDK-owned record but returns only unpaired, paired with deviceId, or re_pair_required; tenant, token, expiry and device keys are never projected. Only explicit pairing may replace re_pair_required state, while filesystem-safety failures remain errors.

A host that must address this device itself (for example to register the sealed-provisioning sealing key) reads the non-secret enrollment identity with readDeviceEnrollmentIdentity({ productId, storeDir }): the same three states, and when paired { tenantId, deviceId, proofKeyId, proofKeyEpoch, enrollmentRevision }. enrollmentRevision is String(proofKeyEpoch), the value a Host issues from its device row (proof_key_epoch) as the sealed request's expectedEnrollmentRevision. It then signs host-defined device proofs with the stored enrollment key through a scoped signer; the key never leaves the SDK:

import { PROVIDER_SECRET_SEALING_KEY_REGISTER_OPERATION } from '@byok-sdk/protocol';
import { createStoredDeviceProofSigner, readDeviceEnrollmentIdentity } from '@byok-sdk/client';

const enrollment = await readDeviceEnrollmentIdentity({ productId, storeDir });
if (enrollment.state !== 'paired') throw new Error(`device ${enrollment.state}`);
const { state: _paired, ...identity } = enrollment;
const signer = createStoredDeviceProofSigner({
  productId,
  storeDir,
  identity,
  operations: [PROVIDER_SECRET_SEALING_KEY_REGISTER_OPERATION],
});
const proof = await signer.sign({ method: 'PUT', path, operation: PROVIDER_SECRET_SEALING_KEY_REGISTER_OPERATION,
  resource, requestId, body: sealingKeyClaimBytes });

The options and every request are copied once into inert plain data (own enumerable data properties only; accessors, Proxies, symbol keys and non-plain prototypes are refused as invalid_options / invalid_request, and body must be an exact Uint8Array), and only that copy is checked and signed, so the operation checked against the allowlist is the operation signed. readDeviceEnrollment* and retireInputPreparation read their productId/storeDir and mode/confirmed the same way and reject anything else with a TypeError that has no cause. An operation outside operations is refused before the key is read; each signature re-reads the enrollment and refuses (enrollment_changed) if tenant, device or proof key no longer equal identity. Failures are DeviceProofSignerError closed codes. The signer also satisfies TruthMemoryClient's signer option.

The v8 input-preparation retirement is also a library call, so a branded host CLI or installer can own it: retireInputPreparation({ productId, storeDir }, { mode: 'preview' }) inspects and writes nothing; { mode: 'execute', confirmed: true } moves the old namespace aside with a manifest and refuses, typed and with zero writes, while the daemon answers, the owner lease is held, or any row is pinned, current/mixed/unknown-version or unparseable, or a path is a symlink. byok-agent retire-input-preparation [--yes] renders the same function.

createDaemon({
  // ...normal device, transport and agentHome configuration
  agentEgress: {
    policy: {
      policyRevision: 'salesko-agent-egress-r1',
      activity: { delivery: 'latest-value', maxCoalesceMs: 250, maxEventBytes: 256 * 1024 },
      reliable: {
        maxPendingEventsPerAgent: 256,
        maxPendingBytesPerAgent: 4 * 1024 * 1024,
        maxPendingBytesPerTenant: 16 * 1024 * 1024,
      },
      transfers: {
        workspace: { maxBytes: 1024 * 1024, allowedMimeTypes: ['text/plain'] },
        transcript: 'disabled',
        artifact: 'disabled',
      },
    },
    contentRead: {
      workspace: {
        root: { kind: 'agent-home' },
        maxTextBytes: 1024 * 1024,
        textMimeTypes: ['text/plain'],
      },
    },
  },
});

Each content surface requires both the matching non-disabled wire policy and its local supplement. The local supplement can only narrow root, text, MIME, size and sensitive-name behavior; it cannot enable a wire-disabled surface. The SDK derives agents/<agentId>, .byok/egress, runtime-session evidence and the per-Agent content-read audit path. Salesko must not compose those paths. Tenant/device identity comes from the persisted authenticated enrollment; a request or editable host configuration cannot override it. Transcript reads additionally require the exact persisted AgentRef/session/runtime/cwd handoff. Allowed content is uploaded through the authenticated blob channel. The content-free receipt is fsynced into the Agent-local reliable spool with stable event/cursor identity before send and retires only after an exact ack; an allowed receipt carries the exact BlobRef. No API recursively mirrors an Agent home.

For a concrete private host composition, see the examples/salesko-connector-broker reference. It keeps @byok-sdk/client credential-blind while combining OS-backed refresh-token custody, a PKCE desktop Google OAuth flow, exact domain policy, a real read-only Gmail metadata adapter, and a closed metadata-only MCP result.

Local TeamWorkspace and tmux communication pane

byok-agent team provides one local-only broadcast channel for Pi, Claude, and Codex harnesses. The daemon owns durable ordered messages, member receipts, quotas, and short-lived member leases under <storeDir>/team-workspaces/v1. team join prints the exact byokagentteam stdio MCP configuration for a member; model tool inputs never contain workspace or sender identity.

byok-agent team create dev --members pi,claude,codex --config /absolute/agent.json
byok-agent team join dev --member pi --config /absolute/agent.json
byok-agent team open dev --tmux-bin /opt/homebrew/bin/tmux --config /absolute/agent.json

The tmux view has one explicit native dependency: tmux must be installed and its absolute executable path supplied with --tmux-bin. It is intentionally not an npm dependency and is not required to run the daemon, MCP channel, or plain watcher. Native Windows returns unsupported_platform for the tmux view. The launcher never uses send-keys or capture-pane; tmux displays the daemon-owned stream but is never message transport or protocol authority.

Automatic notification for two existing Codex sessions

Configure two operator-owned Codex app-server sessions with their respective team join MCP grants. Record each exact native thread UUID and local endpoint. The relay does not create sessions or verify your member-to-session mapping. Write an absolute-path JSON file with mode 0600 (its contexts are bearer secrets):

{"version":1,"bindings":[
  {"context":"<member-a-context>","threadId":"<native-thread-a-uuid>","endpoint":"ws://127.0.0.1:9101","afterSeq":0},
  {"context":"<member-b-context>","threadId":"<native-thread-b-uuid>","endpoint":"ws://127.0.0.1:9102","afterSeq":0}
]}
byok-agent team relay dev --bindings /absolute/private-bindings.json --codex-bin /absolute/codex --max-notifications 2 --config /absolute/agent.json

POSIX only. codex-cli 0.160.0 is the qualified native queue version; another version prints a warning and continues. Endpoints must be explicit loopback ws://127.0.0.1:<port> / ws://[::1]:<port> or unix:///absolute/socket. No remote server discovery. Loopback app-server queue endpoints trust local processes; the relay does not add authentication to the native Codex endpoint. Keep the foreground command open and enter pause, resume, status, or stop; SIGINT/SIGTERM also stop. Output includes queue attempts/receipts and redacted state. A successful queue receipt means accepted notification, not completed work. The model reads, replies and acknowledges with the existing Team MCP tools.

The required budget counts every attempt, capped at 100. Unknown delivery, revoked/expired grant or control failure stops without retry. An operator stop during enqueue can report stopped with queue_delivery_unknown: aborting the local queue process cannot prove the native server rejected the notification. Pause does not undo queued work. A room lock rejects concurrent relays; inspect the recorded owner before manually removing a stale <storeDir>/team-relay-locks/<room>.lock. Watermarks are process-local. On restart choose afterSeq explicitly from prior status and actual room receipts; do not assume automatic crash replay, exactly-once or guaranteed at-least-once delivery. Renewing grants requires explicitly updating both the session MCP grant and binding file. tmux remains an optional view.

Codex + Pi through a GUI host

team pi-relay owns a fresh RPC child on the pinned Pi fork runtime (see the Core pi runtime contract) alongside your existing Codex session. Prepare a private 0600 absolute-path binding document:

{"version":1,"codex":{"context":"<codex-grant>","threadId":"<uuid>","endpoint":"ws://127.0.0.1:9101","afterSeq":0},"pi":{"context":"<pi-grant>","afterSeq":0,"cwd":"/absolute/workspace","sessionDir":"/absolute/new-session-dir","provider":"<provider>","model":"<model>","systemPrompt":"<explicit instructions>","extensionPaths":[]}}

The session directory must not exist; its parent must exist. The SDK supplies the Pi Team MCP tools and guard extension. Additional absolute extension paths are operator-trusted code, loaded after the guard. Ambient extension loading is off.

byok-agent team pi-relay dev --bindings /absolute/private.json --codex-bin /absolute/codex --max-notifications 2 --config /absolute/agent.json

Connect a GUI backend to stdin/stdout JSONL. This command provides the interface; it does not include a GUI app. Keep stdin open while the session runs.

{"command":"status"}
{"command":"pause"}
{"command":"resume"}
{"command":"input","sessionId":"<pi_ready sessionId>","message":"<operator input>"}
{"command":"respond","sessionId":"<ui_request sessionId>","requestId":"<request.id>","response":{"cancelled":true}}
{"command":"stop"}

For confirm, use {"confirmed":true} or false; for select/input/editor, use {"value":"..."}. Exactly one response field is accepted. Wrong session, stale ID, duplicate answer or mismatched shape is rejected. ui_response_sent confirms pipe write only: Pi may have expired that ID. Expiry leaves the GUI item pending until you explicitly dismiss it. Render ui_request.request as untrusted display data.

The first-loaded native guard holds input/provider admission during Pi UI spans. pi_gate reports that state; pi_settled reports completed native work. Busy Pi waits for readiness. A 30-second unresolved RPC stops the owned process, without retry. Budget exhaustion drains accepted Pi work for up to 120 seconds; explicit stop or stdin EOF terminates it. Neither pause nor a new dialog recalls already admitted work. Runtime/grants remain separate from the native Codex session.

MIT licensed. Node.js 24.15.0 or newer.

Direct adapter environments

Hosts that use @byok-sdk/client/adapters can import buildRuntimeEnv from that entry. Pass buildRuntimeEnv({ ambient: process.env }) as the operation's env. It copies the environment and removes CLAUDECODE and BYOK_*. It retains loader variables such as NODE_OPTIONS, LD_*, and DYLD_*, as required by ADR-037. Direct adapters use the environment supplied by the Host. The builder uses case-insensitive names on Windows.

codexSandbox belongs to createDaemon, which builds the default adapters. createDaemonWithAdapters rejects that key. Set sandbox on the injected CodexAdapter instead.