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:
- 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).
- 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).
- Expose vouch behind a reverse proxy for staging/production multi-agent deployments where the KB lives on a separate host from the agents.
- Stream proposal/approval events to an agent that is watching for KB changes (e.g. "notify me when my pending proposal is approved").
- 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
- 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).
- 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.
- 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.
- 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 http → starlette, 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 |
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:
The ROADMAP already lists
vouch serve --transport httpfor 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 existingkb.*method namespace.Concrete example of the problem:
With an HTTP transport, Machine B would run:
What you've tried
proposed/is gitignored and local-only; pushing it would pollute PR reviews and defeat the review-gate semantics.socatover TCP — works as a proof of concept but has no auth, no TLS, and no streaming; any agent on the network can callkb.approvewithout restriction.Suggested shape
1. New
--transport httpmode forvouch serve(cli.py,server.py)Defaults:
--bind 127.0.0.1:7749,--auth token-file(reads~/.vouch/server.tokenorVOUCH_SERVER_TOKENenv 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: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→ 404validation_error→ 422auth_error→ 401/403internal_error→ 5003. Server-Sent Events (SSE) for streaming (
http_server.py)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 serverefuses to start with--auth noneif--bindis not loopback.bearer— static token fromVOUCH_SERVER_TOKENenv var or~/.vouch/server.tokenfile. Token is compared withhmac.compare_digestto avoid timing attacks.token-file— same asbearerbut 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 newserver.tokenslist:VOUCH_AGENTenv 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_URLenv var is set (e.g.http://localhost:7749),vouchcommands proxy through the HTTP server instead of operating on the local filesystem:This makes the CLI a zero-config HTTP client without a separate
vouch remotesubcommand.6. MCP server over HTTP/SSE (
server.py)vouch serve --transport mcp-httpexposes 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.capabilitiesextension (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:vouch initdoes not write aserver:block by default (stdio is the default);vouch serve --transport http --init-configwrites it.Compatibility considerations
This is a VEP-level surface change (new transport, new config stanza, new
kb.*SSE stream):vouch servewith no flags continues to start the MCP stdio server as today.http_server.pyandauth.pymodules add dependencies:starlette+uvicorn(oranyio's built-in HTTP), already in the FastMCP dependency tree..vouch/config.yamlschema changes — new optionalserver:key. Old configs without it still work (defaults apply).A VEP document is required before implementation; the roadmap marks
vouch serve --transport httpas[VEP].Alternatives
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).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.Files that will need changes (non-exhaustive):
src/vouch/http_server.pykb.*dispatch, error mappingsrc/vouch/auth.pysrc/vouch/server.pymcp-httptransport mode; share tool registry withhttp_server.pysrc/vouch/jsonl_server.py_dispatch(method, params)reused by HTTPsrc/vouch/cli.pyvouch serve --transport http/mcp-http;VOUCH_SERVER_URLclient proxy mode;--init-configflagsrc/vouch/capabilities.pysrc/vouch/models.pyServerConfig,TokenEntryto config modelssrc/vouch/storage.pyKBStoreused as-is by HTTP handlerssrc/vouch/audit.pylog_event()as normalsrc/vouch/health.pyvouch doctorchecksserver.token_hashentries for weak/missing hashespyproject.toml[project.optional-dependencies]grouphttp→starlette,uvicorn[standard]schemas/server:config stanzaspec/docs/transports.mdtests/test_http_server.pytests/test_auth.pytests/test_cli.pyvouch serve --transport httpsmoke test;VOUCH_SERVER_URLproxy modeadapters/http/adapter template for agents that connect over HTTPproposals/