@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/startorthread/resumeconfig (mcp_servers, as OARmcpServers), next to the user'sconfig.toml. Codex is qualified against 0.160.0, needs app-server support and usesapproval_policy=neverwith sandboxdanger-full-accessby default (DaemonConfig.codexSandbox). Detection refuses an unavailable app-server. It never refuses a version: an auto-updated Codex is admitted with aruntime_version_unqualifiedadvisory, shown bybyok-agent runtimes. - Pi: before admission, the daemon starts each projected server and reads its
own
tools/listanswer. 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.jsonThe 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.jsonPOSIX 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.jsonConnect 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.
