Skip to content

feat: HTTP transport with bearer-token auth and SSE streaming for vouch serve #153

Description

@dale053

What you're trying to do

Today vouch's two transports — MCP over stdio and JSONL over stdin/stdout — require the agent and the KB server to run in the same process or be pipe-connected. This works well on a single developer's machine but breaks down the moment you want to:

  1. Share a single reviewed KB across multiple machines (e.g. a team CI runner proposes claims; a human on their laptop reviews and approves them via CLI — currently impossible without committing proposals to git, which the layout explicitly forbids).
  2. Run the KB server as a persistent sidecar that multiple concurrent agents connect to, rather than spawning a fresh MCP server process per agent session (which means each agent gets a cold FTS5 index and has to re-warm embeddings).
  3. Expose vouch behind a reverse proxy for staging/production multi-agent deployments where the KB lives on a separate host from the agents.
  4. Stream proposal/approval events to an agent that is watching for KB changes (e.g. "notify me when my pending proposal is approved").
  5. Use vouch from agents that cannot spawn subprocesses — some hosted runtimes (Lambda, Cloud Run, Replit) cannot exec child processes at all, so stdio-based transports are unavailable.

The ROADMAP already lists vouch serve --transport http for 0.1, but there is no spec for the wire format, no auth layer, no streaming design, and no definition of how the HTTP surface maps to the existing kb.* method namespace.

Concrete example of the problem:

# Machine A — CI agent proposes a claim
VOUCH_AGENT=ci-agent vouch propose-claim "Deploy gate requires green tests on main"

# Machine B — human reviewer wants to approve it
vouch pending   # → empty, because proposed/ is gitignored and never pushed
vouch approve <id>  # → impossible, the proposal only exists on Machine A

With an HTTP transport, Machine B would run:

vouch serve --transport http --bind 0.0.0.0:7749 --auth-token $(cat ~/.vouch-token)
# Machine A's agent POSTs to http://machine-b:7749/kb.propose_claim
# Human reviews and approves via the same server from any machine

What you've tried

  • Git-committing proposals manually to make them visible across machines — the on-disk layout docs explicitly say proposed/ is gitignored and local-only; pushing it would pollute PR reviews and defeat the review-gate semantics.
  • Running the JSONL server with socat over TCP — works as a proof of concept but has no auth, no TLS, and no streaming; any agent on the network can call kb.approve without restriction.
  • Using the MCP server via a remote MCP proxy — adds latency and requires the proxy to handle every method; the proxy can't interpret vouch-specific error codes.

Suggested shape

1. New --transport http mode for vouch serve (cli.py, server.py)

vouch serve --transport http [--bind HOST:PORT] [--auth bearer|token-file|none] [--tls-cert FILE --tls-key FILE]

Defaults: --bind 127.0.0.1:7749, --auth token-file (reads ~/.vouch/server.token or VOUCH_SERVER_TOKEN env var), no TLS (TLS expected to be terminated by a reverse proxy for production use).

2. HTTP wire format (http_server.py — new file)

Map the existing kb.* JSONL envelope directly to HTTP:

POST /kb/<method-name>
Content-Type: application/json
Authorization: Bearer <token>

{ ...params... }

→ 200 OK  { ...result... }
→ 4xx/5xx { "error": { "code": "...", "message": "..." } }

This keeps the same kb.* namespace and param/result schemas as JSONL — adapters need only swap the transport. The JSONL "id" correlation field is dropped (HTTP handles that via TCP).

Error codes map to HTTP status:

  • not_found → 404
  • validation_error → 422
  • auth_error → 401/403
  • internal_error → 500

3. Server-Sent Events (SSE) for streaming (http_server.py)

GET /kb/events?topics=proposals,approvals,lifecycle
Authorization: Bearer <token>

→ text/event-stream
event: proposal_created
data: {"proposal_id": "...", "actor": "ci-agent", ...}

event: proposal_decided
data: {"proposal_id": "...", "decision": "approved", "actor": "human", ...}

Topics correspond to audit event types. An agent can subscribe and react to approvals without polling.

4. Auth layer (auth.py — new file)

Three auth modes:

  • none — localhost-only dev mode; vouch serve refuses to start with --auth none if --bind is not loopback.
  • bearer — static token from VOUCH_SERVER_TOKEN env var or ~/.vouch/server.token file. Token is compared with hmac.compare_digest to avoid timing attacks.
  • token-file — same as bearer but token is read from a file on every request, enabling rotation without restart.

Actor attribution: Authorization: Bearer <token> tokens are mapped to an actor name via .vouch/config.yaml's new server.tokens list:

server:
  tokens:
    - name: ci-agent
      token_hash: sha256:<hex>   # hash stored, not plaintext
    - name: human-reviewer
      token_hash: sha256:<hex>

VOUCH_AGENT env var on the server process becomes the fallback actor when no per-token name is configured.

5. CLI client-side HTTP support (cli.py)

When VOUCH_SERVER_URL env var is set (e.g. http://localhost:7749), vouch commands proxy through the HTTP server instead of operating on the local filesystem:

VOUCH_SERVER_URL=http://kb.internal:7749 \
VOUCH_SERVER_TOKEN=<token> \
vouch pending   # → fetches from remote KB
vouch approve <id>   # → POSTs to remote KB

This makes the CLI a zero-config HTTP client without a separate vouch remote subcommand.

6. MCP server over HTTP/SSE (server.py)

vouch serve --transport mcp-http exposes the same FastMCP tools but over HTTP+SSE following the MCP streamable-HTTP spec. Useful for agents whose runtime does not support spawning child processes.

7. kb.capabilities extension (capabilities.py)

{
  "transport": ["stdio-mcp", "stdio-jsonl", "http-jsonl", "http-sse-mcp"],
  "auth": ["none", "bearer", "token-file"]
}

8. On-disk config changes (.vouch/config.yaml)

New server: stanza:

server:
  bind: "127.0.0.1:7749"
  auth: token-file
  tls: false
  tokens:
    - name: ci-agent
      token_hash: sha256:abc123...

vouch init does not write a server: block by default (stdio is the default); vouch serve --transport http --init-config writes it.

Compatibility considerations

This is a VEP-level surface change (new transport, new config stanza, new kb.* SSE stream):

  • Existing stdio transports are not changed — fully backward compatible.
  • vouch serve with no flags continues to start the MCP stdio server as today.
  • New http_server.py and auth.py modules add dependencies: starlette + uvicorn (or anyio's built-in HTTP), already in the FastMCP dependency tree.
  • .vouch/config.yaml schema changes — new optional server: key. Old configs without it still work (defaults apply).
  • Bundle format is unchanged.
  • Audit log records actor from per-token name, so attribution is richer when HTTP is used.

A VEP document is required before implementation; the roadmap marks vouch serve --transport http as [VEP].

Alternatives

  1. Reverse proxy the JSONL server via socat/ncat — no code changes needed, but no auth layer, no streaming, no TLS, and the JSONL framing is not HTTP-native (newlines as delimiters cause problems with HTTP chunked encoding).
  2. Sync proposals via git — commit proposed/ to a branch and let the reviewer pull it. Cheap, but pollutes PR history, exposes draft proposals to anyone with repo access, and ties the proposal lifecycle to git operations.
  3. Dedicated webhook server — a separate service that receives audit events from vouch and forwards them. Solves the streaming problem but not the remote-approve problem, and adds an external dependency.
  4. MCP over HTTP only (no JSONL mapping) — simpler implementation but breaks agents that already use the JSONL transport and can't consume MCP.

Files that will need changes (non-exhaustive):

File Change
src/vouch/http_server.py New file — HTTP/SSE server, route → kb.* dispatch, error mapping
src/vouch/auth.py New file — bearer token validation, actor resolution, token-hash storage
src/vouch/server.py Add mcp-http transport mode; share tool registry with http_server.py
src/vouch/jsonl_server.py Extract dispatch logic into shared _dispatch(method, params) reused by HTTP
src/vouch/cli.py vouch serve --transport http/mcp-http; VOUCH_SERVER_URL client proxy mode; --init-config flag
src/vouch/capabilities.py Advertise available transports and auth modes
src/vouch/models.py Add ServerConfig, TokenEntry to config models
src/vouch/storage.py No structural changes; KBStore used as-is by HTTP handlers
src/vouch/audit.py No structural changes; HTTP handlers call log_event() as normal
src/vouch/health.py vouch doctor checks server.token_hash entries for weak/missing hashes
pyproject.toml New [project.optional-dependencies] group httpstarlette, uvicorn[standard]
schemas/ New JSON Schema for server: config stanza
spec/ New dated SPEC snapshot documenting HTTP wire format + SSE event schema
docs/transports.md Extend with HTTP transport setup, auth config, SSE subscription guide
tests/test_http_server.py New file — route dispatch, auth rejection, SSE event delivery
tests/test_auth.py New file — token validation, timing-safe compare, rotation
tests/test_cli.py vouch serve --transport http smoke test; VOUCH_SERVER_URL proxy mode
adapters/ New http/ adapter template for agents that connect over HTTP
proposals/ New VEP document required before merge

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions