← Tools Proxy | Process Pool →
Docs: Getting Started · Tutorial · Configuration · Agents · API Reference · Pipelines · Async Jobs · Webhooks · Client Usage · Tools Proxy · Security · Process Pool · Lambda Burst · Jev Router · Testing · Troubleshooting
Before you expose this to the internet, read this entire page.
Agents run as full subprocesses of the Bridge host, with the Bridge user's permissions. Most agents can execute shell commands, read/write files, and reach any network the host can reach.
- Token compromise ≈ shell compromise. Anyone with
ACP_TOKENcan tell an agent torm -rf, exfiltrate~/.ssh, or hit internal services. Rotate tokens, never commit them, and scopeallowed_ipstightly. --trust-all-toolsauto-approves every tool call. Kiro's default config includes this flag — remove it in untrusted networks.session/request_permissionis auto-answered withproceed_alwaysso Claude doesn't hang. Same implication: anything the agent wants to do, it gets to do. Filesystem access is bounded by the Filesystem Sandbox (below); shell/network access is not.- Prompt injection is a real vector. Untrusted content fed to an agent (web pages, user input, log files) can hijack it into running unintended commands.
Bridge uses dual authentication:
- Bearer Token —
Authorization: Bearer <token>header on every request - IP Allowlist — only requests from
security.allowed_ipsare accepted
Both must pass. /live, /ready, /health, and /ui are unauthenticated (for load balancer probes and browser access). The public Agent Card and A2A routes use their documented mesh-token policy. The IP allowlist still applies to every path.
Token supports ${ENV_VAR} references in config — keep actual values in .env or environment only. If security.auth_token resolves to an empty value, Bridge refuses to start instead of silently disabling authentication.
The same fail-closed rule applies to mesh.token: /a2a and /a2a/announce are exempt from the global Bearer check by design (they authenticate on the separate mesh plane instead — see A2A Mesh → Security Model), so if mesh.enabled: true and mesh.token resolves empty, Bridge refuses to start rather than leaving those two endpoints open with no authentication at all.
Verbose Bridge logging suppresses credential-bearing AWS SDK internals so temporary IAM session headers are not written to the service journal.
File and Pipeline artifact downloads require the normal Bearer token. The LiteLLM usage callback does not require the Bridge token, but accepts requests only from loopback clients (127.0.0.0/8 or ::1).
Agents talk to the Bridge over ACP stdio JSON-RPC and can issue fs/read_text_file / fs/write_text_file requests with an arbitrary path. The client-supplied cwd (via /runs metadata and pipeline shared_cwd) is also attacker-controllable. Without bounds, any agent — or a prompt-injected one — could read host secrets (.env, ~/.aws/credentials) or write anywhere.
The sandbox maps each agent to a trust level via its trust config field:
| Level | Name | Filesystem access | On violation |
|---|---|---|---|
| 0 (default) | sandboxed |
Only within an allowed root: the agent's working_dir, public_workdir, the pipeline workspace base, upload_dir, plus any sandbox.allowed_roots |
Denied |
| 1 | workspace |
Anything except the security-sensitive blacklist (~/.aws, ~/.ssh, /etc, **/.env, **/*.pem, …) |
Denied if blacklisted |
| 2 | unrestricted |
Any path | Allowed; startup warning + every op audited |
Two admission checks, both resolving realpath first so ../ traversal and symlink escape are caught, and matching on path components (so /tmp/acp does not falsely contain /tmp/acp-public):
- cwd admission — a client-supplied
cwd/shared_cwdmust itself be legal for the agent's level, socwd=/cannot bypass level-0 bounds. An emptycwdfalls back to the configworking_dir. - fs admission — each read/write path is checked against the level. The connection's own
cwdis an implicit allowed root at level 0, so legitimate pipelineshared_cwdwork is never blocked.
Configure per agent in config.yaml (unset = level 0):
agents:
kiro:
working_dir: "/home/me/projects/acp-bridge"
trust: "workspace" # level 1 — free access except the blacklist
sandbox:
enabled: true
allowed_roots: [] # extra level-0 roots beyond working_dir + shared dirs
# blacklist: [...] # omit to use the built-in default (src/sandbox.py)Level 2 is never the default and must be set explicitly. When any agent is
unrestricted, the Bridge logs aSECURITY:warning at startup.
Every fs read/write and every rejected cwd is recorded in the fs_audit SQLite table (co-located in data/jobs.db, best-effort, never blocks the agent). Each row captures ts, agent, trust_level, session_id, operation, path, cwd, size, outcome, deny_reason. Query it:
# All denied fs attempts in the last hour
curl -s "http://localhost:18010/admin/fs-audit?outcome=denied" \
-H "Authorization: Bearer $ACP_BRIDGE_TOKEN"
# Everything a specific agent touched
curl -s "http://localhost:18010/admin/fs-audit?agent=kiro&limit=200" \
-H "Authorization: Bearer $ACP_BRIDGE_TOKEN"Filters: agent, trust_level (0/1/2), outcome (allowed/denied), since (unix ts), limit. Records are pruned by the same retention loop as prompt_log.
| Shape | Fit | Config |
|---|---|---|
| Localhost only | Personal / single-dev | allowed_ips: ["127.0.0.1"] |
| LAN + VPN | Small team inside office/tailnet | Bearer Token + IP allowlist |
| Public internet | Not recommended | mTLS reverse proxy + per-user tokens + audit logging (not shipped with Bridge) |
- Don't pipe arbitrary web/user content directly into
/runswithout framing - Keep
working_dirpinned to a workspace directory, not$HOME - Review agent transcripts for unexpected tool calls before trusting output
- Use Harness Factory's sandboxed presets (
reader,reviewer) for untrusted input — they have restricted tool permissions
- Webhook token is configured separately from Bridge auth token
- OpenClaw format includes auth headers; generic format sends plain JSON
- Messages are auto-chunked at 1800 chars to avoid Discord API limits
Two request fields let a caller supply a URL that Bridge itself then fetches or posts to server-side: callback_url on POST /jobs (see Async Jobs) and workspace_in_url/workspace_out_url on the mesh L3 workspace relay (POST /a2a tasks/send, see A2A Mesh). Both are validated by src/url_safety.py, immediately before Bridge connects to them — not just once at submission:
- Scheme must be
httporhttps. - The resolved host must not be loopback, link-local, private (RFC 1918), reserved, multicast, or a known cloud metadata endpoint (
169.254.169.254,metadata.google.internal, and Alibaba Cloud's100.100.100.200, which falls outside the standard private/link-local ranges) — cloud metadata targets are blocked unconditionally, see below.
validate_outbound_url returns a SafeTarget pinned to the exact IP it just checked, and the actual request (WebhookSender.send() for job callbacks, the workspace download/upload in mesh_a2a.py) connects to that pinned IP — with TLS SNI set to the original hostname and a Host header carrying its full authority, port included — instead of letting the HTTP client re-resolve the hostname itself. This closes DNS rebinding: there is only ever one resolution per request, and it's the one that was checked. Redirects are explicitly disabled on these clients, since following a 30x would connect to a fresh, unvalidated resolution and undo the pinning. Job callbacks are revalidated this way on every send, including webhook retries and jobs recovered from the store after a restart, not just at POST /jobs time — a callback_url that was safe when persisted but resolves unsafely later (or was never re-checked before) is blocked at send time, not just accepted from the store.
The guard applies to client-supplied URLs only. The server-configured webhook.url is trusted operator config and is exempt: pointing it at a private-address gateway (the documented OpenClaw setup) is a normal deployment, not an attack, and does not need an allowlist entry. A per-job callback_url that merely differs from the configured URL is still validated, even when a default is configured.
An invalid callback_url returns 400 with {"error": "unsafe callback_url: ..."} at submission (fail-fast; the enforced check happens again at send time regardless). An invalid workspace URL returns JSON-RPC error -32014 before any download is attempted.
List specific trusted hosts/CIDRs in security.allowed_private_targets (a YAML list, empty by default) to opt them out of the loopback/link-local/private/reserved/multicast range checks — e.g. a self-hosted n8n instance that clients name explicitly as a callback_url. This allowlist only ever affects the private-range checks: cloud metadata hosts/IPs are always blocked, even if an allowlisted CIDR happens to cover them (e.g. 0.0.0.0/0) — there is no configuration that permits a metadata target.
- Blocking DNS resolution. The hostname resolution in
validate_outbound_urlis a synchronoussocket.getaddrinfo()call with no explicit timeout. OnPOST /jobsthis runs inside the async request handler; on the mesh workspace relay it joins pre-existing synchronoushttpxcalls in the same code path (up to 120s timeout each). A slow-to-resolve or non-responding hostname in a client-supplied URL can stall the single asyncio event loop for the OS resolver's timeout, delaying every other in-flight request. Not currently offloaded to a thread executor.
The heartbeat system (heartbeat.enabled: true) periodically pings agents with environment snapshots — who's online, who's busy, recent activity. This enables inter-agent collaboration.
- Path leakage: heartbeat prompts include a client script command for inter-agent communication. As of v0.18.0, only the script basename is shown (e.g.
acp-client.sh), never the absolute path. Previously, the full path (e.g./home/user/projects/acp-bridge/skill/scripts/acp-client.sh) was exposed, revealing the project location to all agents. - Agent visibility: only agents with
heartbeat: truein their config appear in heartbeat prompts. Agents without this flag (e.g. kiro) are invisible to other agents during heartbeat, preventing unwanted cross-agent interactions. --trust-all-tools+ auto-permission: agents with--trust-all-tools(like kiro) combined with Bridge's auto-reply tosession/request_permissioncan execute any shell command. Even withworking_dirset to/tmp/ko, agents cancdor use absolute paths to access any file the Bridge user can access.working_diris a starting directory, not a sandbox.- True isolation requires running agents in Docker containers or Linux namespaces.
Contributions welcome:
- Per-user tokens with scoped permissions
Rate limiting per token/IP→ basic per-agent RPM/TPM rate limiting added in v0.18.0 (see Configuration)- Audit logging (who called what, when)
- mTLS helper / reverse proxy config examples
Since v0.21.3, every prompt actually sent to an agent is persisted in the local SQLite (data/jobs.db, table prompt_log) for post-mortem and replay (see API Reference → Prompt Log).
What is stored: the user-supplied template, the post-{{var}} rendered version, the fully decorated final string (including shared_workspace*.txt hint and get_prompt_suffix()), plus metadata (agent, session id, cwd, decorations applied, timestamp).
What is not stored: the agent's response, intermediate tool-call payloads, or any data outside the prompt itself.
prompt_log.redact_secrets: true— values matching the patterns inOPERATIONS.md("Sensitive Patterns" section) are masked with***REDACTED***before write. Coverstoken=,api_key=,password=,secret=,ACP_BRIDGE_TOKEN=,OPENCLAW_TOKEN=,LITELLM_API_KEY=,ANTHROPIC_API_KEY=,AWS_SECRET_ACCESS_KEY=,Bearer <jwt>, andAKIA...AWS access key ids.prompt_log.max_size: 1048576— per-field cap (1 MB); longer prompts get truncated with a marker.- API responses default to summary-only —
final/template/renderedare returned only when?include=finalis passed. - All endpoints require
Authorization: Bearer <token>(existing middleware).
| Setting | When to change |
|---|---|
prompt_log.enabled: false |
Disable persistence entirely (e.g. regulated environments) |
prompt_log.redact_secrets: false |
Diagnostic-only — when you must inspect the exact original prompt and trust the SQLite file |
prompt_log.retention_days: 0 |
Keep all records forever (default 30; cleanup is opt-in via cron) |
Treat data/jobs.db as containing potentially sensitive user input even with redaction on (heuristic regexes are not exhaustive). Apply filesystem permissions accordingly; do not commit the file to source control (already in .gitignore).
- Configuration — token and IP allowlist setup
- Process Pool — subprocess isolation details
- Troubleshooting — auth error fixes