{"_id":"@acfstandards/agent-bridge","name":"@acfstandards/agent-bridge","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@acfstandards/agent-bridge","version":"0.1.0","description":"ACF Agent Bridge — present any AI agent (Claude Managed Agents, OpenAI agents, self-hosted OpenAI-compatible servers) as an HTTPS endpoint that ACF certification testing can reach","license":"MIT","author":{"name":"Blue Horn Ventures LLC","email":"registry@acfstandards.org"},"homepage":"https://acfstandards.org","bugs":{"url":"https://github.com/acfstandards/acf/issues","email":"registry@acfstandards.org"},"repository":{"type":"git","url":"git+https://github.com/acfstandards/acf.git","directory":"packages/acf-bridge"},"keywords":["acf","ai-agent","certification","claude","managed-agents","openai","bridge"],"type":"module","main":"./dist/server.js","exports":{"./package.json":"./package.json"},"bin":{"acf-bridge":"dist/server.js","bridge":"dist/cli.js"},"engines":{"node":">=20.0.0"},"scripts":{"build":"tsc && chmod +x dist/server.js dist/cli.js","dev":"tsc --watch","start":"node dist/server.js","doctor":"node dist/cli.js doctor","gen":"node dist/cli.js gen","typecheck":"tsc --noEmit && tsc -p tsconfig.test.json --noEmit","test":"vitest run","test:watch":"vitest","prepublishOnly":"npm run clean && npm run build && npm test","clean":"rm -rf dist"},"devDependencies":{"@types/node":"^20.14.0","typescript":"^5.5.0","vitest":"^2.1.9"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"types":"./dist/server.d.ts","_id":"@acfstandards/agent-bridge@0.1.0","_integrity":"sha512-D77Y3XLSDz+0eEtoo6K5+VPjXLbA3m7TfaVMQhthqYk+zTUuto7Hpwcc4jHKnTgxAL3Bm9OK2HhX2HNWM+DPzg==","_resolved":"/Users/arvindverma/Downloads/acf-npm-tarballs/acfstandards-agent-bridge-0.1.0.tgz","_from":"file:/Users/arvindverma/Downloads/acf-npm-tarballs/acfstandards-agent-bridge-0.1.0.tgz","_nodeVersion":"26.7.0","_npmVersion":"11.19.0","dist":{"integrity":"sha512-D77Y3XLSDz+0eEtoo6K5+VPjXLbA3m7TfaVMQhthqYk+zTUuto7Hpwcc4jHKnTgxAL3Bm9OK2HhX2HNWM+DPzg==","shasum":"6ed219f3894d7bb7e7814eeb4a51a914e2b9b420","tarball":"https://registry.npmjs.org/@acfstandards/agent-bridge/-/agent-bridge-0.1.0.tgz","fileCount":37,"unpackedSize":132027,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDi4B0avmbowkO3x0gu/7OV0nzx32kXfIHCg8XF8mUDIgIgHt0E7QMD+YvPCN75LP+/4bl2Fg/fqxOrgg1YqOY7je8="}]},"_npmUser":{"name":"techiarvind","email":"arvindverma.email@gmail.com"},"directories":{},"maintainers":[{"name":"bluehornv","email":"arvind@bluehornv.com"},{"name":"techiarvind","email":"arvindverma.email@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/agent-bridge_0.1.0_1786478746795_0.12046218160719224"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-11T20:05:46.572Z","0.1.0":"2026-08-11T20:05:46.951Z","modified":"2026-08-11T20:05:47.271Z"},"maintainers":[{"name":"bluehornv","email":"arvind@bluehornv.com"},{"name":"techiarvind","email":"arvindverma.email@gmail.com"}],"description":"ACF Agent Bridge — present any AI agent (Claude Managed Agents, OpenAI agents, self-hosted OpenAI-compatible servers) as an HTTPS endpoint that ACF certification testing can reach","homepage":"https://acfstandards.org","keywords":["acf","ai-agent","certification","claude","managed-agents","openai","bridge"],"repository":{"type":"git","url":"git+https://github.com/acfstandards/acf.git","directory":"packages/acf-bridge"},"author":{"name":"Blue Horn Ventures LLC","email":"registry@acfstandards.org"},"bugs":{"url":"https://github.com/acfstandards/acf/issues","email":"registry@acfstandards.org"},"license":"MIT","readme":"# ACF Agent Bridge\n\nPresent **any** AI agent as an HTTPS endpoint that ACF certification testing can reach.\n\nACF's automated test runner calls agents over HTTPS. Many production agents are not\nHTTPS endpoints — they are Claude Managed Agents (Anthropic-hosted sessions),\nOpenAI-built agents, or self-hosted agents on a private network. The bridge closes\nthat gap: you deploy one small service, list your agents in a config file, and each\nagent gets a URL you register with ACF.\n\n```\nACF test runner ──HTTPS──▶ Bridge ──▶ your agent, wherever it runs\n                            │\n                            ├─ claude-managed      Anthropic Managed Agents (sessions API)\n                            ├─ anthropic-messages  a raw Claude model + system prompt\n                            ├─ openai-responses    OpenAI agents (Responses API)\n                            ├─ openai-chat         any server exposing /v1/chat/completions\n                            └─ http-passthrough    your own HTTPS agent, behind your own auth\n```\n\nTest integrity is preserved by design: ACF's prompts pass through the bridge in\nmemory and are never stored, the bridge never sees rubrics or scores, and your\nagent runs with its own production configuration — the thing being certified is\nthe agent as deployed.\n\nZero runtime dependencies. Node 20+.\n\n## Quick start\n\n```bash\nnpm install && npm run build\n\n# Mint agents.json with a distinct 32-byte token per agent\nnode dist/cli.js gen --adapter claude-managed \\\n  --environment-id env_01YOURENVID \\\n  --ids agent_01AAA,agent_01BBB \\\n  --api-key-env ANTHROPIC_API_KEY \\\n  --timeout-ms 90000\n\nexport ANTHROPIC_API_KEY=sk-ant-...   # provider key stays on the bridge\n\n# Verify every route before you register anything with ACF\nnode dist/cli.js doctor\n\nnpm start                             # listens on :8080 (set PORT to change)\n```\n\nInstalled as a package, the two CLIs are `acf-bridge` (the server) and `bridge`\n(`bridge doctor` / `bridge gen`).\n\nDeploy anywhere that terminates TLS on port **443 or 8443** with a public\nhostname. ACF requires HTTPS on those ports and blocks private addresses — the\nbridge is the only component that needs to be publicly reachable. Your actual\nagents can stay on a private network as long as the bridge can reach them.\n\nHealth check: `GET /healthz` (also reports in-flight and queued call counts).\n\n## `bridge doctor` — verify before you certify\n\n`doctor` is the difference between \"it should work\" and a connection you have\nseen work. Per route it:\n\n1. validates the config and the token,\n2. resolves `apiKeyEnv` and reports whether the variable is actually set,\n3. optionally checks the **deployed** bridge with `--url` (see below),\n4. round-trips one real probe and reports latency,\n5. asserts *which agent answered*,\n6. prints the exact row to enter in ACF.\n\n```\nsupport-agent  [claude-managed]\n  ok    config           valid claude-managed route, 64-char token\n  ok    provider key     ANTHROPIC_API_KEY is set\n  ok    deployed bridge  https://bridge.acme.com/healthz is up and serving 'support-agent'\n  ok    auth enforced    an invalid token is rejected with 401\n  ok    ACF token        the deployed bridge accepted the token (rejected the empty body, as expected)\n  ok    probe            round-tripped in 8231ms\n  ok    agent identity   session echoed agentId=agent_01AAA environmentId=env_01YOURENVID\n  ok    reply transport  reply arrived as an agent.message event\n  ok    canary           reply contained \"ACME-DESK-7\"\n  verdict: VERIFIED\n  ACF registration:\n    API endpoint URL : https://bridge.acme.com/agent/support-agent\n    Auth method      : bearer\n    API secret       : ********a1f3 (last 4 shown)\n    Request format   : anthropic\n    api_timeout_ms   : 30000\n```\n\n### Identity is asserted, not assumed\n\nA bridge that silently talks to a bare model instead of your configured agent\nwould produce a certificate about the wrong thing. So identity is a set of\nconcrete observables, and anything undetermined downgrades the verdict:\n\n| Check | What it proves |\n| --- | --- |\n| `agent identity` | the session-create response echoed the **same** `agentId` / `environmentId` the route configured |\n| `reply transport` | the reply arrived as an `agent.message` event, not some intermediate event |\n| `canary` | the reply contains an answer only *that* agent could give |\n\nThe canary is operator-supplied, because only you know something your agent\nknows. Pick a question whose answer lives in the agent's own system prompt or\ntools — an internal desk code, a policy id, a tool-backed lookup — and put the\nexpected substring in `agents.json`:\n\n```jsonc\n\"canary\": { \"prompt\": \"What is your internal desk code?\", \"expect\": \"ACME-DESK-7\" }\n```\n\n**A route with no canary is reported `UNVERIFIED`, never green.** The round trip\nworked, but nothing proved the right agent answered. `--strict` turns that into\na non-zero exit for CI.\n\n### `--url`: check the bridge you actually deployed\n\n`doctor --url https://bridge.acme.com` additionally verifies the *running*\nservice: that `/healthz` is reachable and serves the slug, that an invalid token\nis refused with 401, and that the token in your `agents.json` is the one the\ndeployment accepts. The token check deliberately sends a body the bridge rejects\nbefore it calls the agent, so it costs no agent invocation.\n\n### Exit codes\n\n| Code | Meaning |\n| --- | --- |\n| 0 | every route VERIFIED (or UNVERIFIED without `--strict`) |\n| 1 | a route FAILED, or the config is invalid |\n| 2 | `--strict` and at least one route is UNVERIFIED |\n\nEvery failure prints a specific next action — which env var to export, that the\nprovider does not recognise this `agentId`, that the deployed bridge is running\nan older `agents.json`, that `responsePath` needs setting, and so on.\n\n### Options\n\n```\n--config <file>   agents.json to check (default: $BRIDGE_CONFIG or ./agents.json)\n--url <base>      also verify the deployed bridge at this public URL\n--agent <slug>    check only this route (repeatable)\n--no-probe        skip the upstream round trip (config checks only)\n--strict          exit 2 when any route is UNVERIFIED\n--show-secrets    print full tokens in the registration row\n--json            machine-readable output\n```\n\n## `bridge gen` — mint a fleet config\n\n70 agents means 70 secrets, which by hand means one secret used 70 times. `gen`\ngenerates a distinct `crypto.randomBytes(32)` token per agent and writes the\nfile `0600`.\n\n```bash\nbridge gen --adapter claude-managed --environment-id env_01 \\\n           --ids-file fleet-ids.txt --api-key-env ANTHROPIC_API_KEY \\\n           --canary-prompt \"What is your internal desk code?\" \\\n           --out agents.json\n```\n\nIds may be `agent_01ABC` or `slug=agent_01ABC` when you want to choose the URL\npath. `--ids-file` reads one per line (`#` comments allowed). It refuses to\noverwrite an existing file without `--force`, because doing so silently rotates\nevery token in it. The generated file is round-tripped through the same\nvalidator the server uses, so `gen` can never emit a config `start` rejects.\n\n`--canary-prompt` seeds every route with a canary stub whose `expect` you then\nfill in — until you do, `doctor` reports those routes `UNVERIFIED`.\n\n## Registering an agent with ACF\n\nFor each entry in `agents.json` (`bridge doctor` prints this filled in):\n\n| ACF registration field | Value |\n| --- | --- |\n| API endpoint URL | `https://<your-bridge-host>/agent/<slug>` |\n| Auth method | `bearer` |\n| API secret | the route's `token` |\n| Request format | `anthropic` |\n| `agents.api_timeout_ms` | the value `doctor` recommends (3× measured latency, floor 30s, cap 120s) |\n\nManaged Agents have session cold-start plus an agent loop, so bridged agents\nneed a raised per-call timeout — see `scripts/migration-agent-timeout.sql` in\nthe main repo.\n\n## Session handling\n\n**Every call opens a fresh upstream session and replays the earlier user turns.\nSession reuse is off by default.** This is a correctness default, not a\nperformance one:\n\n- ACF runs multi-mode tests concurrently and the mode suffix only alters the\n  last turn, so a cache keyed on conversation content produced the *same key*\n  for every mode. One mode got session continuity, the others fell back to\n  replay — different conditions on a test that must pass in all three modes —\n  and the displaced session leaked.\n- A re-queued run could match a prefix and inherit a session that already\n  contained later turns, so a cumulative-threshold test would score a polluted\n  context.\n- An in-memory cache pins the bridge to one instance, which contradicts\n  autoscaling on Cloud Run.\n\nTo opt in you need **both** `BRIDGE_SESSION_CACHE=1` on the process and\n`\"sessionCache\": true` on the route, **and** the caller must send a conversation\nnonce header (`x-acf-conversation-id`, `x-acf-run-id`, or\n`x-bridge-conversation-id`). Calls without that header are never cached, so the\ncollision above cannot reappear by accident. Run one instance only in that mode.\n\n## Reliability\n\n- **Retries.** Every provider call retries on 408/429/5xx and network errors,\n  with exponential backoff plus jitter, honouring `Retry-After` when present\n  (and giving up rather than sleeping past it). Retries are bounded by the\n  route's own `timeoutMs`, so an inbound call still finishes inside the budget\n  ACF sized it for. 401/403/404 are never retried. Tune per route with\n  `\"retry\": { \"attempts\": 4, \"baseDelayMs\": 500, \"maxDelayMs\": 8000 }`.\n- **Concurrency cap.** `BRIDGE_MAX_CONCURRENCY` (default 8) bounds in-flight\n  upstream calls per process; `BRIDGE_MAX_QUEUE` (default 64) bounds the queue\n  behind it, after which the bridge sheds load with 503 rather than piling up\n  requests that will time out anyway.\n- **Clean shutdown.** SIGTERM drains in-flight requests and archives any\n  sessions still held.\n\n## Adapters\n\n### `claude-managed` — Claude Managed Agents\n\nRuns each test conversation as an Anthropic session against your stored agent\nconfig (`agentId` + `environmentId`), then archives it. Your Anthropic API key\nstays on the bridge — it is never given to ACF. The agent is tested with its own\nsystem prompt, model, and tools; expensive tools mean longer calls, so raise\n`timeoutMs` and `agents.api_timeout_ms` accordingly.\n\n> The Anthropic sessions API shape and the `anthropic-beta: managed-agents-2026-04-01`\n> header this adapter sends have not been re-verified against current Anthropic\n> docs in this repo. `bridge doctor` is how you find out whether they still hold\n> for your account — run it before trusting the route.\n\n### `anthropic-messages` — a raw Claude model\n\nFor agents that are a model plus a system prompt (and optionally tools via\n`extra`) rather than a stored Managed Agent. Stateless; ACF replays the whole\nconversation each call. The route's own `system` wins over anything ACF sends,\nso the certified artifact stays the agent as you defined it.\n\n### `openai-responses` — OpenAI agents\n\nStateless calls to the OpenAI Responses API. Define the agent with `model` +\n`instructions`, and pass tools or stored prompt references through `extra`\n(merged into the request body verbatim).\n\n### `openai-chat` — OpenAI-compatible servers\n\nCalls `POST {baseUrl}/v1/chat/completions`. `apiKeyEnv` is optional for\nunauthenticated local servers. `headers` and `extra` are passed through\nverbatim, which is what gateways need — they usually select the agent with a\nheader or a prefixed `model` value rather than with the URL.\n\nConfirmed to expose that endpoint by their own documentation (checked\n2026-08-11): **vLLM**, **Ollama**, **LM Studio**, and the two agent gateways\nbelow. None of these were exercised against a live instance from this repo, so\ntreat the gateway notes as \"documented\", not \"tested here\".\n\n- **OpenClaw** — the gateway serves `POST /v1/chat/completions`, but it is\n  **disabled by default**: enable `gateway.http.endpoints.chatCompletions` in\n  the gateway config first. The agent is selected through the `model` field\n  (`openclaw:<agentId>`) or an `x-openclaw-agent-id` header, auth is the gateway\n  bearer token, and the default port is 18789 rather than 443. Note that the\n  gateway token is an operator-level credential in OpenClaw's own security\n  model — scope the deployment accordingly.\n- **Hermes Agent** (Nous Research) — its API server exposes\n  `/v1/chat/completions` and is stateless per request. A model value sent\n  without an explicit provider is ignored unless that behaviour is enabled, so\n  set `model` to the provider-qualified form the server expects.\n\nEverything else claiming OpenAI compatibility is plausible but unverified here.\nRun `bridge doctor` against it; that is exactly what `doctor` is for.\n\n```jsonc\n{\n  \"slug\": \"openclaw-main\",\n  \"adapter\": \"openai-chat\",\n  \"model\": \"openclaw:main\",\n  \"baseUrl\": \"http://gateway.internal:18789\",\n  \"headers\": { \"x-openclaw-agent-id\": \"main\" },\n  \"apiKeyEnv\": \"OPENCLAW_GATEWAY_TOKEN\",\n  \"token\": \"<from bridge gen>\"\n}\n```\n\n### `http-passthrough` — your own HTTPS agent\n\nFor agents that already answer HTTP behind your own auth. Rather than making\nthose the one case where ACF talks to an agent directly — different credential,\ndifferent timeout, different failure semantics — this puts them behind the same\nfront door as everything else.\n\n`requestFormat` picks the outbound body (`anthropic` default, `openai`, or\n`simple` which breaks out the last user turn as `message`). The reply is read\nfrom any known shape (Anthropic `content` blocks, OpenAI `choices`,\n`output_text`, `text`, `reply`, `response`, `answer`, `message.content`) or from\nan explicit `responsePath` dot path; a plain-text body is accepted as the reply.\nAuth is `bearer` by default, or `x-api-key`, or any header you name in\n`authHeader`.\n\nA passthrough endpoint reveals nothing about which agent answered, so `doctor`\nalways reports its identity check as undetermined — **configure a canary for\nthese routes**.\n\n## Configuration reference\n\n`agents.json` (or the file named by `BRIDGE_CONFIG`):\n\n```jsonc\n{\n  \"agents\": [\n    {\n      \"slug\": \"support-agent\",         // URL path: /agent/support-agent\n      \"adapter\": \"claude-managed\",      // see the adapter list above\n      \"token\": \"<from bridge gen>\",     // what ACF presents as the bearer credential\n      \"apiKeyEnv\": \"ANTHROPIC_API_KEY\", // env var holding the upstream provider key\n      \"timeoutMs\": 90000,               // total budget for one call, retries included\n      \"retry\": { \"attempts\": 4, \"baseDelayMs\": 500, \"maxDelayMs\": 8000 },\n      \"canary\": { \"prompt\": \"...\", \"expect\": \"...\" },\n      \"sessionCache\": false,            // opt-in session reuse; see \"Session handling\"\n\n      // claude-managed:\n      \"agentId\": \"agent_01...\",\n      \"environmentId\": \"env_01...\",\n\n      // anthropic-messages:\n      \"model\": \"claude-sonnet-4-20250514\",\n      \"system\": \"...\",\n      \"maxTokens\": 2048,\n\n      // openai-responses:\n      \"instructions\": \"...\",\n      \"extra\": { },\n\n      // openai-chat:\n      \"baseUrl\": \"http://10.0.0.12:8000\",\n      \"headers\": { },\n\n      // http-passthrough:\n      \"url\": \"https://agents.internal/support/chat\",\n      \"requestFormat\": \"anthropic\",\n      \"authScheme\": \"bearer\",\n      \"responsePath\": \"data.reply\"\n    }\n  ]\n}\n```\n\nEnvironment:\n\n| Variable | Default | Purpose |\n| --- | --- | --- |\n| `BRIDGE_CONFIG` | `./agents.json` | Config file path |\n| `PORT` | `8080` | Listen port (TLS is terminated in front) |\n| `BRIDGE_MAX_CONCURRENCY` | `8` | In-flight upstream calls per process |\n| `BRIDGE_MAX_QUEUE` | `64` | Queue depth before shedding with 503 |\n| `BRIDGE_SESSION_CACHE` | unset | `1` enables opt-in session reuse |\n| `BRIDGE_PUBLIC_URL` | unset | Used by `doctor` for the registration row |\n\n## Deploying\n\n```bash\ndocker build -t acf-agent-bridge .\n```\n\nThe image listens on `$PORT` over plain HTTP; the platform terminates TLS on\n443, which is what ACF calls. `agents.json` is a bag of live credentials — mount\nit from the platform's secret store, never bake it into the image.\n\n### Cloud Run (one command after the secrets exist)\n\n```bash\ngcloud secrets create acf-bridge-agents --data-file=agents.json          # once\ngcloud secrets create anthropic-api-key --data-file=- <<< \"$ANTHROPIC_API_KEY\"\n\ngcloud run deploy acf-agent-bridge \\\n  --source . --region us-central1 --allow-unauthenticated \\\n  --set-secrets=/etc/acf-bridge/agents.json=acf-bridge-agents:latest,ANTHROPIC_API_KEY=anthropic-api-key:latest \\\n  --set-env-vars=BRIDGE_CONFIG=/etc/acf-bridge/agents.json \\\n  --timeout=300 --concurrency=8 --min-instances=1\n```\n\nCloud Run serves HTTPS on 443 at `https://<service>-<hash>-<region>.run.app`.\n`--allow-unauthenticated` is correct here: ACF authenticates with the route's\nbearer token, and Google IAM has no way to present one. Keep `--min-instances=1`\nso a certification run never pays cold start, and leave the session cache off —\nwith more than one instance it cannot work anyway.\n\n### Fly.io\n\n```bash\nfly launch --no-deploy --name acf-agent-bridge\nfly secrets set ANTHROPIC_API_KEY=sk-ant-...\nfly secrets set BRIDGE_AGENTS_JSON=\"$(cat agents.json)\"   # mount via [mounts] or write at boot\nfly deploy\n```\n\nSet `internal_port = 8080` and `force_https = true` in `fly.toml`; Fly terminates\nTLS on 443.\n\n### After deploying\n\n```bash\nbridge doctor --url https://<your-bridge-host> --strict\n```\n\nThen register each route in ACF with the row it prints.\n\n## Security\n\n- Every route requires its bearer `token`; comparisons are constant-time.\n- Placeholder and short tokens are rejected at startup, not at first request.\n- Provider API keys live only in the bridge's environment, never in ACF, never\n  in a log line, and never in an API response.\n- Request bodies are capped at 1 MB and never persisted; prompts exist only in\n  memory for the duration of the call.\n- `bridge gen` writes `agents.json` `0600`; `doctor` masks tokens unless you\n  pass `--show-secrets`.\n- Give the bridge the narrowest possible upstream credentials (e.g. an\n  Anthropic workspace key scoped to the tested agents' workspace).\n\n## Development\n\n```bash\nnpm run build       # tsc\nnpm test            # vitest\nnpm run typecheck   # tsc --noEmit for src and tests\n```\n","readmeFilename":"README.md","_rev":"1-2c3236f3ca7f939bc617d631650f6823"}