particle-academy / prism-acp
Agent Client Protocol for Claude and Codex CLIs from PHP, using subscriptions or app API keys. Windows preview with no third-party runtime dependencies.
Requires
- php: ^8.2
- ext-ctype: *
- ext-json: *
- ext-mbstring: *
Requires (Dev)
- laravel/pint: ^1.30
- pestphp/pest: ^3.0|^4.0
- phpstan/phpstan: ^2.0
- rector/rector: ^2.7
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-27 14:58:08 UTC
README
Speak the Agent Client Protocol to Claude or Codex from plain PHP or an application that uses PHP, through a signed-in subscription or an API key supplied by the app. The core has no third-party runtime dependencies and does not require Laravel.
Windows preview for Claude and Codex. Linux/macOS process containment is not implemented; those platforms are refused before a provider starts. Laravel applications can use the core classes; automatic service-provider discovery and an artisan command are not included. The pre-release API may change.
Install
composer require particle-academy/prism-acp:0.1.0-alpha.5
Requires PHP 8.2 or newer, ext-ctype, ext-json, ext-mbstring, Windows with the .NET Framework 4 C# compiler, and an installed,
Claude or Codex CLI. The shipped command uses its signed-in subscription; embedders can supply API keys as shown below.
The supervisor is compiled locally from the included C# source and hash-checked on every
launch. Node is needed only if the chosen installation is an npm shim; it is not a PHP runtime dependency.
Run as an ACP agent
Configure your ACP client to start this command with an absolute working directory:
php vendor/bin/prism-acp --cwd=C:/Projects/my-app --model=haiku php vendor/bin/prism-acp --provider=codex --cwd=C:/Projects/my-app
The client exchanges newline-delimited JSON on stdin/stdout. Diagnostics use stderr. --help and --version do
not launch a provider. Claude is the default. --claude-binary=C:/absolute/path/claude.exe and
--codex-binary=C:/absolute/path/codex.exe select particular installations. Unknown options
are refused. The server supports initialize, session/new, session/load, session/prompt, session/cancel
and session/set_model; model choices come from Claude's initialize response or Codex's model catalog.
The Claude CLI requests permission through the ACP client before every tool call. Only an explicit allow_once answer
allows the tool; cancellation, a missing handler, malformed replies and hook failures block it. Reasons accompany
both allow and deny. This is a tool-approval mechanism, not an operating-system sandbox: a permitted command has
the signed-in user's permissions. Use a trusted ACP client and working directory.
With permission prompts enabled, a stable Claude Code version of at least 2.1.295 is required; the native test pin
is 2.1.310. Prompts and EOF wait for its initialize version, and a missing or changed later system/init version
closes the session. The native exec-form hook allows only its script's validated explicit allow, and blocks silent
zero exits, invalid responses, crashes and timeouts. Its PHP child uses -n and fixed diagnostic settings, so hook
execution does not load the application's ini files or require optional extensions.
Codex uses on-request approval and its own sandbox default. Its five native command/file/permission approval
families map to the choices shown by the ACP client; only an offered choice can allow. Cancellation and missing,
invalid or failed handlers deny. Unsupported MCP elicitation is explicitly declined. Cancelling a turn interrupts
that turn and keeps the app-server and native thread available for another prompt. Loading a native thread replays
its paged transcript; Claude does not provide transcript replay through this driver.
Embed the agent
use Prism\Acp\Agent\{AcpAgent, DriverEvents, Stdio}; use Prism\Acp\Claude\{ClaudeDriver, ClaudeDriverOptions}; use Prism\Acp\JsonRpc\JsonRpcPeer; $base = new ClaudeDriverOptions( cwd: 'C:/Projects/my-app', model: 'haiku', permissionPrompts: ['*'], // every tool, including new tools and MCP tools ); $served = Stdio::serve(Stdio::stdin(), STDOUT, fn (JsonRpcPeer $peer) => new AcpAgent( peer: $peer, driverFactory: fn (array $session, DriverEvents $events) => new ClaudeDriver($base->forSession($session), $events), driverCapabilities: [...ClaudeDriver::CAPABILITIES, 'permissionRequests' => true], )); $served['closed']->await();
Load your application's Composer autoloader before this example. permissionPrompts can instead list individual
tool identifiers, such as ['Bash', 'Write']. Omitting it leaves permission handling to Claude's own configuration;
an embedding application must choose its policy deliberately. Explicit per-session MCP configuration is supported.
The hook interpreter defaults to PHP_BINARY only when the embedding SAPI is cli. For CGI, FPM, phpdbg,
CLI-server or another embedded SAPI, supply phpBinary: 'C:/absolute/path/php.exe' in trusted constructor options.
An implicit PATH lookup is refused. The package probes that executable as a working PHP CLI under containment
before creating the hook configuration. An explicit CGI/debugger executable is refused too. This changes the
previous non-CLI fallback; it does not add Linux/macOS provider containment. The optional trusted
permissionTimeoutSeconds ranges from 1 to 86400 (default 86400); the channel and native guard expire before the
CLI's hook timeout. Both options survive forSession() and cannot be supplied through ACP.
Concurrent Windows first launches publish digest-named supervisor generations before atomically replacing a small digest pointer. Every reader validates the pointer, filename and actual bytes. Existing generations remain available to sessions that selected them; a damaged cache is rebuilt once or refused. The cache filesystem must support hard links and atomic replacement; no uncontained fallback is offered. The cold-cache regression uses owned supervisor copies with inert assembly metadata to force distinct compiler generations. It records real compiler-output digests in every round and asserts that the production C# source stays unchanged; both the released publisher and corrected publisher face the same provider/approval oracle.
For Codex, use CodexDriverOptions and CodexDriver from Prism\Acp\Codex in the same factory, with
driverCapabilities: CodexDriver::CAPABILITIES. The constructor also accepts trusted approvalPolicy, sandbox,
mcpToolApproval and turnInactivityTimeoutMs options. never with danger-full-access removes approvals and
Codex's sandbox; choose those options only as host policy. The default ten-minute inactivity timer pauses while
native work or an approval is outstanding. Typed prompts accept text, images and text resources; images use private
temporary files removed when the driver stops.
Both option classes accept disabledMcpServers: ['inherited-name'] as trusted host policy. forSession() preserves
that policy and copies only the ACP cwd, explicit servers and resume id. Other inherited servers remain available.
Claude combines this deny list and its approval hooks in one private settings file. Codex reads effective inherited
transports before setting enabled: false; malformed configuration refuses startup rather than silently dropping policy.
Codex HTTP MCP bearer headers are rebound to a fresh variable in each child's environment and removed from thread
configuration. The same config disables shell snapshots and preserves effective shell policy while excluding precisely
those variables from model-run commands. Conflicting shell set assignments and malformed or mixed filter policies
refuse startup. This protects against an inherited bearer selector overriding an explicit session token. It is not
an operating-system credential vault: trusted native providers and code with the same user's access remain in the boundary.
App or tenant API keys
A system uses subscriptions or API keys. Choose this as trusted app/tenant configuration, independently of what an
ACP client sends. Omit credentials (or use new ProviderCredentials()) for the existing subscription behavior.
Both driver option classes accept the same credential object:
use Prism\Acp\Credentials\ProviderCredentials; use Prism\Acp\Codex\CodexDriverOptions; $base = new CodexDriverOptions( cwd: 'C:/Projects/my-app', credentials: new ProviderCredentials( mode: 'apiKey', apiKey: $tenantOpenAiKey, home: 'C:/AppState/tenant-42/codex', ), );
For Claude, pass the same object to ClaudeDriverOptions with the tenant's Anthropic key and its own Claude home.
forSession() preserves credentials and host policy; session/new, session/load and model output cannot replace
them. The shipped CLI has no key arguments. No environment variable in the PHP application is changed.
Keys must be non-empty and contain no ASCII control characters; the adapter imposes no prefix or length format. The home must be an absolute app/tenant path, separate from the user's default or moved subscription home, including case and filesystem aliases. A new home is restricted to the current user before launch; an existing home is accepted with the application's chosen permissions. Use the same home to resume its sessions. Separate homes have separate native settings, MCP servers, skills and history.
Claude receives the explicit key through ANTHROPIC_API_KEY and its home through CLAUDE_CONFIG_DIR. Codex receives
only CODEX_HOME in its environment, then the key through account/login/start; the adapter requires an API-key
acknowledgment before any thread starts or resumes. Refusal does not fall back to a subscription. Alternate-case
inherited home variables are replaced, and other inherited subscription-overriding credentials remain withheld.
Keys are absent from argv, default credential-object JSON/debug output and login diagnostics. The explicit Claude
key is redacted from stderr callbacks/storage across chunk boundaries, preserving other diagnostic text and withholding
a dangling key prefix at stream end. This follows the published TS0.25.1 fix for G-116; subscription diagnostics remain
unchanged. Native Codex stores
the key in auth.json inside that home; the native provider and code running as the same OS user can access it.
The optional source-checkout command php tools/native/credentials.php CODEX_EXE CLAUDE_EXE [INSTALLED_AUTOLOAD]
uses dummy keys and owned temporary homes with no model inference. It confirms native Codex account/key persistence
and Claude's authentication status and selected configuration directory,
preserved subscription sentinels and verified process cleanup. Measured versions are Codex0.163.0 and Claude2.1.308.
Claude's read-only status command runs inside the driver's Job Object through a fixture relay that inherits its exact
child environment. The script leaves its dummy fixture root for explicit cleanup by the invoking gate.
Process ownership and environment
Every provider and the Windows stdin relay enter a Job Object before their first instruction. Kill-on-close and
no breakaway cover descendants, including detached grandchildren. Losing the host's control connection terminates
the job even when the relay is blocked reading stdin. A stop returns {graceful, empty, survivors, verified};
verified: false means no supervisor observation was obtained and must not be reported as proven cleanup.
Commands use argument arrays and never a shell. Relative binary paths and arbitrary batch files are refused;
recognized npm shims resolve to Node plus their script. Child environments use an allowlist. Credentials that
override a CLI subscription, including ANTHROPIC_API_KEY and OPENAI_API_KEY, are withheld by default.
Temporary approval/MCP files have restricted permissions. The package emits no telemetry.
Verification and limits
Tests compare the PHP mapper's bytes against published @particle-academy/prism-acp 0.25.1, using real Claude capture
fixtures from its corresponding source commit, hostile mutations and 648 reader cases. Agent scenarios compare
requests, permissions and failures against that reference. CI regenerates these expectations and fails on drift.
Codex scenarios exercise the published driver's actual state machine through synthetic protocol frames, including
outbound RPC, updates, approvals, failures and MCP policy. They are synthetic scenarios, not native turn captures.
PHP behind TS: PARTIAL (6 contracts missing). The executed golden baseline remains 0.25.1; reference releases
through 0.32.0 have been reviewed. Missing contracts tracked by G-141 are 0.26 readRateLimits, 0.27 MCP server status,
0.28 AppContainer LOCAL, 0.29 nativeTools, 0.31 replay markers/opt-out, and G-140's 0.32 restricted startup flags.
The currency watchdog executes PHP API/cache-reader probes, refuses an unreviewed newer published version, and
refuses either a missing contract absent from this list or a newly exposed contract still listed as missing.
These conservative inventory probes require review of surface changes; native safety and reference conformance
are checked separately. The reviewed version advances only in a reviewed change, never automatically.
Windows CI runs fake-provider protocol and real process-tree tests, including a host hard kill while relay input stays open. Ubuntu checks the portable primitives and verifies that unsupported containment refuses before spawn. No CI job signs in to a provider or runs a live model turn. A prior local Windows Claude session and resume were dogfooded in Prism Labs; native Linux live verification has not been performed.
From a source checkout, the optional node tools/native/codex-mcp.mjs PHP_EXE CODEX_EXE [INSTALLED_AUTOLOAD]
control uses native Codex 0.163.0 with an owned
temporary home and deterministic loopback model/MCP responses. It checks start and resume, explicit bearer selection,
actual shell environment exclusion, selective denial, unrelated policy retention and verified process cleanup.
It uses no real credentials or paid inference. Snapshot disabling is asserted in the protocol oracle; PHP does not
claim native Linux snapshot verification. A newer native CLI requires reviewing this measured control version.
Pass a consumer installation's autoloader to exercise its installed classes rather than the checkout's classes.
Three JSON differences from JavaScript are intentional: lone UTF-16 surrogates become U+FFFD; NUL-led object keys are rejected; nesting is limited to 512. Binary resource prompts are not supported. The pinned reference's model catalog stops after twenty pages (G-99), and its initialize client metadata is hardcoded (G-100); this port preserves those shared limitations pending their coordinated fixes. Standalone session-store/model helper APIs and full TypeScript feature parity remain outside this cut.
Development checks:
composer install
composer test
composer types
php vendor/bin/pint --test
composer rector
composer audit
See the release procedure, transport details and the changelog. Report vulnerabilities privately through GitHub's security advisory mechanism.
License
MIT. See LICENSE.