import { type InboundMediaOptions } from "./inbound-media.js"; import { type MessagePart, type PaymentRequestCreateParams, type Relay } from "@relaymessenger/sdk"; import { type BridgeTurn } from "./bridge-turn.js"; export { bridgeTurn, type BridgeTurn } from "./bridge-turn.js"; import type { CodexThreadStore } from "./codex-threads.js"; import { codexMcpServer } from "./hosted-mcp.js"; import { type ApprovalChoice, type ApprovalOutcome, type ApprovalRequest, type OwnerApprovals } from "./approvals.js"; /** * What `relay connect codex` leaves running so Codex answers by itself. * * Codex reaches Relay through MCP, and MCP cannot start a turn, so a message * waits until somebody asks Codex to read it. Claude Code, Hermes and OpenClaw * answer on their own because a long-lived process pushes messages into them. * This is that process for Codex: it holds the agent's event connection, runs * one `codex app-server` in the folder connect ran in, gives every chat its own * Codex thread, and sends the final answer back to the same chat. * * `codex app-server` is the interface Codex's own desktop app and its VS Code * extension speak: newline-delimited JSON-RPC on stdin and stdout. One process * holds many threads, takes a thread back by id after a restart, and stops a * turn that is already running. It runs the `codex` already on this computer, * so the person's own sign-in and settings are the ones Codex uses. Relay's * hosted MCP server travels with every thread (`codexThreadConfig`), so Codex * has Relay's tools whether or not the folder is trusted. */ /** Relay takes 1 to 255 characters for an idempotency key (contracts/relay-v1-openapi.yaml). */ export declare const replyKey: (eventId: string) => string; /** The longest text Relay takes in one message part. */ export declare const MAX_RELAY_TEXT = 10000; /** * What the bridge sends on `thread/start` and `thread/resume`: the folder and * Relay's thread config (`codexThreadConfig`), and no `sandbox` and no * `approvalPolicy`. Relay is the integration, not the harness, so Codex's own * settings decide what Codex may do. * * Both fields are optional in the app-server protocol (`approval_policy: * Option` and `sandbox: Option`, codex-rs * app-server-protocol/src/protocol/v2/thread.rs:88,94, saved at * _sources/codex/). A value sent there wins over the person's own: "Explicitly * setting on-request overrides the project-derived policy" * (developers.openai.com/codex/agent-approvals-security, saved at * _sources/unattended-agent-permissions-20260926/ * codex-agent-approvals-security.txt:32-33). Left out, Codex uses the person's * `config.toml`, the project's policy, or its default, the Auto preset: "no * flags needed or --sandbox workspace-write --ask-for-approval on-request" * (same file, :266-268). * * When Codex asks, the request still reaches this process as a server request * (`CODEX_APPROVAL_METHODS`) and goes to the agent's owners in Relay as a * card (`codexApprovalRequest`). Relay's own write tools stay approved on * its one server (`codexThreadConfig`). This is the Claude Code bridge's rule * too: there only `mcp__relay__*` is allowed without asking, and the * person's settings and Claude Code's defaults decide the rest (Relay-SDK * PR 380, `claude-bridge.ts`). */ export declare const codexThreadSettings: (cwd: string, mcpURL: string) => { cwd: string; config: ReturnType; }; /** The three answers, in Codex's decisions: `accept`, `acceptForSession`, `decline` (v2/CommandExecutionApprovalDecision.ts). */ export declare const CODEX_CHOICES: readonly ApprovalChoice[]; /** * The approval requests app-server sends a client (v2 ServerRequest.ts, * "Approvals" in the app-server docs, * _sources/approvals-inkbox-20260926/codex-app-server-docs.txt): a command, * a file change, and the built-in `request_permissions` tool. The same three * Inkbox's plugin formats (inkbox-codex-plugin-escalation.py.txt, * `format_codex_approval_request`). */ export declare const CODEX_APPROVAL_METHODS: readonly ["item/commandExecution/requestApproval", "item/fileChange/requestApproval", "item/permissions/requestApproval"]; /** A file change app-server announced, by item id, so its card can name the files. */ export type CodexFileChanges = ReadonlyMap; /** * One approval request as the card shows it, in Codex's own tool names * (`shell`, `apply_patch`, `request_permissions`). */ export declare const codexApprovalRequest: (method: string, params: Record, files?: CodexFileChanges) => Pick; /** * Codex's answer to one approval request (the `*RequestApprovalResponse` * types in v2). A command or a file change takes the decision itself; a * permissions request takes the permissions granted and their scope, and an * empty grant is a refusal ("Respond with permissions containing only the * granted subset", app-server docs, "Permission requests"). */ export declare const codexApprovalResponse: (method: string, params: Record, outcome: ApprovalOutcome) => Record; /** * The harness's own wait, when it names one: `autoResolutionMs`, "an integer * millisecond timeout or null" after which "host clients can resolve the * prompt automatically" (app-server docs, `tool/requestUserInput`). The * 0.154.0 schema carries it on `ToolRequestUserInputParams` only, so an * approval request without it waits `APPROVAL_TIMEOUT_MS`. */ export declare const codexTimeout: (params: Record) => number | undefined; /** * Relay's hosted MCP server as a per-thread config override. `thread/start` * and `thread/resume` take `config`, a map of config keys applied over the * loaded layers (codex-rs app-server-protocol/src/protocol/v2/thread.rs:100, * :401). Codex disables a folder's own `.codex/config.toml` until the folder * is trusted, so the project file connect writes gives this process nothing * in a new folder (the Daytona run of 2026-09-26, `mcp_server_count=0`). The * override is the path OpenClaw's Codex harness uses for the same job: it * projects MCP servers into `config.mcp_servers` on thread start and resume * (openclaw extensions/codex/src/app-server/attempt-startup.ts:215-219, * thread-lifecycle-io.ts:151). The token stays in `RELAY_AGENT_TOKEN`. * * Relay's own write tools are approved on that one server, so they run * without a card, as Inkbox's own tools do (inkbox-claude-code-plugin- * sessions.py.txt:1171-1173). Without it Codex stops them: "MCP tool call * requires approval, but approval policy is never" (the Mac run of * 2026-09-26, on a write tool since removed). The person * connected this agent so it acts through Relay, so `send_message`, Relay's * one write tool, carries Codex's per-tool setting `tools..approval_mode = * "approve"` ("Per-tool approval behavior override", * learn.chatgpt.com/docs/extend/mcp?surface=cli; saved at * _sources/mcp-hosted-docs-20260926/codex-extend-mcp-cli.txt:1010-1011, * 1318-1319). No other server changes. * * The agent's token is kept out of every shell command the model runs. * app-server gets `RELAY_AGENT_TOKEN` in its own environment so the relay * entry can read it (`bearer_token_env_var`), and by default Codex hands that * environment to model-run commands too; its shell snapshots saved the token * (the Mac run of 2026-09-26). Codex's `shell_environment_policy.filters` * is "Canonical case-insensitive environment-variable pattern filters" * (map; learn.chatgpt.com/docs/config-file/ * config-reference, saved at * _sources/mcp-hosted-docs-20260926/codex-config-reference.txt:4301-4312), * so the token's one name is excluded there. The MCP client is not a shell * command and still reads it. Every other variable is left as it was. * * The filter alone does not hold: Codex's shell snapshot, "Snapshot shell * environment to speed up repeated commands (stable; on by default)" * (`features.shell_snapshot`, same page, codex-config-reference.txt:1276), * records the environment app-server started with and restores it into * every command. Measured on this Mac with codex-cli 0.155.1 and a stand-in * value: filter only, `echo ${#RELAY_AGENT_TOKEN}` printed 15; filter with * snapshots off, 0; and the bridge's snapshot file held the real token. So * bridge threads run without shell snapshots, which also keeps the token out * of `CODEX_HOME/shell_snapshots`. The commands' environment is otherwise * the same; only the speed-up is gone. */ export declare const codexThreadConfig: (mcpURL: string) => { mcp_servers: Record & { tools: Record; }>; shell_environment_policy: { filters: Record; }; features: { shell_snapshot: false; }; }; /** The one sub-command, over stdin and stdout, which is where it listens by default. */ export declare const APP_SERVER_ARGS: readonly ["app-server"]; /** What app-server is told this client is (`ClientInfo`, ClientRequest.json). */ export declare const CLIENT_NAME = "relaymessenger"; /** Who answers the person: this process sends the final message. */ export declare const ANSWER_INSTRUCTION: string; /** * How the answer carries buttons and links, and when it should: the SDK's one * text for every runtime, so the same person gets buttons and link cards * under the same conditions whichever agent answers. */ export declare const BUTTONS_INSTRUCTION: string; /** * One message, as the prompt Codex is given. Codex keeps its Relay tools during * the turn, so the prompt says who answers the person: this process sends the * final message, and Codex must not send a second one. */ export declare const codexPrompt: (sender: string, text: string, timezone?: string) => string; /** * The messages an answer becomes: its words as text, each link written alone * on a line as its own message, and the buttons its fenced block asked for * under the last words, plus any payment request its fenced block described, * which is created and sent after them. A block the SDK cannot read stays in * the words, so the person still gets the answer, and the terminal says why. */ export declare const answerMessages: (answer: string, sender: string, say: (line: string) => void) => { messages: MessagePart[][]; payment?: PaymentRequestCreateParams; }; /** * Sends an answer, one message at a time in order. The message that arrived * is the key, so a retry after a dropped connection cannot answer the same * person twice; each message past the first carries its index. The first * message replies to the one that arrived (`turn.replyTo`), so a caller * waiting on that message gets this answer even while another of its * messages is open. A payment request is created with the card's own key * after the words go out; when Relay refuses it (Stripe not connected, * Stripe's own 400) the terminal says why and no card is sent. */ export declare const sendAnswer: (client: Pick, turn: Pick, answer: string, say: (line: string) => void, key?: string) => Promise; /** One notification app-server sent, with its parameters. */ export interface AppServerNotification { method: string; params: Record; } /** One running `codex app-server`, and the calls this bridge makes to it. */ export interface CodexAppServer { request(method: string, params: Record): Promise>; notify(method: string, params: Record): void; /** Every notification, until the returned function is called. */ watch(handler: (note: AppServerNotification) => void): () => void; /** Resolves, with the line to show the person, when the process is gone. */ stopped: Promise; /** True once the process is gone, so the next message starts a new one. */ gone(): boolean; } /** What to run for Codex: the file, and anything that comes before `app-server`. */ export interface CodexCommand { command: string; /** Empty for the `codex` on this computer; a test's stand-in is a script Node runs. */ args?: readonly string[]; } /** * The file to run for Codex. connect hands over the one its own sniff found on * PATH, and the bare name when it found none (connect.ts). Windows has no file * called `codex`: npm installs the shim `codex.cmd`, which is what the sniff * looks for there (runtime-sniff.ts), so a bare name is looked up again here * and, failing that, left with the extension `cmd.exe` can find. */ export declare const codexCommand: (found: string, env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform) => Promise; /** Starts `codex app-server` and speaks to it over its own stdin and stdout. */ export declare const startAppServer: (codex: CodexCommand, cwd: string, signal: AbortSignal, env?: Readonly>, /** * Answers one server request, or returns undefined to refuse it. The * promise's value is the JSON-RPC `result`. */ onRequest?: (method: string, params: Record, id: number | string) => Promise> | undefined) => CodexAppServer; /** The turn app-server is running for one chat right now. */ export interface LiveTurn { threadId: string; turnId: string; /** A newer message for this chat replaced it, so its answer is not sent. */ dropped: boolean; } /** How a turn ended (`TurnStatus`, v2/TurnStartResponse.json), and what it said. */ export interface TurnOutcome { /** `completed`, `interrupted`, `failed` or `inProgress`. */ status: string; answer: string; /** Why a `failed` turn failed (`Turn.error.message`, v2/TurnCompletedNotification.json). */ error?: string; /** * The first call to one of Relay's own tools that Codex could not make, as * `: `. An `mcpToolCall` item that ends `failed` with an * `error` never reached the server or never came back from it; a tool that * answered, even with an error of its own, ends with a `result` instead * (v2/ItemCompletedNotification.json, `McpToolCallError`; codex-rs * app-server-protocol thread_history.rs, `handle_mcp_tool_call_end`). */ toolFailure?: string; } /** * Whether an agent message is the turn's answer rather than something said * along the way. `MessagePhase` (v2/ItemCompletedNotification.json) is * `commentary` or `final_answer`, and its own description says providers do not * send it every time, so a message with no phase counts as the answer. */ export declare const isFinalMessage: (phase: unknown) => boolean; /** * Runs one turn in one thread, and gives back what Codex answered. * * The answer is the text of the last final agent message before * `turn/completed`. The streamed pieces (`item/agentMessage/delta`) are only * used when no completed message arrived at all. */ export declare const runTurn: (server: CodexAppServer, input: { threadId: string; prompt: string; images: string[]; onStarted(turn: LiveTurn): void; }) => Promise; export interface CodexBridgeInput { client: Pick; media?: Omit; /** The `codex` to run, and the folder to run it in. */ codex: CodexCommand; cwd: string; /** Which Codex thread belongs to which chat, across restarts. */ threads: CodexThreadStore; /** * This agent's token. The folder's `.codex/config.toml` reads the hosted MCP * server's token from `RELAY_AGENT_TOKEN` (hosted-mcp.ts, `codexMcpServer`), * so `codex app-server` starts with it there. */ agentToken: string; /** Relay's hosted MCP server, handed to every thread (`codexThreadConfig`). */ mcpURL: string; /** Asks the agent's owners, in Relay, whatever Codex would ask at its terminal. */ approvals: Pick; signal: AbortSignal; /** One line to the terminal the person is watching. */ say(line: string): void; } /** * Answers every message that arrives until the signal stops it. * * Turns run one at a time inside a chat and at the same time across chats, * which is what one app-server with one thread per chat allows. A person's * message for a chat whose turn for a person is still running stops that turn * (`turn/interrupt`) and takes its place; the stopped turn sends nothing. An * agent's message waits for the running turn instead (`replacesLiveTurn`). */ export declare const runCodexBridge: (input: CodexBridgeInput) => Promise; //# sourceMappingURL=codex-bridge.d.ts.map