codexacp

package module
v0.0.0-...-1486a8d Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 23, 2026 License: MIT Imports: 37 Imported by: 0

README

acp-go-codex

acp-go-codex exposes the Codex CLI as an Agent Client Protocol agent. It runs one codex app-server for the agent, opens one Codex thread per ACP session on it, maps ACP requests onto the app-server protocol, and streams ACP session updates back to the client.

Codex inherits the adapter's environment and keeps its rollouts in its own home. One adapter runtime holds the home lock until its app-server has exited and been waited on. A session started over ACP can be continued natively:

acp-go-codex           # host runs a session
codex resume NATIVE_SESSION_ID

New, load, and resume responses and session-list entries expose the current native ID as _meta.codex.nativeSessionId. Use it for native CLI continuation. ACP requests continue to use the stable ACP sessionId. The store's configuration record saves both IDs with the matching native history.

Install

go install github.com/savid/acp-go-codex/cmd/acp-go-codex@latest

Verified against codex 0.154.0, found on PATH or named with -path.

Run

acp-go-codex [-path codex] [-home DIR] [-scratch-dir DIR] [-model MODEL] [-seed-file rel=host]... [-debug]
Flag Meaning
-path codex executable; a bare name is searched on PATH
-home Codex home, passed as CODEX_HOME; empty inherits Codex's own resolution
-scratch-dir additional read root for image output the harness wrote outside the workspace; this adapter allocates no ephemeral state
-model default model for new sessions
-seed-file <relpath>=<hostpath> written into Codex's home before the app-server launches; repeatable
-debug debug logs to stderr
-version print the adapter version

OpenTelemetry exporters are configured from the standard OTEL_* variables.

Embed

err := codexacp.Serve(ctx, os.Stdin, os.Stdout,
    codexacp.WithHome("/srv/codex"),
    codexacp.WithSessionStore(store),
)

Options: WithExecutablePath, WithHome, WithScratchDir, WithInputHandoffRoot, WithDefaultModel, WithConfiguredModels, WithEnv, WithCodexConfigOverrides, WithSeedFiles, WithSessionStore, WithConcurrencyLimits, WithImageLimits, WithLogger, WithTracerProvider, WithMeterProvider, WithTextMapPropagator, WithAgentName, WithAgentTitle, WithAgentVersion.

WithCodexConfigOverrides passes -c key=value to the app-server; the shell_environment_policy keyspace is reserved for session environments.

Session options

_meta.codex.options on session/new, session/load, and session/resume, or WithSessionCodexOptions from Go:

Field Meaning
model model for the session
env environment overlay for the session's thread
extraPathDirs absolute directories prepended to the thread's PATH, in order
outputSchema JSON schema every turn's final answer must satisfy; the parsed answer rides _meta.codex.structuredOutput on the prompt response
effort reasoning effort
serviceTier service tier
personality personality
approvalPolicy Codex's own approval policy, forwarded unchanged
sandboxPolicy Codex's own sandbox policy in either spelling; thread/start receives its mode and turn/start receives the object form

_meta.codex.rawEvent.enabled forwards every native app-server event for the session's thread on the _codex/rawEvent notification.

Packaged Codex installations prepend their own codex-path directory before session extraPathDirs when running local tools. The adapter uses the standard installed CLI and preserves this native behavior.

Config options

session/set_config_option accepts model, mode (default, plan), effort, service_tier, and personality. Values forward to the next turn; mode accepts only its own two values, and effort and personality reject an empty value.

Account usage

When model_provider names a provider with its own base_url, and that base publishes a model list at /v1/models, the session's model menu is that list in place of the app-server's presets, so every model is named with the upstream the gateway sends it to. If the list is unavailable or empty, the app-server presets remain available.

_codex/accountUsage reads the ChatGPT account's allowance windows through the shared app-server. providerId is required and selects openai-codex, anthropic, opencode-go, or openrouter; a provider config.toml or the launch overrides route through a gateway that publishes a usage report is read from that report with the key its env_key names. Initialize advertises it as _meta.codex.accountUsage with the value {"method": "_codex/accountUsage", "scope": "agent", "providers": ["openai-codex", "anthropic", "opencode-go", "openrouter"]}. The answer carries one limit per window present, keyed <key>/primary or <key>/secondary by the native limit key, with its used percent, length, and reset time and its observedAt, the account's plan type as plan, and the app-server's own ordinaryUsageAllowed as usageAllowed when it states one. An optional sessionId is validated but does not scope the read. A read on an idle Agent starts the app-server and takes the native-home lock exactly as a new session would. A home with no login answers {"available": false, "reason": "not_authenticated"}; an API-key or Bedrock login, or a ChatGPT account with no window, answers {"available": false, "reason": "not_reported"}.

Session store

WithSessionStore mirrors the thread's rollout rows under the main subpath and the adapter's session record under config, format codex-rollout-jsonl-v1. session/load and session/resume prefer the rollout in Codex's home when it is at least as long as the stored copy, adopt the rows it holds beyond it, and materialize the stored copy at the path the app-server resolves the thread id to otherwise. If Codex never persisted an empty thread, load and resume create a new native thread while retaining the ACP session id. The new binding is committed before the response. Nonempty history and other native failures never take this path. Native rows and session configuration commit as one store generation. A configuration change is durable even when no native rows were added. The same generation captures admitted generated and viewed image bytes under config, so image replay survives deletion of the original files. Invalid stored images fail load; an image the adapter refused at turn time replays as the failed tool call it was.

Development

make test
make lint
make audit
make test-integration-smoke   # needs codex installed, spends no tokens
make test-integration-live    # spends model tokens

Unit tests run the test binary as a scripted fake app-server and need no installed codex, credentials, or network.

Documentation

Overview

Package codexacp exposes the Codex CLI as an Agent Client Protocol agent.

Most hosts run the agent over a pair of JSON-RPC streams using Serve. Serve starts one `codex app-server` for the agent, opens one Codex thread per ACP session on it, maps ACP requests onto the app-server protocol, and streams ACP session updates back to the client. Codex inherits the adapter's environment and keeps its rollouts in its own home, so a session started over ACP can be continued natively with `codex resume` after the adapter closes.

Hosts that need durable remote resume provide WithSessionStore. The store is the durability boundary for session/list, session/load and session/resume; Codex's own rollout is the native copy an operator can continue outside ACP.

Hosts that need adapter telemetry provide OpenTelemetry providers with WithTracerProvider and WithMeterProvider; the package never configures global providers.

Index

Examples

Constants

View Source
const (
	// RawEventMethod is the notification carrying one raw app-server event
	// when a session opted in through _meta.codex.rawEvent.enabled.
	RawEventMethod = "_codex/rawEvent"
	// AccountUsageMethod is the request reading the logged-in account's
	// rate-limit windows through the shared app-server.
	AccountUsageMethod = "_codex/accountUsage"
	// SessionStoreFormat identifies the store layout this package writes: raw
	// Codex rollout rows under the main subpath plus the adapter's session
	// record under the config subpath.
	SessionStoreFormat = "codex-rollout-jsonl-v1"
)

Variables

This section is empty.

Functions

func Serve

func Serve(ctx context.Context, input io.Reader, output io.Writer, opts ...Option) (returnErr error)

Serve runs an ACP agent over the provided streams. It blocks until the context is cancelled or the peer closes the connection, then closes the agent.

Example (Initialize)

ExampleServe_initialize embeds the agent over a pair of pipes, the same wiring a host uses for stdio, and reads the capabilities the handshake advertises.

package main

import (
	"bufio"
	"context"
	"encoding/json"
	"fmt"
	"io"

	codexacp "github.com/savid/acp-go-codex"
)

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	clientToAgentReader, clientToAgentWriter := io.Pipe()
	agentToClientReader, agentToClientWriter := io.Pipe()

	done := make(chan error, 1)

	go func() {
		done <- codexacp.Serve(ctx, clientToAgentReader, agentToClientWriter)
	}()

	_, _ = fmt.Fprintln(clientToAgentWriter,
		`{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":1}}`)

	line, _ := bufio.NewReader(agentToClientReader).ReadString('\n')

	cancel()
	_ = clientToAgentWriter.Close()
	<-done

	var response struct {
		Result struct {
			AuthMethods       []any `json:"authMethods"`
			AgentCapabilities struct {
				LoadSession         bool           `json:"loadSession"`
				SessionCapabilities map[string]any `json:"sessionCapabilities"`
			} `json:"agentCapabilities"`
		} `json:"result"`
	}

	_ = json.Unmarshal([]byte(line), &response)

	fmt.Println(len(response.Result.AuthMethods))
	fmt.Println(response.Result.AgentCapabilities.LoadSession)
	fmt.Println(len(response.Result.AgentCapabilities.SessionCapabilities))
}
Output:
0
true
5

func SetModelRequest

func SetModelRequest(sessionID acp.SessionId, model string) acp.SetSessionConfigOptionRequest

SetModelRequest constructs a model selector update.

func ValidateCodexSessionMeta

func ValidateCodexSessionMeta(meta map[string]any) error

ValidateCodexSessionMeta runs the owned-namespace parsing of a session lifecycle request's _meta without an Agent and returns the same refusal.

func WithSessionCodexOptions

func WithSessionCodexOptions(options CodexOptions) wire.SessionRequestOption

WithSessionCodexOptions merges codex-specific options into _meta.codex.options.

func WithSessionOutputSchema

func WithSessionOutputSchema(schema map[string]any) wire.SessionRequestOption

WithSessionOutputSchema sets the JSON schema the turn's final answer must satisfy; it rides outputSchema on turn/start.

func WithSessionRawEvents

func WithSessionRawEvents(enabled bool) wire.SessionRequestOption

WithSessionRawEvents toggles raw codex event emission for the session.

Types

type Agent

type Agent struct {
	// contains filtered or unexported fields
}

Agent exposes the Codex app-server through ACP. One app-server serves every session; each session owns one thread on it.

func NewAgent

func NewAgent(opts ...Option) *Agent

NewAgent creates an ACP agent for the Codex CLI. Construction never fails; a refused option is reported by Initialize and every session-establishing method as codex_invalid_options.

func (*Agent) Authenticate

func (a *Agent) Authenticate(_ context.Context, params acp.AuthenticateRequest) (acp.AuthenticateResponse, error)

Authenticate exists because the SDK interface requires it. The harness authenticates itself in its own home, outside ACP.

func (*Agent) Cancel

func (a *Agent) Cancel(ctx context.Context, params acp.CancelNotification) (err error)

Cancel interrupts the session's in-flight turn. It is wire-silent on an unknown session or with no turn in flight.

func (*Agent) Close

func (a *Agent) Close() error

Close runs the shutdown ladder for every session, stops the shared app-server, and refuses every later request.

func (*Agent) CloseSession

func (a *Agent) CloseSession(ctx context.Context, params acp.CloseSessionRequest) (resp acp.CloseSessionResponse, err error)

CloseSession runs the shutdown ladder for one session.

func (*Agent) HandleExtensionMethod

func (a *Agent) HandleExtensionMethod(ctx context.Context, method string, params json.RawMessage) (any, error)

HandleExtensionMethod serves the account-usage read; every other extension method is method-not-found.

func (*Agent) Initialize

func (a *Agent) Initialize(ctx context.Context, params acp.InitializeRequest) (resp acp.InitializeResponse, err error)

Initialize implements ACP initialize.

func (*Agent) ListSessions

func (a *Agent) ListSessions(ctx context.Context, params acp.ListSessionsRequest) (resp acp.ListSessionsResponse, err error)

ListSessions lists live sessions and stored sessions, newest first.

func (*Agent) LoadSession

func (a *Agent) LoadSession(ctx context.Context, params acp.LoadSessionRequest) (resp acp.LoadSessionResponse, err error)

LoadSession restores a session and replays its history.

func (*Agent) Logout

func (a *Agent) Logout(_ context.Context, params acp.LogoutRequest) (acp.LogoutResponse, error)

Logout exists because the SDK interface requires it.

func (*Agent) NewSession

func (a *Agent) NewSession(ctx context.Context, params acp.NewSessionRequest) (resp acp.NewSessionResponse, err error)

NewSession starts a thread on the shared app-server.

func (*Agent) Prompt

func (a *Agent) Prompt(ctx context.Context, params acp.PromptRequest) (resp acp.PromptResponse, err error)

Prompt sends one turn to the thread and streams updates until it settles.

func (*Agent) ResumeSession

func (a *Agent) ResumeSession(ctx context.Context, params acp.ResumeSessionRequest) (resp acp.ResumeSessionResponse, err error)

ResumeSession restores a session without replaying its history.

func (*Agent) SetSessionConfigOption

func (a *Agent) SetSessionConfigOption(ctx context.Context, params acp.SetSessionConfigOptionRequest) (resp acp.SetSessionConfigOptionResponse, err error)

SetSessionConfigOption applies one select value.

func (*Agent) SetSessionMode

SetSessionMode exists because the SDK interface requires it. Native modes are config options, never ACP session modes.

func (*Agent) UnstableDeleteSession

func (a *Agent) UnstableDeleteSession(ctx context.Context, params acp.UnstableDeleteSessionRequest) (resp acp.UnstableDeleteSessionResponse, err error)

UnstableDeleteSession tombstones the session first, then closes any live session with the same id. The rollout stays in Codex's home.

type CodexOption

type CodexOption func(*CodexOptions)

CodexOption configures CodexOptions values.

func WithCodexApprovalPolicy

func WithCodexApprovalPolicy(policy any) CodexOption

WithCodexApprovalPolicy configures Codex's approval policy.

func WithCodexEffort

func WithCodexEffort(effort string) CodexOption

WithCodexEffort configures the reasoning effort.

func WithCodexEnv

func WithCodexEnv(env map[string]string) CodexOption

WithCodexEnv configures the session environment overlay.

func WithCodexExtraPathDirs

func WithCodexExtraPathDirs(dirs ...string) CodexOption

WithCodexExtraPathDirs configures the directories prepended to the session PATH.

func WithCodexModel

func WithCodexModel(model string) CodexOption

WithCodexModel configures the session model.

func WithCodexOutputSchema

func WithCodexOutputSchema(schema map[string]any) CodexOption

WithCodexOutputSchema configures structured output for every turn.

func WithCodexPersonality

func WithCodexPersonality(personality string) CodexOption

WithCodexPersonality configures the personality.

func WithCodexSandboxPolicy

func WithCodexSandboxPolicy(policy any) CodexOption

WithCodexSandboxPolicy configures Codex's sandbox policy.

func WithCodexServiceTier

func WithCodexServiceTier(tier string) CodexOption

WithCodexServiceTier configures the service tier.

type CodexOptions

type CodexOptions struct {
	// Model selects the Codex model for this session.
	Model string `json:"model,omitempty"`
	// Env overlays the session's thread environment.
	Env map[string]string `json:"env,omitempty"`
	// ExtraPathDirs are absolute directories prepended, in order, to the PATH
	// of this session's thread.
	ExtraPathDirs []string `json:"extraPathDirs,omitempty"`
	// OutputSchema is the JSON schema every turn's final answer must satisfy.
	OutputSchema map[string]any `json:"outputSchema,omitempty"`
	// Effort is the reasoning effort passed to Codex.
	Effort string `json:"effort,omitempty"`
	// ServiceTier is the service tier passed to Codex.
	ServiceTier string `json:"serviceTier,omitempty"`
	// Personality is the personality passed to Codex.
	Personality string `json:"personality,omitempty"`
	// ApprovalPolicy is Codex's own approval policy, forwarded unchanged.
	ApprovalPolicy any `json:"approvalPolicy,omitempty"`
	// SandboxPolicy is Codex's own sandbox policy, forwarded unchanged.
	SandboxPolicy any `json:"sandboxPolicy,omitempty"`
}

CodexOptions is the per-session options struct carried at _meta.codex.options.

func NewCodexOptions

func NewCodexOptions(opts ...CodexOption) CodexOptions

NewCodexOptions constructs CodexOptions from functional options.

Example
package main

import (
	"fmt"

	codexacp "github.com/savid/acp-go-codex"
)

func main() {
	options := codexacp.NewCodexOptions(
		codexacp.WithCodexModel("gpt-5.5"),
		codexacp.WithCodexEffort("high"),
		codexacp.WithCodexApprovalPolicy("on-request"),
	)

	fmt.Println(options.Model)
	fmt.Println(options.Effort)
	fmt.Println(options.ApprovalPolicy)
}
Output:
gpt-5.5
high
on-request

func (CodexOptions) Meta

func (options CodexOptions) Meta() map[string]any

Meta returns exactly {"codex": {"options": {...}}} with the non-zero fields.

type ConcurrencyLimits

type ConcurrencyLimits struct {
	MaxActiveSessions        int
	MaxConcurrentClientCalls int
}

ConcurrencyLimits controls per-agent backpressure. Zero fields use defaults.

type ImageLimits

type ImageLimits struct {
	MaxInputBytesPerImage     int64
	MaxInputBytesPerPrompt    int64
	MaxOutputBytesPerImage    int64
	MaxOutputBytesPerToolCall int64
}

ImageLimits bounds decoded image bytes. A zero field disables that policy limit; the frame clamp still applies.

type Option

type Option func(*Options)

Option configures the Codex ACP agent.

func WithAgentName

func WithAgentName(name string) Option

WithAgentName sets the protocol identifier advertised during ACP initialize.

func WithAgentTitle

func WithAgentTitle(title string) Option

WithAgentTitle sets the human-readable agent name advertised during ACP initialize.

func WithAgentVersion

func WithAgentVersion(version string) Option

WithAgentVersion sets the agent version advertised during ACP initialize.

func WithCodexConfigOverrides

func WithCodexConfigOverrides(overrides map[string]any) Option

WithCodexConfigOverrides passes config values to the app-server as -c key=value. Nothing is written to disk.

func WithConcurrencyLimits

func WithConcurrencyLimits(limits ConcurrencyLimits) Option

WithConcurrencyLimits sets process-local backpressure limits.

func WithConfiguredModels

func WithConfiguredModels(ids []string) Option

WithConfiguredModels names the models the host lists explicitly.

func WithDefaultModel

func WithDefaultModel(model string) Option

WithDefaultModel selects the model for new sessions.

func WithEnv

func WithEnv(env map[string]string) Option

WithEnv sets the static agent-scoped environment overlay applied to the app-server after the inherited environment.

func WithExecutablePath

func WithExecutablePath(path string) Option

WithExecutablePath selects the codex executable.

func WithHome

func WithHome(path string) Option

WithHome sets Codex's native home, passed to the app-server as CODEX_HOME.

func WithImageLimits

func WithImageLimits(limits ImageLimits) Option

WithImageLimits bounds decoded image bytes. A zero field disables that policy limit; a negative field fails construction.

func WithInputHandoffRoot

func WithInputHandoffRoot(dir string) Option

WithInputHandoffRoot sets the absolute directory under which handoff-form prompt images are read. The adapter never writes there.

func WithLogger

func WithLogger(logger *slog.Logger) Option

WithLogger configures structured diagnostic logging.

func WithMeterProvider

func WithMeterProvider(provider metric.MeterProvider) Option

WithMeterProvider configures the OpenTelemetry meter provider.

func WithScratchDir

func WithScratchDir(dir string) Option

WithScratchDir adds a root image output may be read from.

func WithSeedFiles

func WithSeedFiles(files map[string]string) Option

WithSeedFiles registers files written into Codex's home before the app-server launches. Keys are paths relative to that home.

func WithSessionStore

func WithSessionStore(store acpcore.SessionStore) Option

WithSessionStore configures the session store.

func WithTextMapPropagator

func WithTextMapPropagator(propagator propagation.TextMapPropagator) Option

WithTextMapPropagator configures trace-context extraction from ACP _meta.

func WithTracerProvider

func WithTracerProvider(provider trace.TracerProvider) Option

WithTracerProvider configures the OpenTelemetry tracer provider.

type Options

type Options struct {
	// AgentName is the protocol identifier advertised during ACP initialize.
	AgentName string
	// AgentTitle is the human-readable agent name advertised during ACP initialize.
	AgentTitle string
	// AgentVersion is the agent version advertised during ACP initialize.
	AgentVersion string

	// ExecutablePath selects the codex executable. A bare name is searched on
	// the base PATH; a path containing a separator is used as given. Empty
	// means "codex".
	ExecutablePath string
	// Home is Codex's native config, auth, and session root, passed to the
	// app-server as CODEX_HOME. Empty leaves Codex to resolve its home from the
	// inherited environment exactly as it would from a shell.
	Home string
	// ScratchDir is an additional root image output may be read from. The
	// adapter writes no ephemeral files of its own.
	ScratchDir string
	// InputHandoffRoot is the absolute directory under which handoff-form
	// prompt images are read. Empty rejects the handoff form.
	InputHandoffRoot string
	// DefaultModel selects the model for new sessions.
	DefaultModel string
	// ConfiguredModels are the model ids the host lists explicitly.
	ConfiguredModels []string
	// Env is the static agent-scoped overlay on the inherited process
	// environment the app-server runs with.
	Env map[string]string
	// CodexConfigOverrides are passed to the app-server as -c key=value.
	CodexConfigOverrides map[string]any

	// Logger receives structured diagnostic logs. If nil, the default logger is used.
	Logger *slog.Logger
	// TracerProvider records adapter spans. If nil, tracing is a no-op.
	TracerProvider trace.TracerProvider
	// MeterProvider records adapter metrics. If nil, metrics are no-ops.
	MeterProvider metric.MeterProvider
	// TextMapPropagator extracts trace context from ACP _meta. If nil, W3C
	// trace context plus baggage propagation is used.
	TextMapPropagator propagation.TextMapPropagator

	// SessionStore is the durability boundary for session rows. Nil installs a
	// fresh in-memory store.
	SessionStore acpcore.SessionStore
	// ConcurrencyLimits controls process-local backpressure.
	ConcurrencyLimits ConcurrencyLimits
	// SeedFiles maps paths relative to Codex's home to file contents written
	// there before the app-server launches.
	SeedFiles map[string]string
	// ImageLimits bounds decoded image bytes on prompt input and emitted
	// output. Every field defaults to 6 MiB when the option is omitted.
	ImageLimits ImageLimits
	// contains filtered or unexported fields
}

Options configures the ACP agent process and the shared app-server it starts.

Directories

Path Synopsis
cmd
acp-go-codex command
Package integration holds the tests that run against an installed Codex.
Package integration holds the tests that run against an installed Codex.
internal

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL