Python client library for the Agent Client Protocol, with native CLI sessions and bounded connection pooling.
English | 简体中文
- Features
- Installation
- Quick Start
- Supported Agents
- Usage
- Session Management
- Configuration
- Architecture
- Testing
- Project Documents
- AIXP Labs
- License
- Alignment & Philosophy
- 30 CLI integrations: 29 ACP adapters plus one legacy Cursor CLI adapter; see Supported Agents
- Native CLI sessions: The provider CLI remains the source of truth; soulacp stores only session mappings and transport state
- No duplicated conversation history: Prompts are not replayed through a second middleware-owned transcript
- ManagedSession: High-level API with safe session reuse, explicit reset, startup retry, and fallback
- Bounded connection pooling: Session-safe reuse, FIFO backpressure, lifecycle retirement
- Session store: TTL-backed user→session mapping with memory or file storage
- Async streaming: Real-time response chunks via asyncio
- Safe retry: Exponential backoff for retryable control-plane failures; prompts are never blindly replayed
- Structured session context: Validated additional roots and stdio/HTTP/SSE MCP server definitions
- Host services: Configured-root file access and bounded ACP terminal process management
- Current ACP lifecycle: Capability-gated authentication, logout, session close, request cancellation, and config APIs
- Pure stdlib core: No required runtime dependencies; the OpenTelemetry API is optional
SoulACP requires Python 3.10 or later and at least one supported agent CLI or ACP bridge installed and, when required, authenticated.
python -m pip install soulacpThe source checkout can contain unreleased APIs documented in this README.
git clone https://github.com/AIXP-Labs/SoulACP.git
cd SoulACP
python -m pip install -e .
# Development and test tools
python -m pip install -e ".[dev]"# Published package
python -m pip install "soulacp[otel]"
# Current source checkout
python -m pip install -e ".[otel]"import asyncio
from soulacp import ManagedSession
async def main():
async with ManagedSession(provider="claude", model="claude-acp/sonnet") as session:
# Simple query — session auto-managed
response = await session.query("Hello!", user_id="user1")
print(response)
# Streaming
async for chunk in session.stream("Write hello world", user_id="user1"):
print(chunk, end="", flush=True)
asyncio.run(main())ManagedSession automatically handles:
- Session reuse: Same user_id gets same CLI session across requests
- Explicit reset:
reset_session(user_id)clears a reviewed stale/overflowed session without implicit prompt replay - Retry: Exponential backoff on safe, pre-prompt transient failures
- Fallback: Optional provider switch only when the prompt outcome is known not to be in progress
import asyncio
from soulacp import ACPConfig, ACPConnectionPool, resolve_client_class
async def main():
config = ACPConfig(provider="claude", model="claude-acp/sonnet")
client_class = resolve_client_class("claude")
async with ACPConnectionPool(config, client_class) as pool:
async with pool.acquire() as (client, session_id):
response = await client.query("Hello!")
print(response)
asyncio.run(main())| Agent | Provider | Model Example | Base Command |
|---|---|---|---|
| Claude Code | claude |
claude-acp/sonnet |
claude-agent-acp (claude-code-acp legacy) |
| Gemini | gemini |
gemini-acp/gemini-3-flash-preview |
gemini --acp |
| OpenCode | opencode |
opencode-acp/default |
opencode acp |
| OpenClaw | openclaw |
openclaw/default |
openclaw acp --no-prefix-cwd |
| Cursor (ACP) | cursor |
cursor-acp/default |
cursor-agent acp |
| Cursor (Legacy) | cursor-cli |
cursor-cli/gpt-4 |
cursor-agent -p |
| Codex | codex |
codex-acp/gpt-5.5 |
codex-acp |
| Qwen Code | qwen |
qwen-acp/qwen-coder |
qwen-code --acp --experimental-skills |
| Kimi CLI | kimi |
kimi-acp/default |
kimi acp |
| Codebuddy Code | codebuddy |
codebuddy-acp/default |
codebuddy-code --acp |
| Cline | cline |
cline-acp/default |
cline --acp |
| GitHub Copilot | copilot |
copilot-acp/default |
copilot --acp |
| Minion Code | minion |
minion-acp/default |
minion-code acp |
| Mistral Vibe | vibe |
vibe-acp/default |
vibe-acp |
| Nova | nova |
nova-acp/default |
nova acp |
| Crow CLI | crow |
crow-acp/default |
crow-cli acp |
| Amp | amp |
amp-acp/default |
amp-acp |
| Auggie | auggie |
auggie-acp/default |
auggie --acp |
| Autohand | autohand |
autohand-acp/default |
autohand-acp |
| Corust Agent | corust |
corust-acp/default |
corust-agent-acp |
| DeepAgents | deepagents |
deepagents-acp/default |
deepagents-acp |
| Factory Droid | droid |
droid-acp/default |
droid exec --output-format acp-daemon |
| fast-agent | fastagent |
fastagent-acp/default |
fast-agent-acp -x |
| Copilot LS | copilot-ls |
copilot-ls/default |
copilot-language-server --acp |
| Goose | goose |
goose-acp/default |
goose acp |
| Junie | junie |
junie-acp/default |
junie --acp=true |
| Kilo | kilo |
kilo-acp/default |
kilo acp |
| pi ACP | pi |
pi-acp/default |
pi-acp |
| Qoder | qoder |
qoder-acp/default |
qodercli --acp |
| Stakpak | stakpak |
stakpak-acp/default |
stakpak acp |
The base-command column is not an installation command. Adapters can append
model, policy, or authentication arguments at launch. Model availability
depends on the installed CLI, account, and authentication method.
The namespaced model ID provider-acp/default (and provider/default) leaves
model selection to the provider; SoulACP does not send default as a literal
model name or overwrite an existing provider model setting.
Registry helpers expose best-effort installation hints and inspect PATH
without starting provider processes:
from soulacp import list_agents, list_installed_agents
for agent in list_agents():
print(f"{agent.name:12} {agent.install_cmd}")
installed = [agent.name for agent in list_installed_agents()]
print("Installed:", installed)Install the current Claude and Codex ACP bridges with:
npm install -g @agentclientprotocol/claude-agent-acp @agentclientprotocol/codex-acpThe renamed Claude bridge is preferred; claude-code-acp remains a legacy
compatibility fallback. The Codex bridge includes its runtime, but the
codex login commands below require the Codex CLI on PATH:
npm install -g @openai/codexAccording to the official transition notice,
Gemini CLI stopped serving Google AI Pro, Ultra, and free individual accounts
on 2026-06-18. The gemini --acp adapter now requires a supported
Enterprise/Google Cloud or paid API authentication path; installing the CLI
alone does not grant individual-tier access.
import asyncio
from soulacp import ManagedSession
async def main():
async with ManagedSession(provider="claude", model="claude-acp/sonnet") as session:
response = await session.query("Hello!")
print(response)
asyncio.run(main())import asyncio
from soulacp import ManagedSession
async def main():
async with ManagedSession(provider="gemini", model="gemini-acp/gemini-3-flash-preview") as session:
response = await session.query("Hello!")
print(response)
asyncio.run(main())Codex ACP supports an eligible ChatGPT subscription or API-key authentication.
For local ChatGPT authentication, run codex login once; the Codex bridge then
uses ~/.codex/auth.json without requiring OPENAI_API_KEY.
import asyncio
from soulacp import ManagedSession
async def main():
# Example model; availability depends on the account and bridge version
async with ManagedSession(provider="codex", model="codex-acp/gpt-5.5") as session:
response = await session.query("Hello!")
print(response)
asyncio.run(main())Environment variables (all optional):
| Variable | Values | Default | Purpose |
|---|---|---|---|
CODEX_REASONING_EFFORT |
minimal / low / medium / high / xhigh |
medium |
Reasoning budget |
CODEX_SANDBOX_MODE |
read-only / workspace-write / danger-full-access |
(codex default) | File-system permission |
CODEX_APPROVAL_POLICY |
untrusted / on-request / never |
(Codex default) | Approval prompts |
CODEX_NETWORK_ACCESS |
true / 1 / yes |
not injected | Opt in to network in workspace-write; other values leave Codex config unchanged |
The following disables approval prompts, allows workspace writes, and enables network access. Use it only in a trusted workspace:
Bash:
export CODEX_SANDBOX_MODE=workspace-write
export CODEX_APPROVAL_POLICY=never
export CODEX_NETWORK_ACCESS=truePowerShell:
$env:CODEX_SANDBOX_MODE = "workspace-write"
$env:CODEX_APPROVAL_POLICY = "never"
$env:CODEX_NETWORK_ACCESS = "true"For API-key authentication, let Codex persist the selected authentication mode
instead of relying on an environment key alone. Assuming OPENAI_API_KEY is
already set:
# Bash
printf '%s' "$OPENAI_API_KEY" | codex login --with-api-key# PowerShell
$env:OPENAI_API_KEY | codex login --with-api-keyThe same model ID format is used, but model availability depends on the account and current Codex ACP bridge. Consult the official model catalog instead of relying on legacy aliases.
import asyncio
from soulacp import ManagedSession
async def main():
# Claude for code generation
async with ManagedSession(provider="claude", model="claude-acp/sonnet") as claude:
code = await claude.query("Write a sorting algorithm in Python")
# Gemini for code review
async with ManagedSession(provider="gemini", model="gemini-acp/gemini-3-flash-preview") as gemini:
review = await gemini.query(f"Review this code:\n{code}")
print(review)
asyncio.run(main())ManagedSession uses ProviderSessionStore internally to map (user_id, provider) to CLI session IDs:
import asyncio
from soulacp import ManagedSession
async def main():
async with ManagedSession(provider="claude", model="claude-acp/sonnet") as session:
# First request — creates a CLI session and stores the mapping
await session.query("Remember 42.", user_id="alice")
# Second request — reuses Alice's CLI session
await session.query("What number?", user_id="alice")
# A different user receives a different logical session
await session.query("Hello!", user_id="bob")
asyncio.run(main())import asyncio
from soulacp import ManagedSession, ProviderSessionStore, FileCache
async def main():
store = ProviderSessionStore(cache=FileCache("~/.soulacp/sessions.json"))
async with ManagedSession(
provider="claude",
model="claude-acp/sonnet",
session_store=store,
) as session:
await session.query("Remember 42.", user_id="alice")
asyncio.run(main()) # Closing the session flushes pending file-cache writes| Event | Behavior |
|---|---|
| Successful first request | New CLI session created, mapping stored after pool release (TTL 7 days) |
| Subsequent requests | Same user_id → same CLI session (reuse) |
| Concurrent requests for one user/session | Serialized within one ManagedSession instance; different sessions can still run concurrently |
| Context overflow | Raise to caller; call reset_session(user_id) only after deciding whether to submit a new turn |
| Confirmed stale stored session | Clear that provider mapping once, then explicitly create a fresh session |
| Pre-prompt connection/timeout error | Preserve the mapping and retry the same session within the configured attempt budget |
| Prompt transport/RPC failure | Raise without retry or fallback because side effects may already exist |
| Fallback | Switch provider only before prompt execution (e.g. startup failure); reuse that provider's own stored session |
from soulacp import MemoryCache, FileCache
# In-memory (default) — fast, lost on restart
memory = MemoryCache(max_size=10000)
# File-based — persists across restarts
file_cache = FileCache("~/.soulacp/sessions.json", debounce_seconds=1.0)File-cache paths expand ~; writes use an atomic temp-file replacement and
ManagedSession.close() flushes pending debounced mutations. User IDs are
stored as fixed-width pseudonymous SHA-256 key components, with read-time
migration only for unambiguous legacy plaintext keys. Numeric IDs, lowercase
10/32-character hexadecimal IDs, and delimiter-ambiguous legacy identities are
not auto-migrated because the historical formats cannot prove ownership;
affected mappings must be re-established. One ProviderSessionStore serializes
its compound lookup, migration, update, clear, and flush operations so a legacy
migration cannot overwrite a newer mapping. Coordinating multiple store or
cache instances remains the external backend's responsibility. FileCache normalizes values to
their standard JSON representation before publishing them and returns detached
read snapshots, so later caller mutation cannot change persisted state. Cache
keys must be valid UTF-8 strings; TTLs must be non-negative integers within the
finite runtime range. FileCache is a single-owner backend: do not
open the same path from multiple FileCache instances or processes.
Deployments with multiple owners should provide a coordinated external
CacheBackend. Stored session IDs are not encrypted, so protect the cache file
with appropriate OS permissions and do not share it.
import asyncio
from soulacp import ACPConnectionPool, ACPConfig, resolve_client_class
async def main():
config = ACPConfig(provider="claude", model="claude-acp/sonnet")
client_class = resolve_client_class("claude")
async with ACPConnectionPool(config, client_class) as pool:
# Explicit session reuse
async with pool.acquire() as (client, sid):
await client.query("Remember 42.")
async with pool.acquire(session_id=sid) as (client, sid2):
assert sid2 == sid
response = await client.query("What number?")
print(response)
# Force a new transport without bypassing the hard connection limit
async with pool.acquire(session_id=sid, fresh=True) as (client, sid3):
assert sid3 == sid
stats = pool.get_stats() # Non-blocking, no health-check I/O
print(stats)
asyncio.run(main())Native ACPClientBase adapters retain the agent's advertised capabilities,
implementation info, authentication methods, and current session config in
agent_capabilities, agent_info, auth_methods, and
session_config_options; session_mode_state tracks advertised modes and the
current selection. Stable session_info_update, usage_update, and
available_commands_update notifications are validated into session_info,
session_usage, and available_commands. These five session-owned states are
cleared together for a new/load/resume transition and restored together if the
transition fails. Every stable ACP v1 session-update shape, including content
blocks, tool calls, and plans, is structurally validated before state mutation
or callback delivery. set_update_callback() remains the raw, receive-ordered hook
for every update, including tool calls, plans, and thought chunks that are not
flattened into response text. Capability-gated list_sessions() and
delete_session() cover ACP session-history management; set_config_option()
updates flat or grouped select and boolean options, while set_mode() switches
an advertised legacy mode for backward compatibility. ACP is moving dedicated
mode methods into session config options, so category mode config options are
preferred when an agent advertises them. Configured models similarly use an
advertised category model select option; an already-selected value emits no
RPC, and the non-standard session/set_model method is only a compatibility
fallback when no model option is advertised. Agent-pushed config_option_update
and current_mode_update events keep both states current. authenticate(),
logout(), and close_session() expose the current ACP lifecycle methods and
reject calls not advertised by the agent. Successful logout or closure of the
active session invalidates pool reuse, so releasing that lease retires the
transport. Session load/resume performs no transport I/O and reports that the
stored session cannot be restored unless the agent advertises loadSession or
sessionCapabilities.resume; managed sessions then clear that unusable mapping
once and start fresh. The standalone
legacy CursorCLIClient exposes only the minimal ACPClient protocol.
Only a leading <provider>-acp/ or <provider>/ namespace is removed from a
configured model. Nested IDs such as claude-acp/openai/gpt-5 therefore remain
openai/gpt-5. When an executable explicitly returns JSON-RPC -32601 for the
legacy session/set_model fallback, soulacp warns once and caches that result
for the same adapter/executable; later clients use the provider default without
repeating the unsupported request.
| Variable | Default | Description |
|---|---|---|
ACP_PROVIDER |
claude |
Provider name |
ACP_MODEL |
claude-acp/sonnet |
Model identifier |
ACP_CWD |
Current directory | Working directory for the agent subprocess |
ACP_POOL_SIZE |
5 | Maximum retained idle connections |
ACP_POOL_MAX_CONNECTIONS |
8 | Hard open + creating limit per ACPConnectionPool instance |
ACP_POOL_ACQUIRE_TIMEOUT |
30 | FIFO capacity wait timeout (seconds) |
ACP_POOL_IDLE_TIMEOUT |
600 | Seconds since release before idle retirement |
ACP_POOL_KEEPALIVE_INTERVAL |
300 | Idle local transport-state check interval; 0 disables |
ACP_POOL_MAX_AGE |
7200 | Soft connection age limit at lease boundaries |
ACP_POOL_MAX_USES |
50 | Soft successful-lease limit |
ACP_POOL_CROSS_SESSION_REUSE |
false | Permit explicit resume on a transport bound to another session |
ACP_TIMEOUT_CONNECT |
30 | Connection timeout (seconds) |
ACP_TIMEOUT_PROMPT |
21600 | End-to-end prompt deadline (seconds, 6 hours) |
ACP_TIMEOUT_STREAM |
1800 | No-ACP-activity stream timeout (seconds, 30 minutes) |
ACP_INHERIT_PARENT_ENV |
true | Inherit the full parent environment in provider and ACP terminal children |
ACP_AUTO_APPROVE |
true | Prefer allow_once; false prefers reject_once |
ACP_ENABLE_FALLBACK |
false | Enable pre-prompt provider fallback |
ACP_MAX_RETRIES |
3 | Total pre-prompt attempt budget; 0 still performs one attempt |
ACP_RETRY_BASE_DELAY |
1 | Retry backoff base (seconds) |
ACP_MAX_TURNS |
unset | Optional Claude-only maxTurns override |
ACP_OTEL_PROPAGATE |
0 | Inject active W3C traceparent/tracestate at subprocess startup when set to 1; baggage is excluded |
ACP_LOG_SENSITIVE_DATA |
0 | Set exactly to 1 to opt into raw provider diagnostics in debug/error logs |
Security:
ACP_AUTO_APPROVE=trueautomatically chooses an allow option (allow_oncepreferred) when the agent requests permission. This is not an OS sandbox. Set it tofalsefor untrusted workspaces and review the provider CLI's own permission model.
pool_size is not a concurrency limit. Total physical capacity is bounded by
pool_max_connections for each pool instance. Each ManagedSession creates
one pool per provider:model key, so N keys in one instance can reserve up to N
times the per-pool limit; multiple ManagedSession instances multiply this
again. There is no process-wide limiter. With
pool_cross_session_reuse=False, an anonymous request never receives an idle
client already bound to another logical session. This favors isolation and
makes the pool primarily a session-affinity cache. A requested session ID can
have only one live lease at a time. ACPConnectionPool validates and snapshots
its ACPConfig when constructed, and each physical client receives a separate
snapshot. Later caller or client mutation therefore cannot change the pool's
validated capacity and lifecycle policy; construct a new pool to apply policy
changes.
Boolean environment variables accept only true/false, 1/0, yes/no, or
on/off; invalid values raise ValueError instead of silently disabling a
feature. OpenClaw does not receive --session by default, so each ACP bridge
uses its isolated session key. Setting OPENCLAW_SESSION_KEY deliberately opts
into OpenClaw's shared/attached session routing.
| Variable | Adapter | Purpose |
|---|---|---|
MOONSHOT_API_KEY |
Kimi | API-key authentication |
MISTRAL_API_KEY |
Vibe | API-key authentication |
OPENCLAW_URL |
OpenClaw | Gateway URL |
OPENCLAW_TOKEN_FILE |
OpenClaw | Token authentication override via a protected file |
OPENCLAW_PASSWORD_FILE |
OpenClaw | Password authentication override via a protected file when no token file is configured |
OPENCLAW_SESSION_KEY |
OpenClaw | Explicitly attach/share a Gateway session; unset preserves isolated acp-bridge:<uuid> routing |
OPENCLAW_VERBOSE |
OpenClaw | Enable verbose bridge logging with true, 1, or yes |
OPENCLAW_URL must be an absolute ws:// or wss:// URL without embedded
credentials. Plain OPENCLAW_TOKEN and OPENCLAW_PASSWORD launch overrides
are rejected: OpenClaw accepts them only as command-line values, where local
process tools can read them. Use configured Gateway authentication or a
protected credential file instead.
from soulacp import ACPConfig
config = ACPConfig(
provider="claude",
model="claude-acp/sonnet",
pool_size=5,
pool_max_connections=8,
pool_acquire_timeout=30,
pool_idle_timeout=600,
pool_keepalive_interval=300,
pool_max_age=7200,
pool_max_uses=50,
pool_cross_session_reuse=False,
timeout_connect=30,
timeout_prompt=21600,
timeout_stream=1800,
inherit_parent_env=True,
auto_approve_permissions=True,
enable_fallback=False,
additional_directories=["/absolute/path/to/shared"],
mcp_servers=[
{
"name": "local-tools",
"command": "/absolute/path/to/mcp-server",
"args": [],
"env": [],
}
],
)extra_args is appended to the subprocess argv without shell expansion and is
visible to local process-inspection tools. Never put credentials in it.
extra_env is merged into the subprocess environment and overrides duplicate
keys. Treat both as trusted configuration; do not log them indiscriminately.
ACPConfig representations omit the working directory, additional roots, MCP
definitions, extra arguments, and environment overrides to reduce accidental
log exposure. The fields remain directly accessible and must still be handled
as sensitive configuration.
Provider and ACP terminal subprocesses inherit the parent environment by default
for CLI authentication and compatibility. Set inherit_parent_env=False (or
ACP_INHERIT_PARENT_ENV=false) to retain only a bounded runtime/path/locale
allowlist; then supply required credentials explicitly through extra_env or the
provider's own credential store. This limits environment propagation but is not
an OS sandbox. additional_directories and mcp_servers
are structured constructor-only fields; they intentionally have no environment
string parser. Roots and stdio commands must be absolute. HTTP/SSE MCP servers
require absolute HTTP(S) URLs without embedded credentials; use validated
header entries for authentication. MCP metadata must already use strict JSON
types with string object keys; tuples and non-string keys are rejected instead
of being silently coerced. Remote MCP servers and additional roots are
sent only when the agent advertises the corresponding
capability, otherwise session creation fails before an unsupported request is
sent.
import asyncio
from soulacp import ACPConfig, ManagedSession
async def main():
config = ACPConfig.from_env()
async with ManagedSession(
provider=config.provider,
model=config.model,
config=config,
) as session:
print(await session.query("Hello!"))
asyncio.run(main())ManagedSession requires provider and model. When config is omitted, it
loads the remaining settings with ACPConfig.from_env(), but the constructor's
provider and model override ACP_PROVIDER and ACP_MODEL. When a config is
provided, all other fields are preserved without hidden environment overrides;
the constructor still replaces config.provider and config.model. The
pattern above uses the environment-selected provider and model explicitly.
Prompt and stream deadline failures close the underlying transport and are not
automatically retried or sent to a fallback provider. This prevents requests
with an unknown outcome or partial output from being replayed.
For streaming APIs, time spent by the caller processing an already-yielded
chunk is not charged again by the generator-side prompt deadline. A native
ACP session/prompt RPC that is still in flight retains its original
end-to-end deadline.
When an individual outbound RPC is cancelled or times out, soulacp first sends
the standard best-effort $/cancel_request notification. Incoming
$/cancel_request notifications cancel matching host requests before their
reply starts and return JSON-RPC code -32800; prompt timeout cleanup still
retires the transport so a late response cannot leak into a later lease.
Native client connect/disconnect operations are serialized. Prompt/stream and
session load/resume operations are also serialized per client so a completed
old prompt cannot overwrite a newly selected session. Concurrent callers cannot
publish duplicate subprocesses, disconnect waits for an in-flight connect, and
owned cleanup completes before caller cancellation is propagated. Pool lease
release and retirement use the same ownership rule even when cancellation
arrives while cleanup is waiting for the pool lock, so capacity cannot remain
stranded in a leased state.
Bounded user-cancellable waits also preserve caller cancellation on Python 3.10
when the awaited operation completes in the same event-loop turn.
Legacy Cursor query cleanup likewise owns child-process reaping through repeated
caller cancellation. It also bounds output-pipe drain after direct process exit
and explicitly closes exited subprocess transports. A failed terminate/kill never releases physical pool
capacity: the client or terminal record remains owned, close() reports the
failure, and a later close() retries it. ManagedSession retains only the
pools and store flushes that failed, so successful cleanup is not repeated.
Native asyncio transports and OpenCode apply the same rule to a subprocess that
arrives after startup timeout: the late handle is claimed, reaped, or retained
for a later cleanup retry. RPC waits also observe direct subprocess exit while
preserving a bounded window for buffered tail frames, so an inherited stdout
pipe cannot hold a request until its full configured deadline. OpenCode
reader-thread callbacks are bound to the dispatch queue that scheduled them, so
a delayed callback from a disconnected transport cannot enter a replacement
transport generation.
Idle health checks inspect local process and pipe state without injecting probe
bytes into the newline-delimited JSON-RPC transport.
ACP v1 initialization must negotiate protocol version 1, and every advertised
authentication method must have a non-empty agent-provided id; soulacp never
synthesizes one. Known advertised capability fields and every stable v1 RPC
response are validated before local session, mode, or configuration state is
committed. Response and known nested _meta values must be objects or null;
supported host requests apply the same metadata contract and return -32602
for malformed metadata. ACP v1 permission requests must include a valid
toolCall and options with non-empty IDs, names, and standard kinds before
soulacp selects one. Explicit pre-v1 ID-only options remain a compatibility
path when every option omits kind. ACP stdout and legacy Cursor JSON stdout are decoded as
strict UTF-8, and server request IDs accept the protocol's null, int64, and
string forms while responses remain correlated to soulacp's int64 integer IDs.
Externally decoded JSON also rejects duplicate object keys so fields cannot be
silently replaced during parsing. Inbound and outbound JSON reject invalid
Unicode and non-finite numbers instead of emitting implementation-specific JSON;
OpenCode's inherited OPENCODE_CONFIG_CONTENT must likewise be a strict JSON
object. Prompt completion is accepted only from the correlated
session/prompt response with a standard
stopReason (end_turn, max_tokens, max_turn_requests, refusal, or
cancelled). Message-end update extensions do not complete a turn. Incoming
session updates and all supported host requests must identify the current session;
stale cross-session messages fail closed. Transport loss after the prompt write
raises ACPRequestOutcomeUnknownError; malformed completion raises
ACPProtocolError. Any RPC interrupted after its write may have reached the
agent, so the transport is retired after best-effort $/cancel_request rather
than being returned to the pool with potentially divergent session state.
ACP filesystem requests honor the v1 absolute path; schema-boundary line=0
maps to the first 1-based line, and limit=0 returns no lines. Requests are
contained to cwd plus validated additional_directories and bound file, text,
write, and directory sizes. Reads accept regular files only and enforce the byte
limit on the opened stream, including files that grow after path validation.
This is best-effort path containment under stable filesystem topology, not
race-resistant symlink protection or an OS sandbox.
Terminal working directories use the same configured roots; requests use
no-shell argv execution, bounded output (including
outputByteLimit=null or 0), the standard truncated result flag, and a limit
of 32 unreleased or late-spawning terminal records and 32 concurrent
terminal-wait host requests per client service. A cancelled terminal create
retains its capacity reservation until the late child is reaped. SoulACP owns
the direct child process, and terminal wait follows that child's exit with only
a bounded output-drain window; use OS job/container
isolation when descendant-process cleanup must be guaranteed.
get_stats() retains the original keys and adds closed/idle/active/checking/
closing state, creating reservations, unreaped transports, waiters, claimed
sessions, capacity, creation/disconnect failures, cleanup retries, session
hits, resume results, keepalive loop failures, evictions, lifecycle peaks, and a policy snapshot
including the effective startup-attempt budget.
Metrics use only low-cardinality attributes; session IDs, prompts, user IDs,
paths, and PIDs are never metric attributes. Built-in provider names are kept;
unknown/custom provider labels are collapsed to custom in metrics and spans,
and unrecognized pool-dimension values are collapsed to other.
soulacp uses only the OpenTelemetry API. Applications remain responsible for configuring the SDK, readers, exporters, and backend. Without the optional API package, all pool instrumentation is a no-op. Instruments are process-wide; metrics and RPC spans omit session IDs, prompts, error text, user IDs, paths, and PIDs. Telemetry backend failures are isolated and cannot replace operation results or exceptions.
Default SoulACP logs and public exception strings redact provider messages,
RPC data, session IDs, stderr, user IDs, and cache paths. RPCError and
ACPProviderError retain raw fields for programmatic diagnosis; raw text is
rendered only through format_diagnostic(include_sensitive=True). Setting
ACP_LOG_SENSITIVE_DATA=1 additionally enables raw provider diagnostics in
library logs and should be limited to short-lived, access-controlled debugging.
ProviderSessionStore uses memory by default. An explicitly configured
FileCache stores provider session IDs unencrypted and uses deterministic
pseudonymous user-key hashes; protect the file with OS permissions and do not
share it across trust domains. Only unambiguous legacy plaintext cache keys are
migrated when accessed; ambiguous mappings fail closed and must be
re-established.
Legacy Cursor CLI prompts are sent through stdin, not argv. Cursor's documented
resume interface still requires the session ID in --resume, and explicit
OPENCLAW_SESSION_KEY values are likewise command-line metadata. Prefer the
native Cursor ACP adapter when process-list privacy is required. SoulACP agent
subprocesses run as the current OS account and inherit the parent environment
by default; minimal-environment mode does not change that OS trust boundary.
This library is not a credential sandbox.
ManagedSession (high-level API)
├── ProviderSessionStore (user→session mapping)
│ └── CacheBackend (MemoryCache / FileCache)
├── ACPConnectionPool (bounded reuse + FIFO backpressure + lifecycle)
│ └── ACPClientBase (JSON-RPC over stdio subprocess)
│ ├── 29 ACP adapter implementations (see Supported Agents)
│ └── CursorCLIClient (legacy non-ACP integration)
└── Services
├── FSService (file system operations)
└── TerminalService (no-shell argv execution, live bounded output, kill/release)
# Default suite (provider CLI and model calls are skipped)
python -m pytest tests/ -v
# Integration tests (requires installed and authenticated CLIs)
python -m pytest --run-integration tests/test_integration.py -v # Claude Code
python -m pytest --run-integration tests/test_integration_gemini.py -v # Gemini
python -m pytest --run-integration tests/test_integration_codex.py -v # Codex
# All unit + integration + stdio stress tests
python -m pytest --run-integration tests/ -v
# Static checks used by CI
python -m ruff check src/soulacp/ tests/ examples/
python -m ruff format --check src/soulacp/ tests/ examples/
python -m pyright
# Bash; in PowerShell, set `$env:PYTHONPATH = "src"` first.
PYTHONPATH=src python -m pyright --verifytypes soulacp --ignoreexternal
python -m bandit -q -r src/soulacp
# Build both source and wheel distributions
python -m buildFiles named test_integration*, test_spike_*, and test_stress_* require
--run-integration; the default suite intentionally skips them.
| Document | Purpose |
|---|---|
| CHANGELOG.md | Release history and behavioral changes |
| SECURITY.md | Security policy and vulnerability reporting |
| CONTRIBUTING.md | Contribution workflow |
| GOVERNANCE.md | Project governance and decision-making |
| CODE_OF_CONDUCT.md | Community standards |
AIXP Labs aixp.dev
AIXP Labs develops and maintains the following core projects:
| Project | Description | Website |
|---|---|---|
| HSAW | Human Sovereignty and Wellbeing — Axiom 0 white paper (foundation) | hsaw.dev |
| AIZP | AI Zenith-Zero Protocol — runtime behavioral alignment | aizp.dev |
| AILP | AI List Protocol — agent discovery and capability advertising | ailp.dev |
| AIVP | AI Value Protocol — international commerce, crypto asset settlement | aivp.dev |
| AIRP | AI RMB Protocol — Mainland China commerce, RMB licensed settlement | airp.dev |
| AIBP | AI Bot Protocol — social communication and trust | aibp.dev |
| AIAP | AI Application Protocol — governance and compliance | aiap.dev |
| AISP | AI Skill Protocol — single-file skills with machine-enforced contract red lines | aisp.dev |
| AISOP | AI Standard Operating Protocol — flow program definition | aisop.dev |
| SoulSkill | AISP skill reference library & multi-CLI plugin distribution | soulskill.dev |
| SoulAgent | Drop-in AI agent invoked directly by any CLI / SDK / IDE | soulagent.dev |
| SoulBot | AI agent runtime & orchestration framework (scheduling, agent-spawn, inter-agent comms) | soulbot.dev |
| SoulACP | Adapter library — bridging CLI tools and LLM providers (this project) | soulacp.dev |
This software is experimental and provided for research and educational purposes only. Not intended for production use. Use at your own risk. The authors assume no liability for any damages arising from the use of this software. See LICENSE for full terms (Apache 2.0).
Apache License 2.0 - Copyright 2026 AIXP Labs AIXP.dev | SoulACP.dev
Align Axiom 0: Human Sovereignty and Wellbeing. Version: SoulACP V0.2.0. www.soulacp.dev
- No HITL, HSAW. Human Sovereignty and Wellbeing is Axiom 0, requiring no hypocritical human-in-the-loop.
- No w.a.s.h, Real h.s.a.w.
- 人非蝼蚁,人为道。
- We are not beggars. We the People.