hack-house/docs/spec-agent-bridge.md
leetcrypt 700e33e3b1 docs: AI agent bridge spec (model-agnostic, /ai command, owner-gated ops)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-01 01:24:48 -07:00

166 lines
8.0 KiB
Markdown

# hack-house → AI Agent Bridge — Spec
> **Status:** Draft v1 · **Date:** 2026-06-01
> **Scope:** Let model-agnostic AI agents participate in a hack-house room as
> first-class encrypted clients — addressed on demand to answer questions, do
> research, check work, and give hints — without weakening the zero-knowledge
> server, E2E Fernet, or SRP guarantees.
> **Baseline reviewed:** `cmd_chat/` server + `hh/` client @ `dev`.
---
## 0. Decisions locked (from product owner)
| # | Decision | Choice |
|---|----------|--------|
| A | Runtime language | **Python** agent runtime (`cmd_chat/agent/`), reusing the existing client's SRP/Fernet/WS plumbing. Not in the Rust TUI. |
| B | Default model | **Ollama, local, recommended.** Privacy-preserving default. |
| C | Model-agnostic | A **provider plugin interface** any backend can implement (ollama, anthropic, openai-compatible, or a custom `module:Class`). |
| D | Addressing | **`/ai` command.** `/ai <question>` if exactly one agent present; `/ai <agent> <question>` to target one by name. |
| E | Sandbox access | Per-agent, owner-gated, mirroring `/drive`: `/ai enable-sbx <agent>` / `/ai disable-sbx <agent>`. Default **off**. |
| F | Operator permission | Operating an agent is privileged. The **room admin** (host) may `/ai grant <user>` / `/ai revoke <user>` to let non-hosts bring/operate an agent. |
| G | Capacity | Each agent consumes one room seat against `max_users`. MVP documents raising `CMD_CHAT_MAX_USERS`; a server-side "agent pool" is a possible later change. |
---
## 1. Core model
**An agent is just another client.** It runs SRP with the room password + a
display name, derives the room key `HKDF(password, room_salt)`, opens the WS,
decrypts every broadcast, and — only when explicitly addressed — encrypts a
reply and sends it like any human. The server (`chat_ws`, a blind relay that
stamps the authenticated `username` on each frame) needs **no changes** for the
MVP.
```
human TUI ─┐ ┌─ agent runtime (headless python client)
├── encrypted WS ─ SERVER ─┤ decrypt → addressed? → Provider.complete()
human TUI ─┘ (blind relay) └─ → encrypt reply → send as chat
```
This preserves all existing guarantees: the server still sees only ciphertext;
the agent only reads a room it already has the password for.
### Privacy posture (non-negotiable defaults)
1. **Addressed-only.** The agent reads everything (it's a client) but forwards to
the model *only* what triggers it. No passive surveillance; lower cost/noise.
2. **Local-first.** Ollama/llama.cpp are the recommended default; cloud providers
are opt-in.
3. **Announce-on-join.** On joining, the agent posts a visible chat line so humans
know an agent is present and where their words go, e.g.
`🤖 oracle joined — ollama/llama3 (local), operated by alice`.
---
## 2. Command grammar (`/ai`)
Reserved first-tokens (an agent name may **not** collide with these):
`list`, `grant`, `revoke`, `enable-sbx`, `disable-sbx`.
| Command | Who | Effect |
|---|---|---|
| `/ai <question>` | any member | Address the sole agent (error if 0 or >1 present). |
| `/ai <agent> <question>` | any member | Address a named agent. |
| `/ai list` | any member | List agents present + provider/model, operator, sbx state. |
| `/ai grant <user>` | **admin** | Allow `<user>` to operate agents. |
| `/ai revoke <user>` | **admin** | Withdraw operator permission. |
| `/ai enable-sbx <agent>` | **sandbox owner** | Allow agent to run sandbox commands. |
| `/ai disable-sbx <agent>` | **sandbox owner** | Withdraw sandbox access. |
Resolution order for the first token: reserved verb → known agent name →
otherwise treat the whole remainder as a question to the sole agent.
---
## 3. Permission tiers (mirrors the existing drive/sudo ACL)
| Tier | Who | Granted by | What it unlocks |
|---|---|---|---|
| **admin** | room/server host | seeded at launch | grant/revoke operators |
| **operator** | admin-granted (or admin) | `/ai grant` | summon & operate an agent the room will honor |
| **sandbox-owner** | whoever hosts `/sbx` | implicit | `/ai enable-sbx` per agent |
| **member** | anyone in the room | — | `/ai <question>` to a present agent |
### Identifying the admin
The agent and operator clients learn the admin's username at agent launch
(`--admin <username>`), defaulting to the room/server host. Because the server
stamps the authenticated sender on every frame, an agent can trust that an
`/ai grant bob` line genuinely came from the admin before honoring it.
### Enforcement honesty (matches the drive-ACL trust model)
- **Sandbox-exec gating is hard-enforced:** the sandbox owner's broker is the
sole writer to the PTY (it already filters drive input), so an agent's
`sandbox_exec` only runs if the owner has `enable-sbx`'d it. Real.
- **Operator ACL is app-layer:** a well-behaved agent obeys the admin's
broadcast ACL and refuses to act if its operator isn't authorized. A hostile
process that already has the room password could ignore this — the same class
of trust as the app-level drive ACL (minus the OS backstop that `sudo` adds).
Documented, not hidden.
---
## 4. Wire conventions
All agent control rides the existing "encrypted JSON with a prefix" convention
(`{"_sbx":…}`, `{"_ft":…}`, `{"_perm":…}` → add `{"_ai":…}`), so it's E2E like
everything else.
- **Announce / meta** (agent → room, on join):
`{"_ai":"meta","agent":"oracle","operator":"alice","provider":"ollama","model":"llama3","sbx":false}`
- **Operator ACL** (admin → room): `{"_ai":"acl","operators":["alice"],"sbx":["oracle"]}`
**MVP simplification:** to avoid Rust TUI work up front, the announce can be a
plain visible chat line and `/ai` queries can be sent as ordinary chat that the
agent scans for. Structured `_ai` frames + a 🤖 roster glyph are Phase 3.
---
## 5. Provider interface (the "API to plug in")
```python
class Provider(Protocol):
name: str
supports_tools: bool
def complete(self, system: str, messages: list[Msg],
tools: list[Tool] | None = None) -> Reply: ...
```
- Bundled adapters: `ollama` (default), `anthropic`, `openai-compatible`
(OpenAI / Groq / Together / local vLLM …).
- Third-party: `--provider mypkg.module:MyProvider`.
- Per-agent config (TOML): `name, provider, model, admin, context_window,
system_prompt, tool_allowlist, rate_limit`.
When triggered, the agent assembles `system` + a bounded window of recent room
transcript + the trigger, calls `complete()`, buffers the reply, and sends it as
one encrypted chat message. (Streaming = Phase 3; the protocol is append-only.)
---
## 6. Phasing
| Phase | Scope |
|---|---|
| **0** | Design + `dev` branch ✓ |
| **1 (MVP)** | Python runtime; `/ai` query (addressed-only); providers ollama + anthropic + openai-compatible; bounded context; buffered reply; plain-text announce-on-join; operator ACL (`/ai grant`/`revoke`) honored by the agent. Minimal Rust: route `/ai …` to chat send. |
| **2** | Tool layer: `web_fetch` + `sandbox_exec` (owner-gated via `/ai enable-sbx`); `/research` `/check` `/hint` verbs; provider tool-use where supported, text-only fallback elsewhere. |
| **3** | Rust TUI polish: structured `{"_ai":…}` frames, 🤖 roster glyph, "typing…" line, `/ai list` rendering, response streaming/coalescing. |
---
## 7. Security notes
- **Prompt injection:** room text is untrusted LLM input. Harden the system
prompt; the agent never exposes the password/key; `sandbox_exec` is capped +
owner-gated.
- **Secrets:** provider API keys live in the agent's env, never in the room.
- **Abuse/cost:** respond only when addressed + per-agent rate limit.
- **Capacity:** agent = one seat; document raising `CMD_CHAT_MAX_USERS`.
---
## 8. Open question
- **Designating the room admin in-protocol.** MVP uses `--admin <username>` at
agent launch (defaults to host). A sturdier option (server marks an admin via
`CMD_CHAT_ADMIN` env or an admin token) would make the operator ACL
tamper-resistant but requires a small server change — deferred unless wanted.