Skip to content

feat: add ACP agent foundation (synced agents table, proxy, ws-tickets)#624

Open
ital0 wants to merge 12 commits into
mainfrom
feat/acp-foundation
Open

feat: add ACP agent foundation (synced agents table, proxy, ws-tickets)#624
ital0 wants to merge 12 commits into
mainfrom
feat/acp-foundation

Conversation

@ital0
Copy link
Copy Markdown
Collaborator

@ital0 ital0 commented Apr 17, 2026

Summary

Backend foundation for multi-agent support via the Agent Client Protocol (ACP). This is PR1a of a 4-PR split of the original PR #531. No concrete agents yet — just the infrastructure everything else plugs into.

  • New agents PowerSync-synced table + agent_id column on chat_threads (Drizzle migration 0013)
  • GET /agents discovery endpoint with generic AgentProvider pattern (returns empty until Feature A/B register providers)
  • POST /ws-ticket — short-lived auth token system (30s TTL, 10K max, hard cap enforced)
  • WS /agent-proxy/ws/:agentId — generic WebSocket + HTTP/SSE relay with SSRF protection (validateSafeUrl + DNS-pinned safeFetch)
  • New settings: ENABLED_AGENTS (comma-separated allowlist) and ALLOW_CUSTOM_AGENTS (enforced at ticket creation)
  • Shared agent-types.ts (RemoteAgentDescriptor) and powersync-tables.ts update
  • Frontend schema (src/db/tables.ts, src/db/powersync/schema.ts) included to satisfy type-check satisfies Record<PowerSyncTableName, …>

Two-PR deploy requirement

Per CLAUDE.md PowerSync policy:

  1. Merge this PR
  2. Run migration in backend deploys
  3. Update PowerSync Cloud dashboard to include the new `agents` sync rule (already in both local `powersync-service/config/config.yaml` and `deploy/config/powersync-config.yaml`)
  4. Only then merge PR1b (frontend ACP core + chat store migration)

Deploying the frontend before sync rules are live would cause silent sync failure.

Known limitation

WebSocket upstream connections in `agent-proxy` lack DNS-pinning. `validateSafeUrl` provides synchronous hostname-only SSRF protection, but a DNS rebinding attack between validation and connection could bypass this. The HTTP path uses `safeFetch` with full DNS pinning. Documented inline; `createSafeWebSocket` is a follow-up.

Test plan

  • `cd backend && bun test` — 702 pass, 0 fail (+ ~40 new tests: ws-ticket lifecycle, agent routes, proxy helpers, settings)
  • `cd backend && bun run type-check` — 0 new errors (1 pre-existing `settings.ts:83` `origin` error unrelated)
  • `bun run type-check` (root) — 0 new errors (2 pre-existing unrelated)
  • `bun test:5x` — no new failures (6 pre-existing failures in `sse.test.ts` snapshot, unrelated)
  • Drizzle migration journal entry present (`_journal.json` idx=13)
  • Sync rules added to both `powersync-service/config/config.yaml` and `deploy/config/powersync-config.yaml`
  • After merge: run migration + update PowerSync Cloud dashboard before PR1b

Review notes

The full 4-PR split plan is documented at `.ultraplan/ACP-Haystack-PR-Split-Plan.md`. This PR ships the foundation that PR1b, Feature A (Haystack), and Feature B (Custom Agents) all build on top of.

Reading order for reviewers:

  1. `shared/agent-types.ts` + `backend/src/agents/types.ts` — the `AgentProvider` contract
  2. `backend/src/db/powersync-schema.ts` + migration `0013` — schema
  3. `backend/src/agents/routes.ts` — the discovery endpoint
  4. `backend/src/auth/ws-ticket*.ts` — auth tokens
  5. `backend/src/agent-proxy/routes.ts` — the WS/HTTP relay (most complex piece, SSRF-protected)

Note

High Risk
Introduces new network-facing proxying and token issuance (/agent-proxy, /ws-ticket) plus new PowerSync-synced data (agents, chat_threads.agent_id), which are security- and data-flow-sensitive areas where SSRF/credential-handling regressions could be impactful.

Overview
Adds the foundation for ACP-backed multi-agent support by introducing a new PowerSync-synced agents table and associating chats to agents via chat_threads.agent_id (plus updated sync rules, shared table list, and frontend schema/encryption mappings).

Exposes new backend endpoints: unauthenticated GET /agents for agent discovery with env-based allowlisting (ENABLED_AGENTS), authenticated POST /ws-ticket for short-lived one-time WS tickets (with per-user quota + 429 Retry-After), and WS /agent-proxy/ws to relay JSON-RPC over either upstream WebSocket or HTTP/SSE with SSRF validation, bounded WS-connect backlogs, and safer handling of API keys (disallowing cleartext ws:///http:// when credentials are present).

Reviewed by Cursor Bugbot for commit ce6819c. Bugbot is set up for automated code reviews on this repo. Configure here.

- Add synced `agents` PowerSync table (schema, migration, sync rules)
- Add agents, agent-proxy, and ws-ticket routes wired into the app
- Add shared agent types and `agentId` on chat threads
- Add ENABLED_AGENTS and ALLOW_CUSTOM_AGENTS settings with helper + tests
Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@github-actions
Copy link
Copy Markdown

Semgrep Security Scan

No security issues found.

@ital0 ital0 self-assigned this Apr 17, 2026
Comment thread backend/src/agent-proxy/routes.ts
Comment thread backend/src/agent-proxy/routes.ts Outdated
@claude

This comment has been minimized.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 17, 2026

PR Metrics

Metric Value
Lines changed (prod code) +870 / -1
JS bundle size (gzipped) 🟢 1.02 MB → 1.02 MB (-4.9 KB, -0.5%)
Test coverage 🟢 70.57% → 70.57% (+0.0%)
Load time (preview) Preview not ready — Render deploy may have timed out

Updated Mon, 20 Apr 2026 17:44:39 GMT · run #1049

- Add _resetTicketsForTesting to clear in-memory ticket store
- Reset between tests to prevent cross-test pollution
Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

- tighten parseApiKey against non-object/null/non-string apiKey inputs
- add parseClientMessage helper returning JSON-RPC parse error on bad input
- surface upstream WS error detail and expose clearConnections for tests
- rename _resetTicketsForTesting to clearTickets and cover expiry/capacity
- return fulfilled provider agents when another provider rejects
Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@claude

This comment has been minimized.

Comment thread backend/src/agent-proxy/routes.ts
Comment thread backend/src/agents/routes.ts
Comment thread src/db/tables.ts
- reject binary/array/primitive WS frames in parseClientMessage
- inject fetch into handleHttpMessage so upstream behavior is unit-testable
- preserve JSON-RPC request id in non-JSON upstream error responses
- join multi-line SSE `data:` lines with \n instead of concatenating
- document rationale for unauthenticated /agents and co-located sync schema
Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

Comment thread backend/src/agent-proxy/routes.ts
@claude

This comment has been minimized.

Comment thread backend/src/agent-proxy/routes.ts Outdated
- Reject JSON primitives/arrays in parseClientMessage via isPlainObject guard
- Drop unused :agentId path param from WS route (ticket carries identity)
- Add agents table to encryptedColumnsMap (name, url, command, args, auth_method)
Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

Comment thread backend/src/agent-proxy/routes.ts
@claude

This comment has been minimized.

Comment thread backend/src/agent-proxy/routes.ts
Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

Comment thread backend/src/agent-proxy/routes.ts
Comment thread backend/src/agents/types.ts Outdated
@claude

This comment has been minimized.

Comment thread backend/src/agent-proxy/routes.ts
- extend ws:// guard to also reject http:// when an apiKey is set
- http:// would leak Authorization header in cleartext
- drop unused RemoteAgentDescriptor re-export from agents/types
Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

Comment thread backend/src/agent-proxy/routes.ts
Comment thread shared/agent-types.ts Outdated
…imeout

- Queue client messages while upstream WS is CONNECTING, flush on open;
  cap at 64 messages / 256KB and close downstream with 4005 on overflow.
- Clear the 30s initial-connect timeout once HTTP response headers arrive
  so long-running SSE streams are not aborted mid-stream.
- Inject a WebSocket factory into openWsRelay for test isolation and
  export handleWsMessage / WsConnectionState for direct unit coverage.
- Drop unused shared/agent-types.ts.
Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

Comment thread backend/src/agent-proxy/routes.ts
Comment thread backend/src/agent-proxy/routes.ts
- throw TicketQuotaError (HTTP 429 + Retry-After) after 20 active tickets per user
- apply IP rate limit middleware to POST /ws-ticket
- cover quota, independence per user, and expiry eviction in tests
Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

Comment thread backend/src/agent-proxy/routes.ts
- gate concurrent HTTP messages on a bootstrap promise so subsequent
  requests carry the Acp-Session-Id captured by the first response;
  reset the gate on failure so the next message can retry
- close upstream WS before downstream on backlog overflow to avoid
  leaking an in-flight handshake until the peer's idle timeout
- rename maxSseBufferBytes to maxSseBufferChars to match what is
  actually measured (string length, not bytes)
Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

Comment thread backend/src/agent-proxy/routes.ts
Comment thread backend/src/auth/ws-ticket.ts
Comment thread src/db/encryption/config.ts Outdated
Comment thread backend/src/config/settings.ts
- add ENABLED_AGENTS and ALLOW_CUSTOM_AGENTS to backend .env.example
- extend agents encrypted columns with install_path, package_name, description
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ce6819c. Configure here.

releaseBootstrap()
}
return
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notification path discards session headers during bootstrap

Medium Severity

The notification/response code path can claim the bootstrap role (when it's the first message on the connection) but completely discards the fetch response, never capturing Acp-Session-Id or Acp-Connection-Id headers. It then calls releaseBootstrap() which, seeing sessionId is still null, resets bootstrapPromise to null and unblocks waiters. Any request that was awaiting the bootstrap gate proceeds without session headers, and the connection briefly enters a state where both bootstrapPromise and sessionId are null — allowing multiple concurrent messages to each claim a new bootstrap attempt, risking session thrashing.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ce6819c. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants