diff --git a/docs/integration-tiers.md b/docs/integration-tiers.md new file mode 100644 index 00000000..a597f164 --- /dev/null +++ b/docs/integration-tiers.md @@ -0,0 +1,66 @@ +# AgentTower Integration Tiers (research note) + +Status: Research note (reference only) — v0.2 +Date: 2026-06-18 + +> **Research only — not the authoritative source.** The connection-tier model is +> formalized in the OpenSpec change `openspec/changes/agent-connection-tiers/` +> (`proposal.md`, `design.md`, and the `agent-integration` spec delta). For any +> normative description of the tiers, the two axes, integration modes, the +> router-vs-puppeteer boundary, the adoption story, or the no-network-listener +> invariant, read that change — **not this note.** This file is kept only as an +> orientation/on-ramp plus a little extra comparative context, to avoid the two +> drifting apart. + +## Orientation: the connection-strength ladder + +Per agent, AgentTower negotiates the strongest available channel and always +falls back to the tmux floor. (Full normative model: `agent-connection-tiers`.) + +| Tier | Channel | Inbound (events) | Outbound (delivery) | Works on | +| --- | --- | --- | --- | --- | +| **0 — tmux** | `pipe-pane` + paste-buffer | scraped output, rule-classified | paste-buffer + Enter | _anything_ (universal floor; ships today) | +| **1 — native harness** | hooks / MCP / stream-json | exact harness events | structured message delivery the harness accepts | Claude / Codex, per capability | +| **2 — Omnigent** | Omnigent session API / SSE | uniform structured events | hand off to Omnigent to drive | any harness Omnigent wraps | + +Two settled decisions anchor the model (both specified normatively in the +design): **AgentTower is the router, not the puppeteer** (agents report *into* +it; it never drives an agent's task loop or intercepts tool calls), and +**Omnigent is the puppet master** AgentTower hands off to for action-level +driving. + +## What the authoritative change covers + +Rather than restate it here, see `openspec/changes/agent-connection-tiers/` for: + +- the tier ladder and Tier-0-as-floor guarantee; +- independent inbound/outbound axis negotiation; +- the per-agent `integration` mode (`auto` / `tmux-only` / `native` / + `omnigent`); +- tier-independent outbound safety (FEAT-009 gate + kill switch in front of + every tier); +- first-class adoption of pre-instrumented panes; +- the no-network-listener invariant (hooks call the CLI over the Unix socket; + the MCP server is a stdio bridge to that socket). + +## Extra comparative context (the part not in the design) + +This is the background that motivated the model and is not duplicated in the +normative change: + +- **Why Omnigent is Tier 2, not a competitor.** See + `docs/related-work-omnigent.md` — Omnigent *wraps and drives* agents; AgentTower + *observes and routes* unmodified ones. Tier 2 is the deliberate hand-off seam, + not a merge of the two products. +- **Native harness surfaces that make Tier 1 possible.** Claude Code exposes + `-p`/`--print` headless with `--output-format stream-json` / + `--input-format stream-json`, lifecycle **hooks** (PreToolUse / PostToolUse / + Notification / Stop), and **MCP**. Codex exposes a headless `exec` mode, a JSON + output mode, and an MCP server mode. **Codex exact flag spellings must be + confirmed before they enter any implementing feature's tasks** (tracked in the + change's tasks). +- **The realism constraint.** Strong tiers generally require control at session + *startup* (hooks/MCP load at start; a running TUI can't switch into + stream-json mid-flight). That is why managed launch and pre-instrumented + adoption are the two unlock points, and why Tier-1 inbound via hooks is the + recommended first step. diff --git a/docs/related-work-omnigent.md b/docs/related-work-omnigent.md new file mode 100644 index 00000000..31793c3c --- /dev/null +++ b/docs/related-work-omnigent.md @@ -0,0 +1,141 @@ +# Related Work: Databricks Omnigent + +Status: Research note (reference only) — v0.1 +Date: 2026-06-18 + +> **Research only.** This is a related-work/reference note. It records no +> committed scope and modifies no canonical spec. Proposed AgentTower scope +> derived from this analysis lives under `openspec/changes/` (the OpenSpec +> change flow), not in this file. The connection-tier model in particular is +> formalized in `openspec/changes/agent-connection-tiers/`. + +This note compares AgentTower with **Omnigent**, the open-source "meta-harness" +that Databricks released (Apache-2.0, mid-June 2026). The two tools share a +goal — coordinating multiple AI coding agents — but sit at opposite ends of the +coupling spectrum. This doc records the comparison and the Omnigent-derived +ideas that are candidates for AgentTower scope. + +Primary sources: + +- Databricks blog — "Introducing Omnigent: A Meta-Harness to Combine, Control + and Share Your Agents" +- GitHub — `omnigent-ai/omnigent` + +## 1. One-line framing + +> **Omnigent is a meta-_harness_ — it runs your agents. AgentTower is a +> meta-_observer/router_ — it coordinates agents that are already running.** + +Same neighbourhood, opposite ends of the coupling spectrum. AgentTower could +even observe panes that happen to be Omnigent-launched sessions. + +## 2. What each tool is + +**AgentTower** is a local control tower that **observes and routes between +agents that already exist** in tmux. It does not launch or wrap agents. It +discovers tmux panes inside bench containers, lets a human label them +`master` / `slave` / `swarm`, tails their output via `tmux pipe-pane`, +classifies events (`waiting_for_input`, `test_failed`, …), and safely routes +structured text notifications between panes via paste-buffers. Host daemon +(`agenttowerd`) plus thin container clients over a Unix socket; SQLite + JSONL; +no network listener; CLI-first. + +**Omnigent** is a **meta-harness that wraps and launches agents** behind a +uniform API. A `runner` executes Claude Code / Codex / Cursor / Pi / OpenAI +agents inside OS sandboxes (macOS `seatbelt`, Linux `bubblewrap`, or cloud +sandboxes via Modal / Daytona / Islo), and an optional `server` adds policy +enforcement, a web UI (`localhost:6767`), and **live session sharing by URL** so +teammates can watch, co-drive, or fork a session from any device. Python 3.12+ +with a TypeScript harness CLI; `uv` packaging. + +## 3. Where they overlap + +| Dimension | AgentTower | Omnigent | +| --- | --- | --- | +| Core goal | Coordinate many parallel agents | Coordinate many parallel agents | +| Multi-harness aware | Capabilities: claude, codex, gemini, opencode | Wraps claude code, codex, cursor, pi, openai | +| Governance | Permission gates, routing kill-switch, no-input-to-unknown | Stacked policies: ask-before-shell, spend caps, tool-call limits | +| Audit trail | JSONL event history | Policy checks every action (allow / block / ask) | +| Sandbox-conscious | Bench / devcontainer-first | Omnibox OS sandbox + cloud sandboxes | +| Local-first option | Yes (only option) | Yes (plus cloud / server modes) | + +## 4. Where they fundamentally differ + +| | AgentTower | Omnigent | +| --- | --- | --- | +| Relationship to the agent | Observer / router — agents run independently in tmux; AT attaches from outside | Harness / wrapper — AT _is_ the thing that launches and mediates the agent | +| Coupling | Loosely coupled; zero changes to Claude / Codex / tmux | Tightly coupled; agent runs _inside_ its runner with a uniform tool API | +| Who drives orchestration | A human-designated `master` agent pane drives; AT is just transport | The server / policy layer + uniform API drives composition | +| Control mechanism | tmux pane I/O (paste-buffer + Enter) | Programmatic API + OS sandbox interception (incl. network transforms) | +| Agent-state knowledge | Inferred from terminal output (conservative, rule-based) | Native (it owns the session) | +| Collaboration | None (single operator, CLI) | First-class: share session URL, co-attach, fork, OIDC multi-user | +| Deployment | Host daemon + container clients, no network listener | CLI + optional server, Docker / Render / Fly / Modal, reachable "from your phone" | +| Substrate scope | tmux + Docker bench containers specifically | Any wrapped harness, local or cloud sandbox | + +## 5. Why they don't really compete + +- **Omnigent owns the agent.** It is an SDK / framework — you build _on_ it, and + the agent exists only because Omnigent launched it inside its sandbox with its + tool API. That is why it can do clean policy interception and URL sharing: it + sits _in_ the request path. +- **AgentTower watches agents it does not own.** It is deliberately + non-invasive (PRD non-goal #1: don't replace Claude / Codex / tmux; success + metric: "without requiring changes to Claude, Codex, tmux, or Docker"). That + is why it must _infer_ state from terminal scrollback and route via + safe-but-fragile paste-buffers. It sits _beside_ the path. + +So AgentTower is closer to a **tmux-native NOC / observability layer + message +bus**, while Omnigent is a **multi-harness agent runtime / SDK**. Omnigent's +`runner` is precisely the part AgentTower deliberately refuses to be. + +**Guardrail for AgentTower:** do not converge toward Omnigent's tight coupling. +The value proposition is coordinating _unmodified_ agents in _existing_ +tmux/bench setups. Adopting a wrapping runner would turn AgentTower into a +competitor of both Omnigent and Claude Code, rather than a complement. + +## 6. Omnigent-derived features (candidate scope) + +The following ideas fit AgentTower's non-invasive thesis. They are **candidates +only** — not committed scope and not yet written into any canonical spec. They +are the raw input for a comprehensive OpenSpec proposal; any adopted item will +be captured under `openspec/changes/`. They are listed here with the +AgentTower-appropriate framing. + +1. **Layered / stacked policies.** Omnigent stacks policies at server, per-agent, + and per-session levels (ask-before-shell, token-spend caps, tool-call + limits). AgentTower today has a per-pane `send_input_allowed` boolean plus a + global routing kill switch. A layered policy model (global → per-agent → + per-route) generalises this and lands the existing "explicit per-agent + permission grants" item in architecture §24. AgentTower's version governs + _routing/input delivery_, not agent tool calls (which it cannot see). + +2. **Session sharing / co-attach / fork.** Omnigent's headline collaboration + feature: share a live session by URL, let a teammate co-drive, or fork the + conversation. For AgentTower this maps onto multi-operator support over the + FEAT-011 `app.*` backend — multiple humans observing the same registry/event + stream, with read-only "watch" vs. permitted "co-drive". This is the natural + answer to PRD Open Question #2 (TUI vs. web vs. CLI) and is V2 because MVP has + no network listener. + +3. **Pluggable execution-sandbox backends.** Omnigent treats local OS sandboxes + and cloud sandboxes (Modal / Daytona) as interchangeable execution targets. + AgentTower's analogue is treating the bench container as one _substrate + backend_ among several (host-only tmux, future remote/cloud bench) behind the + discovery interface — additive to the existing "host-only tmux discovery" + backlog item. + +4. **Reachable-from-anywhere / mobile surface.** Omnigent makes sessions + reachable from a phone. AgentTower's MVP is explicitly no-network-listener; + this is a deliberate V2 item, gated behind an authenticated transport, and + should reuse the FEAT-011 contract rather than scrape CLI output. + +5. **Network-egress policy at the boundary.** Omnigent's Omnibox can transform / + restrict agent network requests. AgentTower cannot intercept an unmodified + agent's syscalls, but it _can_ record this as a known capability gap and, if + ever needed, document an opt-in container-network-policy recommendation + rather than implement interception itself. + +Items 1–3 are the highest-leverage and lowest-risk for AgentTower's model. +Items 4–5 are noted for completeness but carry the most tension with the +local-first, non-invasive constitution and should not be pursued without an +explicit decision. diff --git a/openspec/changes/README.md b/openspec/changes/README.md new file mode 100644 index 00000000..661ddcf7 --- /dev/null +++ b/openspec/changes/README.md @@ -0,0 +1,60 @@ +# Omnigent-Derived Change Family — Roadmap + +This directory holds a coordinated family of OpenSpec changes derived from the +AgentTower ↔ Omnigent analysis in `docs/related-work-omnigent.md` (research +only) and `docs/integration-tiers.md` (research only). The changes are +independent OpenSpec proposals; this note records how they fit together. + +## Dependency / build order + +```text +agent-connection-tiers (A) ── foundation: how AT connects to an agent + │ (Tier 0 tmux / Tier 1 native / Tier 2 Omnigent) + ├── routing-policy-layers (B) ── layered delivery policies; composes with + │ the tier-independent outbound gate + ├── discovery-backends (D) ── pluggable substrates feeding the registry + │ (independent of B) + └── app-collaboration (C, V2) ── multi-operator watch/co-drive over + the FEAT-011 app backend; ideally + composes with B +``` + +Recommended order: **A → B → D → C**. B and D are largely independent and can +proceed in parallel; C builds on the app backend and ideally on B. + +## Shared guardrails (every change) + +- **No network listener** — C/E gated behind a separately decided authenticated + transport. +- **Router, not puppeteer** — AgentTower routes; Omnigent (Tier 2) drives. +- **No host-absolute paths** in any artifact. +- Every requirement carries at least one scenario (`openspec validate --strict` + clean). + +## Dispositions of the remaining candidates + +- **E — reachable-from-anywhere / mobile:** deferred non-goal. Recorded inside + `app-collaboration` with the gate it needs (authenticated transport + a + deliberate reversal of the no-network-listener stance). Becomes its own change + only if that gate is opened. +- **F — agent network-egress policy:** advisory only. AgentTower cannot + intercept an unmodified agent's syscalls, so this is a documented capability + gap (and at most an opt-in container-network recommendation), not a change. + +## Bench ownership + +Per `AGENTS.md` (Single-Bench Feature Ownership), every feature is owned by +exactly one execution bench. All four changes are backend/daemon/contract work +and are owned by **`py-bench`** (Python daemon, CLI, and local `app.*` +contracts). `app-collaboration` defines only the **backend contract** for +multi-operator roles; any Flutter desktop UI that consumes it is a **separate +`flutter-bench` follow-up feature**, split per the AGENTS.md cross-bench rule. + +## Changes + +| ID | Change | Capability | Owner (bench) | Status | +| --- | --- | --- | --- | --- | +| A | `agent-connection-tiers` | `agent-integration` | `py-bench` | proposed (validated) | +| B | `routing-policy-layers` | `routing-policy` | `py-bench` | proposed | +| D | `discovery-backends` | `discovery-backends` | `py-bench` | proposed | +| C | `app-collaboration` | `app-collaboration` | `py-bench` (Flutter UI → separate `flutter-bench` feature) | proposed (V2) | diff --git a/openspec/changes/agent-connection-tiers/.openspec.yaml b/openspec/changes/agent-connection-tiers/.openspec.yaml new file mode 100644 index 00000000..95ae5a2c --- /dev/null +++ b/openspec/changes/agent-connection-tiers/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-06-18 diff --git a/openspec/changes/agent-connection-tiers/README.md b/openspec/changes/agent-connection-tiers/README.md new file mode 100644 index 00000000..8d522670 --- /dev/null +++ b/openspec/changes/agent-connection-tiers/README.md @@ -0,0 +1,3 @@ +# agent-connection-tiers + +Per-agent connection-strength tier model (Tier 0 tmux / Tier 1 native harness / Tier 2 Omnigent hand-off) diff --git a/openspec/changes/agent-connection-tiers/design.md b/openspec/changes/agent-connection-tiers/design.md new file mode 100644 index 00000000..a3aea21f --- /dev/null +++ b/openspec/changes/agent-connection-tiers/design.md @@ -0,0 +1,153 @@ +# Design: Agent Connection-Strength Tier Model + +## Context + +AgentTower is a non-invasive observer/router over tmux panes in bench +containers (host daemon + thin clients over a Unix socket; SQLite + JSONL; no +network listener). Its only connection mechanism today is the tmux floor: +`pipe-pane` for observation, paste-buffer for delivery. The harnesses it targets +expose stronger structured surfaces, and Databricks' Omnigent provides a full +agent-driving runtime. This design defines how AgentTower opportunistically uses +stronger channels without giving up the universal floor or its router identity. + +## Goals / Non-Goals + +**Goals** +- A per-agent ladder from the tmux floor to native harness integration to an + Omnigent hand-off. +- Independent negotiation of inbound (events) and outbound (delivery). +- A settled, documented role boundary (router vs puppet master). +- First-class Tier-1 for both managed launches and adopted pre-instrumented + panes. + +**Non-Goals** +- AgentTower does not drive an agent's task loop or intercept its tool calls. +- AgentTower does not embed or re-implement the Omnigent runner. +- No tier introduces a network listener. +- No code lands in this change (spec/design only). + +## The connection-strength ladder + +| Tier | Channel | Inbound (events) | Outbound (delivery) | Works on | +| --- | --- | --- | --- | --- | +| **0 — tmux** | `pipe-pane` + paste-buffer | scraped output, rule-classified | paste-buffer + Enter | _anything_ (universal floor; ships today) | +| **1 — native harness** | hooks / MCP / stream-json | exact harness events (Notification ⇒ `waiting_for_input`, Stop ⇒ `completed`) | structured message the harness accepts | Claude / Codex, per capability | +| **2 — Omnigent** | Omnigent session API / SSE | uniform structured events | hand off to Omnigent to drive | any harness Omnigent wraps | + +Tier 0 is the guaranteed floor and is never removed. Tiers 1–2 are upgrades. + +## Decision 1 — Two independent axes + +Inbound (observation/events) and outbound (delivery) negotiate **separately**. +A discovered interactive Claude pane can keep its human-in-the-loop TUI, upgrade +its *events* to Tier 1 via hooks, and keep its *input* on the Tier-0 +paste-buffer. This asymmetric state is the expected common case. + +Rationale: it lets the lowest-risk, highest-value upgrade (read-only event +reporting) ship independently of structured delivery, and it matches the reality +that observation and delivery have very different safety profiles. + +## Decision 2 — Router, not puppeteer + +AgentTower stays the router: +- **Inbound:** agents *push* events via hooks, and/or *self-report* via an MCP + **server** AgentTower hosts (tools like `report_status`, `request_route`). + AgentTower is the MCP server; the agent is the client calling in. AgentTower + does **not** act as an MCP client that drives the agent. +- **Outbound (Tier 1):** still *message routing* — delivering a prompt/ + notification through whatever structured inbox the harness accepts — not + taking over the task loop or governing tool calls. +- **Action-level driving, sandboxing, and policy over an agent's own tool calls + is Tier 2 (Omnigent).** AgentTower hands the agent off and continues to + observe/route around it. + +Rationale: this preserves AgentTower's value proposition (coordinate *unmodified* +agents) and avoids it becoming a competing runtime to Omnigent or Claude Code. + +## Decision 3 — Adoption of pre-running panes is first-class + +Strong integration generally requires control at session **startup** (hooks and +MCP wiring load at start; a running TUI can't switch into `stream-json` +mid-flight). AgentTower supports two entry points: + +- **Managed launch (FEAT-013):** AgentTower sets flags/settings/hooks/MCP config + when it spawns the pane and freely chooses Tier 1 or Tier 2. +- **Adopted pre-instrumented panes:** AgentTower ships its own hook scripts + MCP + server, so pre-instrumenting a session is just adding AgentTower to the + harness settings. Such a pane lights up Tier 1 on adoption; an uninstrumented + pane stays at the tmux floor. Adoption never downgrades below the floor. + +## Decision 4 — No-network-listener invariant preserved + +Both Tier-1 mechanisms bridge to the daemon through the **existing Unix socket / +thin client**: +- Hook scripts run `agenttower` CLI subcommands that talk to the host daemon + over the mounted Unix socket. +- AgentTower's MCP server is a **stdio** subprocess (launched by the harness) + that bridges to the same Unix socket. + +AgentTower remains a *client* of any external server, including Omnigent's +localhost server. No tier opens a network listener. This keeps the MVP +constitution (`architecture.md` §23) intact. + +## Decision 5 — Per-agent integration mode + tier-independent safety + +A per-agent `integration` field, negotiated at register/launch: +- `auto` (default): probe capability, use the strongest available axis-by-axis, + fall back to tmux. +- `tmux-only`: force the non-invasive floor (audit / maximum-safety mode). +- `native`: require Tier 1; fail loudly if unsupported. +- `omnigent`: route through Omnigent (Tier 2). + +The FEAT-009 permission gate and the global routing kill switch sit in front of +*every* outbound tier; upgrading the channel never bypasses the gate or the +`events.jsonl` audit trail. + +## Native harness surfaces (Tier 1) + +**Claude Code** — `-p` / `--print` headless with `--output-format stream-json` + +`--input-format stream-json` (managed/headless workers); **hooks** +(PreToolUse / PostToolUse / Notification / Stop) as the primary inbound channel; +**MCP** server hosted by AgentTower for self-report. + +**Codex** — headless `exec`, a JSON output mode, and an MCP server mode (same +shape as Claude). **Exact flag spellings to be confirmed before implementation.** + +## Related work — why Omnigent is Tier 2 + +Databricks open-sourced Omnigent (Apache-2.0), a meta-*harness* that *wraps and +launches* agents (Claude Code, Codex, Cursor, Pi, OpenAI) inside OS sandboxes +behind a uniform API, with stacked policies and live session sharing. AgentTower +is a meta-*observer/router* that coordinates agents it does not own. They sit at +opposite ends of the coupling spectrum and are complementary: Omnigent owns and +drives the agent (puppet master); AgentTower observes and routes around it +(router). That is precisely why Omnigent is the top tier AgentTower *hands off* +to rather than something it reimplements. + +## Risks / Trade-offs + +- **Version skew** with harness hook/MCP schemas and the Omnigent API — isolate + each tier behind an adapter so a breaking upstream change is contained. +- **Two policy layers** at Tier 2 (Omnigent governs agent actions; AgentTower + governs routing/delivery) — document precedence to avoid "who blocked this?" + debugging traps. They are orthogonal: AgentTower decides *whether to send*, + Omnigent decides what the agent may *do*. +- **Cloud Omnigent sandboxes** break the host-visible-log + UID invariant + (`architecture.md` §4.1) and `docker exec` discovery — out of scope here; + flagged for a dedicated future decision. + +## Migration / Phasing + +1. Tier-1 inbound via hooks (read-only; retires §13 classifier problem). +2. AgentTower-hosted MCP server (stdio → socket); enables pre-instrumented + adoption. +3. Tier-1 outbound structured delivery behind the FEAT-009 gate. +4. Tier-2 Omnigent hand-off. + +## Open Questions + +- Codex exact flag spellings (`exec` / JSON / MCP) — confirm before tasks. +- Should an agent's negotiated tier be surfaced through the FEAT-011 `app.*` + backend in the first implementing feature, or deferred? +- Cloud Omnigent sandboxes as a separate substrate backend — defer to its own + change. diff --git a/openspec/changes/agent-connection-tiers/proposal.md b/openspec/changes/agent-connection-tiers/proposal.md new file mode 100644 index 00000000..515cd4b6 --- /dev/null +++ b/openspec/changes/agent-connection-tiers/proposal.md @@ -0,0 +1,74 @@ +# Add Agent Connection-Strength Tier Model + +## Why + +AgentTower connects to every agent through a single mechanism today: tmux +`pipe-pane` for observation and paste-buffer for delivery. That floor is +universal and fully non-invasive, but it is also the weakest possible channel — +events are *inferred* from scraped scrollback (the `architecture.md` §13 +conservative-classifier problem) and delivery *guesses* whether a pane is ready +(`architecture.md` §16). Meanwhile the harnesses AgentTower already targets +expose far stronger, structured surfaces: Claude Code has hooks, an MCP +interface, and headless `stream-json`; Codex has a headless/JSON/MCP surface; +and Databricks' open-source Omnigent meta-harness can drive an agent's full +action loop under policy and sandboxing. + +AgentTower should be able to *use* the strongest channel an agent supports +without abandoning the tmux floor for agents that support nothing. This change +defines that ladder and the role boundaries around it, so later features +implement against a settled model instead of ad-hoc per-harness hacks. + +## What Changes + +- **Introduce a per-agent connection-strength ladder.** Tier 0 = tmux floor + (today). Tier 1 = native harness integration (Claude hooks + an + AgentTower-hosted MCP server + headless `stream-json`; Codex exec/json/MCP). + Tier 2 = Omnigent meta-harness hand-off. +- **Negotiate inbound (events) and outbound (delivery) independently** per + agent, so a pane can have exact event reporting while delivery stays on the + paste-buffer floor. +- **Settle the role boundary:** AgentTower stays the **router** — agents report + *in* via hooks and an AgentTower-hosted MCP **server**; AgentTower does not + drive an agent's task loop or intercept its tool calls. **Omnigent is the + puppet master** that AgentTower hands off to for action-level + driving/sandboxing/policy. +- **Make adoption of pre-running panes first-class.** AgentTower ships its own + hook scripts + MCP server, so a pane started pre-instrumented lights up Tier 1 + on adoption; an uninstrumented pane stays at the tmux floor. Adoption never + downgrades an agent below the floor. +- **Add a per-agent `integration` mode:** `auto` (default; probe, then fall back + to tmux), `tmux-only`, `native`, `omnigent`. +- **Preserve the no-network-listener invariant.** Hook scripts call the + `agenttower` CLI over the existing Unix socket; the MCP server is a stdio + subprocess bridging to that same socket; AgentTower remains a *client* of any + external server (including Omnigent's localhost server). No new listener. +- **Keep the safety layer tier-independent.** The FEAT-009 permission gate and + global routing kill switch sit in front of *every* outbound tier; every + delivery is still audited in `events.jsonl`. + +## Impact + +- **New capability spec:** `agent-integration` (this change's spec delta). +- **Touches (in later implementing features, not in this change):** + capability/role metadata (FEAT-006), event pipeline (`architecture.md` §13), + input delivery (`architecture.md` §16 / FEAT-009 gate), managed session + creation (FEAT-013), and the app backend (FEAT-011) for surfacing tier state. +- **Phasing (recommended, each a later implementing feature):** + 1. Tier-1 inbound via hooks — read-only, lowest risk, retires the §13 + classifier problem on its own. + 2. AgentTower-hosted MCP server (stdio → socket) — enables pre-instrumented + adoption and structured self-report. + 3. Tier-1 outbound structured delivery — behind the FEAT-009 gate. + 4. Tier-2 Omnigent hand-off. +- **No code ships in this change** — it is a spec/design proposal only. +- **Open item:** Codex exact flag spellings (`exec`, JSON mode, MCP mode) MUST + be confirmed against current Codex docs before they enter an implementing + feature's tasks. +- **Related work:** see `design.md` for the Omnigent (Databricks open-source + meta-harness) comparison that frames Tier 2. + +## Tooling note + +This change's artifacts follow the standard OpenSpec file conventions. Validate +with `openspec validate agent-connection-tiers --strict` in any environment that +has the OpenSpec CLI installed. diff --git a/openspec/changes/agent-connection-tiers/specs/agent-integration/spec.md b/openspec/changes/agent-connection-tiers/specs/agent-integration/spec.md new file mode 100644 index 00000000..6951f1a6 --- /dev/null +++ b/openspec/changes/agent-connection-tiers/specs/agent-integration/spec.md @@ -0,0 +1,138 @@ +# agent-integration + +Capability: how AgentTower connects to each agent, on a per-agent +connection-strength ladder, while remaining the router (not the puppeteer). + +## ADDED Requirements + +### Requirement: Per-agent connection-strength ladder + +AgentTower SHALL connect to each agent on a tiered ladder — Tier 0 (tmux floor: +`pipe-pane` observation + paste-buffer delivery), Tier 1 (native harness +integration), Tier 2 (Omnigent meta-harness hand-off) — and SHALL select a tier +per agent rather than globally. Tier 0 SHALL always be available and SHALL never +be removed. + +#### Scenario: Tier 0 is the universal floor + +- **WHEN** an agent supports no stronger channel +- **THEN** AgentTower connects at Tier 0 (pipe-pane observation + paste-buffer + delivery) and the agent is fully usable + +#### Scenario: Strongest available tier is selected per agent + +- **WHEN** two agents are registered, one Tier-1-capable and one not +- **THEN** AgentTower connects the capable agent at Tier 1 and the other at + Tier 0, independently + +### Requirement: Independent inbound and outbound axes + +AgentTower SHALL negotiate the inbound (observation/events) axis and the +outbound (delivery) axis independently per agent. An agent MAY have a higher +inbound tier than its outbound tier, or vice versa. + +#### Scenario: Asymmetric tiers on one agent + +- **WHEN** an interactive agent exposes Tier-1 inbound events but no structured + delivery inbox +- **THEN** AgentTower uses Tier-1 events for that agent while delivery remains on + the Tier-0 paste-buffer + +### Requirement: Router-first role boundary + +AgentTower SHALL act as the router: agents report *in* via hooks and/or an +AgentTower-hosted MCP server, and AgentTower SHALL NOT drive an agent's task loop +or intercept its tool calls. Action-level driving, sandboxing, and policy over an +agent's own tool calls SHALL be delegated to Omnigent (Tier 2). + +#### Scenario: AgentTower hosts the MCP server, agent is the client + +- **WHEN** an agent self-reports status via MCP +- **THEN** AgentTower receives the report as the MCP server and the agent acts as + the MCP client; AgentTower issues no command that drives the agent's task loop + +#### Scenario: Driving is delegated to Omnigent + +- **WHEN** an operator needs an agent driven and governed at the action level +- **THEN** AgentTower hands the agent off to Omnigent (Tier 2) and continues to + observe and route around it, rather than driving the agent itself + +### Requirement: Per-agent integration mode + +Each agent SHALL have an `integration` mode of `auto`, `tmux-only`, `native`, or +`omnigent`. `auto` SHALL be the default and SHALL probe for the strongest +available channel per axis and fall back to Tier 0 when probing fails or a +stronger tier is unsupported. + +#### Scenario: auto falls back to the floor + +- **WHEN** an agent's `integration` is `auto` and no stronger channel is + detected +- **THEN** AgentTower connects the agent at Tier 0 without error + +#### Scenario: native requires Tier 1 + +- **WHEN** an agent's `integration` is `native` but Tier 1 cannot be established +- **THEN** AgentTower reports an actionable failure rather than silently + downgrading + +#### Scenario: tmux-only forces the floor + +- **WHEN** an agent's `integration` is `tmux-only` +- **THEN** AgentTower connects only at Tier 0 even if a stronger channel is + available + +### Requirement: First-class adoption of pre-running panes + +AgentTower SHALL support upgrading adopted (already-running) panes to Tier 1 when +they were started pre-instrumented with AgentTower's shipped hook scripts and/or +MCP server. Adoption SHALL NOT downgrade an agent below the Tier-0 floor. + +#### Scenario: Pre-instrumented adopted pane lights up Tier 1 + +- **WHEN** an already-running pane that was started with AgentTower's hook + scripts / MCP server configured is adopted +- **THEN** AgentTower negotiates Tier 1 for it on adoption + +#### Scenario: Uninstrumented adopted pane stays on the floor + +- **WHEN** an already-running pane without AgentTower instrumentation is adopted +- **THEN** AgentTower connects it at Tier 0 and it remains fully usable + +### Requirement: No-network-listener invariant for all tiers + +No connection tier SHALL introduce a network listener. Tier-1 hook scripts SHALL +reach the daemon by invoking the `agenttower` CLI over the existing mounted Unix +socket, and the AgentTower MCP server SHALL be a stdio subprocess that bridges to +that same socket. AgentTower SHALL remain a client of any external server, +including Omnigent's localhost server. + +#### Scenario: Hooks reach the daemon over the Unix socket + +- **WHEN** a harness hook fires on an instrumented agent +- **THEN** the hook script delivers the event by calling the `agenttower` CLI + over the mounted Unix socket, opening no network listener + +#### Scenario: MCP server bridges over stdio to the socket + +- **WHEN** a harness launches AgentTower's MCP server +- **THEN** the server runs as a stdio subprocess and bridges to the host daemon + over the existing Unix socket, opening no network listener + +### Requirement: Tier-independent outbound safety + +Every outbound delivery, at every tier, SHALL pass the FEAT-009 permission gate +and respect the global routing kill switch, and SHALL be recorded in +`events.jsonl`. Upgrading the outbound tier SHALL NOT bypass the gate, the kill +switch, or the audit trail. + +#### Scenario: Tier-1 delivery still honors the kill switch + +- **WHEN** the global routing kill switch is engaged +- **THEN** an outbound Tier-1 structured delivery is blocked exactly as a Tier-0 + paste-buffer delivery would be + +#### Scenario: Every tier's delivery is audited + +- **WHEN** a message is delivered at any tier +- **THEN** an audit record for that delivery is written to `events.jsonl` diff --git a/openspec/changes/agent-connection-tiers/tasks.md b/openspec/changes/agent-connection-tiers/tasks.md new file mode 100644 index 00000000..08b9a58c --- /dev/null +++ b/openspec/changes/agent-connection-tiers/tasks.md @@ -0,0 +1,67 @@ +# Tasks: Agent Connection-Strength Tier Model + +> This change is a spec/design proposal. Implementation is sequenced into later +> features; the tasks below capture the spec-acceptance and the ordered +> implementation hand-off. Items marked **(impl)** belong to a future +> implementing feature, not to this change's merge. + +## 1. Spec authoring (this change) + +- [ ] 1.1 Define the `agent-integration` capability spec with the tier ladder, + two-axis negotiation, router-first boundary, adoption story, + no-listener invariant, integration modes, and tier-independent safety. +- [ ] 1.2 Cross-check the spec against existing constraints: `architecture.md` + §13 (classification), §16 (delivery), §23 (security/no-listener), §4.1 + (UID invariant), FEAT-009 (permission gate + kill switch). +- [ ] 1.3 Confirm no requirement contradicts the MVP constitution in + `openspec/config.yaml` (no network listener; AT routes, does not execute + workflow logic). + +## 2. Pre-implementation confirmations + +- [ ] 2.1 Confirm Codex exact flag spellings (headless `exec`, JSON output + mode, MCP server mode) against current Codex docs. Record findings before + any Codex task is written. +- [ ] 2.2 Confirm Claude Code hook event names and payloads + (PreToolUse / PostToolUse / Notification / Stop) and the MCP stdio + server registration shape against current Claude Code docs. +- [ ] 2.3 Decide whether negotiated tier is surfaced via the FEAT-011 `app.*` + backend in phase 1 or deferred. + +## 3. Phase 1 — Tier-1 inbound via hooks **(impl, future feature)** + +- [ ] 3.1 Ship AgentTower hook scripts (host-path-safe; relative/runtime paths). +- [ ] 3.2 Add an `agenttower report-event` CLI subcommand + daemon socket method + that maps harness hook events to AgentTower event types. +- [ ] 3.3 Per-agent `inbound_tier` negotiation with fall-back to tmux scraping. +- [ ] 3.4 Prove exact `waiting_for_input` / `completed` events replace inferred + classification for an instrumented agent (retires §13 problem for it). + +## 4. Phase 2 — AgentTower-hosted MCP server **(impl, future feature)** + +- [ ] 4.1 stdio MCP server bridging to the existing Unix socket (no listener). +- [ ] 4.2 `report_status` / `request_route` tools (agent reports IN; AT never + drives the agent). +- [ ] 4.3 Pre-instrumented adoption path: an adopted pane with the AT MCP server + configured lights up Tier 1; uninstrumented panes stay on the floor. + +## 5. Phase 3 — Tier-1 outbound structured delivery **(impl, future feature)** + +- [ ] 5.1 Per-agent `outbound_tier` negotiation, independent of `inbound_tier`. +- [ ] 5.2 Structured delivery adapter for the native harness inbox. +- [ ] 5.3 Route delivery through the FEAT-009 permission gate + kill switch + + `events.jsonl` audit for every tier (no bypass). + +## 6. Phase 4 — Tier-2 Omnigent hand-off **(impl, future feature)** + +- [ ] 6.1 Per-agent `integration=omnigent` mode + Omnigent session correlation. +- [ ] 6.2 Consume Omnigent structured events as the inbound source; AT routes + around the Omnigent-driven agent. +- [ ] 6.3 Document the two-policy-layer precedence (AT routing vs Omnigent + action policy). + +## 7. Validation + +- [ ] 7.1 Run `openspec validate agent-connection-tiers --strict` when the CLI + is available. +- [ ] 7.2 Confirm each ADDED requirement has at least one `#### Scenario:`. diff --git a/openspec/changes/app-collaboration/.openspec.yaml b/openspec/changes/app-collaboration/.openspec.yaml new file mode 100644 index 00000000..95ae5a2c --- /dev/null +++ b/openspec/changes/app-collaboration/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-06-18 diff --git a/openspec/changes/app-collaboration/README.md b/openspec/changes/app-collaboration/README.md new file mode 100644 index 00000000..7cd3b78d --- /dev/null +++ b/openspec/changes/app-collaboration/README.md @@ -0,0 +1,3 @@ +# app-collaboration + +Multi-operator app sessions: watch vs co-drive (V2, local-only) diff --git a/openspec/changes/app-collaboration/design.md b/openspec/changes/app-collaboration/design.md new file mode 100644 index 00000000..c91d149d --- /dev/null +++ b/openspec/changes/app-collaboration/design.md @@ -0,0 +1,91 @@ +# Design: Multi-Operator App Collaboration + +## Context + +FEAT-011 added the `app.*` namespace over the host daemon's Unix socket: a +versioned, host-only, session-based API (cap 8 concurrent sessions via +`app.hello`) that a packaged desktop control panel uses instead of scraping CLI +output. Today nothing distinguishes one app session from another in terms of +authority — every session can call every mutation. This design adds +collaboration semantics: multiple humans connected at once, with read-only +watchers and permitted co-drivers, while staying local and listener-free. + +## Goals / Non-Goals + +**Goals** +- Multiple concurrent operators see one consistent live view. +- A clear, server-enforced split between `observer` (read-only) and `operator` + (mutating) sessions. +- Cross-session auditability. + +**Non-Goals** +- Remote / URL / mobile sharing — requires an authenticated transport and a + reversal of the no-network-listener stance; explicitly deferred. +- External identity (OIDC) — the trust model stays same-host-UID + socket + permission, as in FEAT-011. +- Session *forking* (independent continuation) — candidate for a later change. + +## Decision 1 — Session roles over the existing socket + +`app.hello` issues a session; this change adds a session **role** of `observer` +or `operator`. The role is assigned at session establishment under the existing +same-host-UID trust model — no new credential or listener is introduced. The +8-session cap and in-memory session registry from FEAT-011 are reused. + +Rationale: collaboration is a property of *who is connected to the local +daemon*, not a new transport. Keeping it on the existing socket preserves the +constitution (`architecture.md` §23). + +## Decision 2 — Consistent shared view + +All sessions read the same SQLite registry and the same `events.jsonl` stream, +so any two connected clients converge on the same state. Event tailing is +per-session but sourced from the one daemon stream; no client holds private +authoritative state. + +Rationale: "watch" only has meaning if every watcher sees the same thing the +driver sees. + +## Decision 3 — Server-side co-drive enforcement + +Mutating `app.*` methods (the FEAT-011 operator mutations: `app.agent.update`, +`app.log.*`, `app.send_input`, `app.queue.*`, `app.route.*`) are permitted only +for `operator` sessions. `observer` sessions calling a mutation receive a +closed-set rejection. Where `routing-policy-layers` is present, co-drive +deliveries also pass the policy stack. + +Rationale: enforcement must be server-side; a read-only client must not be able +to mutate by crafting requests. + +## Decision 4 — Cross-session audit + +Each mutation records the originating app session id alongside the existing +audit fields, so "who did what" is answerable across simultaneous operators. + +## Decision 5 — Explicit local-only boundary + +This change is same-host socket only. Remote/URL/mobile sharing is named as a +non-goal with its gate: an authenticated network transport whose adoption is a +separate, deliberate decision (the deferred "reachable-from-anywhere" item). +This is the seam that future work would attach to. + +## Risks / Trade-offs + +- **Accidental co-drive** — default new sessions to `observer`; require an + explicit step to become `operator`. +- **Conflicting operators** — two operators mutating at once is serialized by + the daemon; multi-master arbitration (architecture §17) already covers + conflicting deliveries. +- **Scope creep toward remote** — the local-only boundary is a hard line in this + change. + +## Migration / Phasing + +1. Add session role to `app.hello` / the session registry. +2. Enforce observer/operator on mutating methods. +3. Cross-session mutation audit. + +## Open Questions + +- Is watch + co-drive sufficient for v1, or is session fork needed? +- Default role policy: always `observer` until explicitly promoted? diff --git a/openspec/changes/app-collaboration/proposal.md b/openspec/changes/app-collaboration/proposal.md new file mode 100644 index 00000000..6f66859c --- /dev/null +++ b/openspec/changes/app-collaboration/proposal.md @@ -0,0 +1,43 @@ +# Add Multi-Operator App Collaboration (Watch / Co-Drive) + +## Why + +Omnigent's headline collaboration feature is sharing a live agent session so +teammates can watch, co-drive, or fork it. AgentTower's analogue is letting +multiple operators connect to the **same** daemon and see the same registry and +event stream, with a clear split between read-only *watch* and permitted +*co-drive*. This answers PRD Open Question #2 (how multiple humans observe the +system) using the existing FEAT-011 `app.*` backend, without inventing a new +control plane. + +This is a **V2** capability: true remote/URL sharing requires an authenticated +network transport, which collides with the MVP no-network-listener stance. This +change therefore scopes collaboration to **local, same-host** app sessions and +records remote sharing as an explicit non-goal with the gate it would require. + +## What Changes + +- **Assign each app session a role:** `observer` (read-only) or `operator` + (may mutate). FEAT-011 already supports up to 8 concurrent app sessions. +- **Guarantee a consistent shared live view:** all connected sessions observe + the same registry and event stream. +- **Enforce co-drive server-side:** only `operator` sessions may perform + mutations; `observer` sessions are read-only, reusing the `app.*` permission + model (and `routing-policy-layers` where relevant). +- **Audit across sessions:** record which session performed which mutation. +- **Draw an explicit local-only boundary:** same-host socket only; remote / URL + sharing and mobile access are non-goals here, gated behind a future + authenticated-transport decision. + +## Impact + +- **New capability spec:** `app-collaboration` (extends the FEAT-011 `app.*` + surface; no new listener). +- **Builds on:** FEAT-011 (app backend + session registry). Ideally composes + with `routing-policy-layers` (policies can govern co-drive deliveries). +- **No code ships in this change** — spec/design proposal only. +- **Non-goals:** remote / mobile transport (the deferred "reachable-from- + anywhere" item), OIDC / external identity, and session *forking* semantics + (candidate to defer). +- **Open question:** is watch + co-drive sufficient for v1, or is session fork + required? diff --git a/openspec/changes/app-collaboration/specs/app-collaboration/spec.md b/openspec/changes/app-collaboration/specs/app-collaboration/spec.md new file mode 100644 index 00000000..4abcbe2d --- /dev/null +++ b/openspec/changes/app-collaboration/specs/app-collaboration/spec.md @@ -0,0 +1,98 @@ +# app-collaboration + +Capability: let multiple operators connect to the same local AgentTower daemon +and share one live view, with a server-enforced split between read-only watchers +and permitted co-drivers. Local, same-host only; no new network listener. + +## ADDED Requirements + +### Requirement: App session roles + +AgentTower SHALL assign each app session a role of `observer` (read-only) or +`operator` (may perform mutations), established under the existing FEAT-011 +same-host-UID trust model without introducing a new listener or persisted +credential. + +#### Scenario: Observer and operator sessions coexist + +- **WHEN** two app sessions connect, one as `observer` and one as `operator` +- **THEN** both are tracked in the session registry with their respective roles + +### Requirement: Default role and server-side operator promotion + +AgentTower SHALL default every new app session to `observer`. A session SHALL +become `operator` only through an explicit, server-side promotion recorded by +the daemon; a client SHALL NOT be able to self-select `operator` (e.g. via an +`app.hello` parameter). Because all sessions already share the FEAT-011 +same-host-UID trust model, this role is a **coordination control** to prevent +accidental concurrent co-drive, not a privilege boundary; existing pre-role +clients that predate this capability SHALL be treated as `observer` until +promoted. + +#### Scenario: New session defaults to observer + +- **WHEN** an app session is established (including a client that predates this + capability) +- **THEN** it is `observer` until an explicit server-side promotion occurs + +#### Scenario: Client cannot self-select operator + +- **WHEN** a client attempts to declare itself `operator` at connection time +- **THEN** AgentTower keeps it as `observer` and requires an explicit server-side + promotion to grant `operator` + +### Requirement: Consistent shared live view + +AgentTower SHALL present all connected app sessions with the same registry state +and the same event stream, sourced from the single daemon, so connected clients +converge on identical state. + +#### Scenario: Both sessions see a new event + +- **WHEN** a new event is emitted while an observer and an operator are both + connected +- **THEN** both sessions observe that event from the shared daemon stream + +### Requirement: Server-side co-drive enforcement + +AgentTower SHALL permit mutating `app.*` methods only for `operator` sessions +and SHALL reject mutating calls from `observer` sessions by reusing the existing +FEAT-011 closed-set error code `permission_denied` with +`details.reason = "observer_read_only"` (no new top-level error code is +introduced). Enforcement SHALL be server-side and SHALL NOT depend on client +cooperation. + +#### Scenario: Observer mutation is rejected + +- **WHEN** an `observer` session calls a mutating `app.*` method +- **THEN** AgentTower rejects the call with `permission_denied` / + `details.reason = "observer_read_only"` and performs no mutation + +#### Scenario: Operator mutation is allowed + +- **WHEN** an `operator` session calls a mutating `app.*` method +- **THEN** AgentTower performs the mutation through the same validation path as + the CLI + +### Requirement: Cross-session mutation audit + +AgentTower SHALL record the originating app session id on every mutation so that +"who did what" is answerable when multiple operators are connected. + +#### Scenario: Mutation records its session + +- **WHEN** an `operator` session performs a mutation +- **THEN** the audit record for that mutation identifies the originating app + session id + +### Requirement: Local-only collaboration boundary + +AgentTower SHALL scope collaboration to same-host app sessions over the existing +Unix socket. Remote, URL-based, or mobile sharing SHALL NOT be provided by this +capability and SHALL require a separately decided authenticated transport. + +#### Scenario: No remote sharing surface is exposed + +- **WHEN** collaboration is enabled +- **THEN** no network listener is opened and collaboration is reachable only + from same-host app sessions over the existing socket diff --git a/openspec/changes/app-collaboration/tasks.md b/openspec/changes/app-collaboration/tasks.md new file mode 100644 index 00000000..a4e7a558 --- /dev/null +++ b/openspec/changes/app-collaboration/tasks.md @@ -0,0 +1,37 @@ +# Tasks: Multi-Operator App Collaboration + +> Spec/design proposal (V2 capability). Implementation is sequenced into a later +> feature; items marked **(impl)** belong to that feature, not to this change's +> merge. + +## 1. Spec authoring (this change) + +- [ ] 1.1 Define the `app-collaboration` capability spec: session roles, shared + consistent view, server-side co-drive enforcement, cross-session audit, + and the explicit local-only boundary. +- [ ] 1.2 Cross-check against FEAT-011 (`app.*`, session registry, host-only, + 8-session cap) and `architecture.md` §23 (no network listener). +- [ ] 1.3 Confirm remote/URL/mobile sharing is recorded only as a gated + non-goal, not a requirement. + +## 2. Pre-implementation decisions + +- [ ] 2.1 Decide whether session fork is in v1 or deferred. +- [ ] 2.2 Decide the default session role and the promotion step to `operator`. + +## 3. Implementation **(impl, future feature)** + +- [ ] 3.1 Add an `observer` / `operator` role to `app.hello` + the FEAT-011 + session registry (no new listener, no new persisted credential). +- [ ] 3.2 Ensure all connected sessions share one consistent registry/event + view. +- [ ] 3.3 Enforce co-drive server-side: only `operator` sessions may call + mutating `app.*` methods; `observer` mutations are rejected. +- [ ] 3.4 Record the originating app session id on every mutation audit entry. +- [ ] 3.5 Compose with `routing-policy-layers` for co-drive deliveries where + present. + +## 4. Validation + +- [ ] 4.1 `openspec validate app-collaboration --strict`. +- [ ] 4.2 Confirm each ADDED requirement has at least one `#### Scenario:`. diff --git a/openspec/changes/discovery-backends/.openspec.yaml b/openspec/changes/discovery-backends/.openspec.yaml new file mode 100644 index 00000000..95ae5a2c --- /dev/null +++ b/openspec/changes/discovery-backends/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-06-18 diff --git a/openspec/changes/discovery-backends/README.md b/openspec/changes/discovery-backends/README.md new file mode 100644 index 00000000..4711129a --- /dev/null +++ b/openspec/changes/discovery-backends/README.md @@ -0,0 +1,3 @@ +# discovery-backends + +Pluggable discovery/substrate backends behind one interface diff --git a/openspec/changes/discovery-backends/design.md b/openspec/changes/discovery-backends/design.md new file mode 100644 index 00000000..b37492ef --- /dev/null +++ b/openspec/changes/discovery-backends/design.md @@ -0,0 +1,109 @@ +# Design: Pluggable Discovery / Substrate Backends + +## Context + +AgentTower discovers work in exactly one way today: the host daemon runs +`docker ps` to find bench containers, then `docker exec -u "$USER" … tmux +list-panes` to find panes inside them (FEAT-003 / FEAT-004). Pane identity is +`(container, tmux socket, session, window, pane index, pane id)`. The registry, +log attachment, event pipeline, and routing all assume this single substrate. +This design extracts a backend interface so additional substrates become +additive, not invasive. + +## Goals / Non-Goals + +**Goals** +- One interface that any substrate implements to feed the existing registry. +- Bench-container discovery becomes the first backend with zero behavior change. +- A second (host-only tmux) backend proves the abstraction. +- Backend failures are isolated; identities never collide across backends. + +**Non-Goals** +- Cloud/remote bench backend implementation (interface + host-only reference + only). +- Omnigent sessions as a backend — that is the Tier 2 hand-off in + `agent-connection-tiers`, not a discovery substrate here. +- Changing the registry/event/routing data model beyond identity namespacing. + +## Decision 1 — The `DiscoveryBackend` interface + +A backend exposes: +- `enumerate_targets()` — the hosts/containers it manages. +- `enumerate_panes(target)` — tmux panes within a target. +- `attach_log_capability(pane)` — whether/how host-visible logging is possible + for that pane (so the FEAT-007 invariant is a backend property, not a global + assumption). +- `identity_key(pane)` — a stable, backend-namespaced key. + +Rationale: this is the minimal surface the current daemon already needs; it just +names it so it can be implemented more than once. + +## Decision 2 — Identity namespacing + +Every backend prefixes its identities with a backend id (e.g. `bench:` vs +`host:`). tmux pane ids (`%4`) are only unique within a tmux server, so the +registry key becomes `(backend_id, target_id, tmux_socket, pane_id, …)`. + +Rationale: prevents a host-tmux `%4` from colliding with a container-tmux `%4`, +and lets the same physical machine be seen by two backends without aliasing. + +### Migration of existing bench-only deployments (concrete plan) + +Existing deployments have rows written before namespacing existed; they carry no +backend prefix. The migration is deterministic and tied to the schema-version +bump that introduces the `backend_id` column: + +1. **At upgrade (one-time, in the schema migration that adds `backend_id`):** + every existing `agents` / `panes` / `log_offsets` row is stamped + `backend_id = 'bench'` in a single transaction, because pre-namespacing + discovery could only have come from the bench-container substrate. The + migration is **idempotent** (guarded by the schema version) and **history- + preserving** (it rewrites the key column in place; it does not delete or + re-create rows, so events, offsets, and queue references stay attached). +2. **Read-time shim during the transition:** any lookup that receives a legacy + key with no prefix is treated as `bench:`, so an in-flight client or a + not-yet-migrated reference resolves correctly until step 1 completes. +3. **No dual-write:** after the migration runs, all writers emit prefixed keys + only; the shim exists solely to tolerate legacy reads and can be removed a + release later. + +This makes the bench backend's identities `bench:`-namespaced with zero history +loss and no operator action, and gives downstream schema work a fixed contract +to build on. + +## Decision 3 — Per-backend degraded isolation + +Each backend scan runs and degrades independently. A backend that throws (Docker +socket down, tmux missing) is marked degraded with an actionable status; other +backends keep scanning. This generalizes FEAT-003/004's existing +graceful-degradation requirement from "Docker" to "any backend". + +## Decision 4 — Bench backend first, host-only backend second + +- **Backend #1 (bench-container):** a refactor of FEAT-003/004 behind the + interface. Acceptance is "no observable behavior change". +- **Backend #2 (host-only tmux):** enumerate the host user's tmux sockets + directly (no `docker exec`). Proves the interface and delivers the + long-deferred host-only discovery item. + +## Risks / Trade-offs + +- **Refactor risk** — bench backend must be behavior-preserving; cover with the + existing FEAT-003/004 integration tests before extraction. +- **Identity migration** — addressed concretely in Decision 2: a one-time, + idempotent, history-preserving stamp of existing rows to `backend_id='bench'` + plus a transition read-time shim for legacy unprefixed keys. +- **Config surface growth** — keep per-backend config minimal for MVP. + +## Migration / Phasing + +1. Extract the interface; wrap current Docker logic as the bench backend. +2. Add the backend registry + config + identity namespacing (back-compat for + existing rows). +3. Add the host-only tmux backend. + +## Open Questions + +- Should backend selection be global config or per-bench/per-host overrides? +- How long should the legacy-key read-time shim (Decision 2, step 2) be retained + before removal — one release, or longer for slow upgraders? diff --git a/openspec/changes/discovery-backends/proposal.md b/openspec/changes/discovery-backends/proposal.md new file mode 100644 index 00000000..ec2bcba9 --- /dev/null +++ b/openspec/changes/discovery-backends/proposal.md @@ -0,0 +1,45 @@ +# Add Pluggable Discovery / Substrate Backends + +## Why + +AgentTower's discovery is hardwired to one substrate: bench containers found via +`docker ps`, with tmux panes enumerated through `docker exec` (FEAT-003 / +FEAT-004). Every later layer — registry, logs, events, routing — assumes that +single source. Databricks' Omnigent treats execution substrates as +interchangeable; AgentTower should similarly abstract *where panes/agents come +from* so host-only tmux and future remote/cloud benches can be peer backends +behind one interface — without rewriting the registry/event/routing stack each +time. (Omnigent sessions are explicitly **not** a discovery backend; they are the +Tier-2 hand-off in `agent-connection-tiers`. See Non-goals.) + +This change defines a `DiscoveryBackend` interface, refactors bench-container +discovery to be its first implementation (no behavior change), and adds a +host-only tmux backend as a second reference implementation to prove the seam. + +## What Changes + +- **Define a `DiscoveryBackend` interface contract:** enumerate hosts/ + containers, enumerate panes, probe attach-log capability, and produce stable + identity keys. +- **Refactor bench-container discovery as backend #1** — a pure refactor with no + behavior change. +- **Add a backend registry + config:** enable/disable per backend, per-backend + configuration, and identity namespacing so two backends cannot collide on tmux + pane ids. +- **Isolate degraded state per backend:** one backend failing (e.g. Docker + down) must not crash or degrade others. +- **Add a host-only tmux backend as reference #2** to validate the abstraction + and land a long-standing "later" item. + +## Impact + +- **New capability spec:** `discovery-backends`. +- **Builds on / refactors:** FEAT-003 (container discovery), FEAT-004 (container + tmux discovery). Registry/event/routing layers consume backends through the + interface rather than calling Docker directly. +- **Independent of** `routing-policy-layers` and `app-collaboration`. +- **No code ships in this change** — spec/design proposal only. +- **Non-goals:** implementing a cloud/remote bench backend (interface + + host-only reference only); Omnigent-as-a-backend (that is Tier 2 in + `agent-connection-tiers`, handled separately). +- **Open question:** the cross-backend identity-keying scheme. diff --git a/openspec/changes/discovery-backends/specs/discovery-backends/spec.md b/openspec/changes/discovery-backends/specs/discovery-backends/spec.md new file mode 100644 index 00000000..bb5263cc --- /dev/null +++ b/openspec/changes/discovery-backends/specs/discovery-backends/spec.md @@ -0,0 +1,75 @@ +# discovery-backends + +Capability: abstract *where AgentTower discovers panes/agents from* behind a +pluggable backend interface, so multiple substrates (bench containers, host-only +tmux, future remote benches) feed the same registry without rewriting it. + +## ADDED Requirements + +### Requirement: Discovery backend interface + +AgentTower SHALL define a discovery backend interface that enumerates targets +(hosts/containers), enumerates tmux panes within a target, probes host-visible +log-attachment capability per pane, and produces a stable identity key. The +registry, log, event, and routing layers SHALL consume panes through this +interface rather than calling a specific substrate (Docker) directly. + +#### Scenario: Registry consumes panes via the interface + +- **WHEN** a backend enumerates panes +- **THEN** the panes enter the registry through the backend interface, with no + layer above discovery calling Docker directly + +### Requirement: Bench-container discovery as a backend + +AgentTower SHALL provide bench-container discovery (FEAT-003 / FEAT-004) as a +backend implementation with no observable change in behavior versus the current +hardwired discovery. + +#### Scenario: Bench backend behavior is unchanged + +- **WHEN** only the bench-container backend is enabled +- **THEN** the panes and containers discovered are identical to the pre-refactor + behavior + +### Requirement: Backend identity namespacing + +AgentTower SHALL namespace pane/agent identities by backend so two backends +cannot collide on tmux pane ids. The registry key SHALL include the backend id +in addition to the existing tmux coordinates. + +#### Scenario: Same pane id across backends does not collide + +- **WHEN** a host-only backend reports pane `%4` and a bench backend reports + pane `%4` +- **THEN** AgentTower treats them as two distinct agents, namespaced by backend + +#### Scenario: Existing bench-only rows are migrated without history loss + +- **WHEN** a deployment created before namespacing is upgraded +- **THEN** every existing agent/pane identity is stamped into the `bench` + namespace by a one-time idempotent migration, with its events, offsets, and + queue references preserved, and no operator action required + +### Requirement: Per-backend degraded isolation + +AgentTower SHALL run and degrade each backend independently. A backend that +fails (e.g. its runtime is unavailable) SHALL be marked degraded with an +actionable status while other backends continue scanning. + +#### Scenario: One backend down does not stop others + +- **WHEN** the bench backend's Docker runtime is unavailable but a host-only + backend is healthy +- **THEN** the bench backend is reported degraded and the host-only backend + still discovers panes + +### Requirement: Backend enablement configuration + +AgentTower SHALL allow each backend to be enabled or disabled in configuration. +A disabled backend SHALL produce no scans and no discovered panes. + +#### Scenario: Disabled backend produces no scans + +- **WHEN** a backend is disabled in configuration +- **THEN** AgentTower performs no discovery through that backend diff --git a/openspec/changes/discovery-backends/tasks.md b/openspec/changes/discovery-backends/tasks.md new file mode 100644 index 00000000..f3fddee6 --- /dev/null +++ b/openspec/changes/discovery-backends/tasks.md @@ -0,0 +1,39 @@ +# Tasks: Pluggable Discovery / Substrate Backends + +> Spec/design proposal. Implementation is sequenced into a later feature; items +> marked **(impl)** belong to that feature, not to this change's merge. + +## 1. Spec authoring (this change) + +- [ ] 1.1 Define the `discovery-backends` capability spec: the backend + interface, identity namespacing, per-backend degraded isolation, and the + two reference backends. +- [ ] 1.2 Cross-check against FEAT-003 / FEAT-004 (current discovery) and + FEAT-007 (host-visible-log invariant as a backend property). +- [ ] 1.3 Confirm the refactor is behavior-preserving for the bench backend. + +## 2. Pre-implementation decisions + +- [ ] 2.1 Decide the cross-backend identity-key format and migration for + already-registered agents. +- [ ] 2.2 Decide global vs per-target backend configuration. + +## 3. Implementation **(impl, future feature)** + +- [ ] 3.1 Define the `DiscoveryBackend` interface (enumerate targets / panes, + attach-log capability probe, identity_key). +- [ ] 3.2 Refactor bench-container discovery (FEAT-003/004) as backend #1 with + no behavior change; guard with existing integration tests. +- [ ] 3.3 Add a backend registry + per-backend config + enable/disable. +- [ ] 3.4 Implement identity namespacing (`backend_id` prefix). +- [ ] 3.4a Ship the one-time, idempotent, history-preserving migration that + stamps existing rows `backend_id='bench'` (tied to the schema-version + bump), plus the transition read-time shim for legacy unprefixed keys. +- [ ] 3.5 Per-backend degraded-state isolation (one backend's failure does not + affect others). +- [ ] 3.6 Add the host-only tmux backend as reference #2. + +## 4. Validation + +- [ ] 4.1 `openspec validate discovery-backends --strict`. +- [ ] 4.2 Confirm each ADDED requirement has at least one `#### Scenario:`. diff --git a/openspec/changes/routing-policy-layers/.openspec.yaml b/openspec/changes/routing-policy-layers/.openspec.yaml new file mode 100644 index 00000000..95ae5a2c --- /dev/null +++ b/openspec/changes/routing-policy-layers/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-06-18 diff --git a/openspec/changes/routing-policy-layers/README.md b/openspec/changes/routing-policy-layers/README.md new file mode 100644 index 00000000..1f0c092b --- /dev/null +++ b/openspec/changes/routing-policy-layers/README.md @@ -0,0 +1,3 @@ +# routing-policy-layers + +Layered routing/input delivery policies (global -> per-agent -> per-route) diff --git a/openspec/changes/routing-policy-layers/design.md b/openspec/changes/routing-policy-layers/design.md new file mode 100644 index 00000000..f9a634f5 --- /dev/null +++ b/openspec/changes/routing-policy-layers/design.md @@ -0,0 +1,107 @@ +# Design: Layered Routing/Input Delivery Policies + +## Context + +AgentTower is a router: it delivers structured prompts/notifications between tmux +panes, gated by FEAT-009's permission model (per-pane `send_input_allowed` +derived from role) and a single global routing kill switch (`daemon_state. +routing_enabled`). FEAT-010 adds route subscriptions that enqueue through the +same gate. There is no graduated, contextual control between "fully allowed" and +"globally off". This design adds a layered policy model that sits in the +delivery path without changing AgentTower's router identity. + +## Goals / Non-Goals + +**Goals** +- A deterministic, auditable policy stack (global → per-agent → per-route). +- Policy types that a router can actually enforce on delivery. +- Full backward compatibility with FEAT-009 defaults. + +**Non-Goals** +- Governing an agent's own tool calls / shell / file / network actions — that is + Tier 2 (Omnigent), per `agent-connection-tiers`. +- LLM-based or semantic policy evaluation. +- Per-agent explicit grant *identity* models beyond role+capability (future). + +## Decision 1 — Layered resolution and precedence + +Policies attach at three scopes and resolve in a fixed order: + +1. **global** — applies to all deliveries (the kill switch is the canonical + global policy). +2. **per-agent** — keyed by target `agent_id` (and optionally source). +3. **per-route** — keyed by `route_id` (FEAT-010). + +Policies are **restrictive-only**: each layer may deny, hold, or tighten a cap, +but no layer can broaden what a lower-specificity layer (or the FEAT-009 base +permission) already allows. The effective decision is the **most restrictive +across all layers** — concretely, **deny-wins for allow/deny** (a deny at any +layer blocks, and a global deny is absolute and never overridable), and +**most-restrictive-wins (minimum)** for numeric caps. The fixed scope order +(global → per-agent → per-route) only matters for attribution in the audit +trail, not for the outcome, since the most-restrictive rule wins regardless of +layer. + +Rationale: deterministic, explainable, and safe-by-default — a more specific +layer can only narrow delivery, never silently re-open it. + +## Decision 2 — Router-appropriate policy types (MVP set) + +- **rate cap** — max N deliveries per target per time window. +- **require-approval** — hold delivery until an operator approves. This maps onto + the existing FEAT-009 queue model: the message sits in the `blocked` state and + is released to `queued`/`delivered` via `agenttower queue approve` (FEAT-009's + closed state set is `queued`, `blocked`, `delivered`, `canceled`, `failed` — + there is no separate `approved` state; approval is the operator action that + moves a `blocked` row forward). +- **allow/deny by source** — match on source role + capability. +- **target-busy / quiet hold** — defer delivery while the target is mid-command + or within a configured quiet window (conservative; ties to the + `architecture.md` §16 idle question). +- **global kill switch** — the existing switch, modeled as the top global deny. + +All default to the *least-surprising* behavior: absent any policy, deliveries +behave exactly as FEAT-009 today. + +## Decision 3 — Policy decisions are audited (without breaking the no-policy path) + +When a user-defined policy participates in a decision, AgentTower emits one +`events.jsonl` record — `policy_allowed` / `policy_blocked` / `policy_held` — +carrying the deciding layer, the matched rule id, and the target/route. This +mirrors FEAT-010's `route_matched` / `route_skipped` auditability so the full +delivery chain stays inspectable via `agenttower events`. + +Crucially, the **no-policy default path emits no policy audit rows**: when only +the FEAT-009 base layer applies, no `policy_*` event is written. This preserves +the byte-identical-stream guarantee in Decision 4 and avoids breaking existing +FEAT-009/010 audit consumers and compatibility tests. + +## Decision 4 — Backward compatibility + +`send_input_allowed` and `routing_enabled` are re-expressed as the base layer of +the model: an `unknown` target carries an implicit per-agent deny; the kill +switch is the global deny. A deployment with no user-defined policies produces +byte-identical delivery behavior to pre-policy AgentTower. + +## Risks / Trade-offs + +- **Complexity vs predictability** — mitigated by the fixed precedence + the two + simple merge rules (deny-wins, min-wins). +- **Hold/approval UX** — pending-approval deliveries must be visible and + cancellable (reuse `agenttower queue`). +- **Policy DSL scope creep** — MVP should ship fixed rule-types; a DSL is an + open question, not a commitment. + +## Migration / Phasing + +1. Schema: add a `policies` table + the three scope keys. +2. Evaluate policies in the existing delivery path (no new delivery surface). +3. CLI + app-backend read/write. +4. Backward-compat mapping of FEAT-009 defaults. + +## Open Questions + +- Fixed rule-types only, or a minimal policy DSL, for the first implementing + feature? +- Should rate-cap windows be per-source, per-target, or per-(source,target) + pair by default? diff --git a/openspec/changes/routing-policy-layers/proposal.md b/openspec/changes/routing-policy-layers/proposal.md new file mode 100644 index 00000000..5454c97d --- /dev/null +++ b/openspec/changes/routing-policy-layers/proposal.md @@ -0,0 +1,47 @@ +# Add Layered Routing/Input Delivery Policies + +## Why + +AgentTower's delivery safety today is two coarse knobs: a per-pane +`send_input_allowed` boolean (FEAT-009) and a single global routing kill switch. +There is no way to say "this master may send at most N prompts per minute to a +slave", "hold deliveries to this target for approval", or "deny delivery from +this capability" without code changes. Databricks' Omnigent demonstrated the +value of *stacked, contextual* policies; AgentTower needs the same expressiveness +— but scoped to what a **router** legitimately controls: routing and input +*delivery* decisions, never the agent's own tool calls (which AgentTower cannot +observe; that is Tier 2 / Omnigent territory). + +This change adds a layered policy model that resolves global → per-agent → +per-route rules deterministically and gates every outbound delivery, composing +with the connection tiers from `agent-connection-tiers` (which already requires +tier-independent outbound safety). + +## What Changes + +- **Introduce a layered policy model** resolved in a fixed precedence order: + global → per-agent → per-route, with a deterministic merge. +- **Define router-appropriate policy types:** per-target delivery rate caps, + require-approval (ask-before-deliver), allow/deny by source role + capability, + target-busy / quiet holds, and the global kill switch modeled as the + top-level global policy. +- **Audit every decision:** each allow / block / hold writes an `events.jsonl` + record naming the deciding layer and rule. +- **Make policies declarative and inspectable** via an `agenttower policy` + CLI surface and the FEAT-011 app backend. +- **Preserve backward compatibility:** the existing `send_input_allowed` flag + and global kill switch map onto the new model as the base layer; with no + policies defined, behavior is identical to today. + +## Impact + +- **New capability spec:** `routing-policy`. +- **Builds on:** FEAT-009 (permission gate + kill switch), FEAT-010 (routes), + and `agent-connection-tiers` (every outbound tier passes the gate). +- **Touches (in implementing features):** the delivery path, SQLite schema (a + policies table), the audit event surface, the CLI, and the app backend. +- **No code ships in this change** — spec/design proposal only. +- **Non-goals:** governing an agent's own shell/file/network actions (Tier 2 / + Omnigent); LLM-based policy evaluation. +- **Open question:** fixed rule-types vs a small policy DSL for the first + implementing feature. diff --git a/openspec/changes/routing-policy-layers/specs/routing-policy/spec.md b/openspec/changes/routing-policy-layers/specs/routing-policy/spec.md new file mode 100644 index 00000000..825bf009 --- /dev/null +++ b/openspec/changes/routing-policy-layers/specs/routing-policy/spec.md @@ -0,0 +1,96 @@ +# routing-policy + +Capability: layered, contextual control over routing and input *delivery* +decisions in AgentTower. Scoped to what a router controls; it does not govern an +agent's own tool calls. + +## ADDED Requirements + +### Requirement: Layered policy resolution + +AgentTower SHALL compute each delivery decision across all applicable policy +layers (global, per-agent, per-route) using **most-restrictive-wins**: deny-wins +for allow/deny rules (a deny at any layer blocks, and a global deny is absolute +and never overridable) and minimum-wins for numeric limits. The scope order +(global → per-agent → per-route) SHALL affect only audit attribution and +tie-break labeling, never the outcome. + +#### Scenario: A more specific allow cannot override a broader deny + +- **WHEN** a per-agent layer denies delivery and a per-route layer allows it +- **THEN** delivery is blocked (deny-wins), regardless of the per-route allow + +#### Scenario: Global deny is absolute + +- **WHEN** a global policy denies routing (the kill switch is engaged) +- **THEN** delivery is blocked regardless of any per-agent or per-route policy + +#### Scenario: Most-restrictive numeric limit wins + +- **WHEN** a per-agent rate cap of 10/min and a per-route rate cap of 3/min both + apply to a delivery +- **THEN** the effective cap is 3/min + +### Requirement: Router-scoped policy types + +AgentTower SHALL support delivery policy types that a router can enforce: +per-target rate caps, require-approval holds, allow/deny by source role and +capability, and target-busy / quiet holds. Policies SHALL NOT attempt to govern +an agent's own shell, file, or network actions. + +#### Scenario: Rate cap blocks excess deliveries + +- **WHEN** a per-target rate cap of 3 per minute is configured and a fourth + delivery to that target is attempted within the window +- **THEN** the fourth delivery is blocked until the window resets + +#### Scenario: Require-approval holds delivery + +- **WHEN** a require-approval policy matches a delivery +- **THEN** the delivery is held in a pending state and is not delivered until an + operator approves it + +#### Scenario: Deny by source capability + +- **WHEN** a policy denies delivery from source capability `shell` +- **THEN** a delivery originating from a `shell`-capability agent is blocked + +### Requirement: Policy decision auditing + +AgentTower SHALL write an audit record to `events.jsonl` for every decision in +which a user-defined policy participates, using the event type `policy_allowed`, +`policy_blocked`, or `policy_held` to name the outcome, and carrying the deciding +layer and the matched rule. When only the FEAT-009 base layer applies (no +user-defined policy participates), AgentTower SHALL NOT emit any policy audit +record, so the no-policy event stream is byte-identical to pre-policy +AgentTower. + +#### Scenario: Blocked delivery is audited + +- **WHEN** a delivery is blocked by a per-route deny +- **THEN** an audit record is appended to `events.jsonl` identifying the + per-route layer and the matched rule + +#### Scenario: No-policy delivery emits no policy audit record + +- **WHEN** a delivery is allowed and no user-defined policy participated in the + decision +- **THEN** no policy audit record is written and the event stream matches + pre-policy AgentTower exactly + +### Requirement: Backward-compatible defaults + +AgentTower SHALL express the existing `send_input_allowed` permission and global +routing kill switch as the base layer of the policy model. With no user-defined +policies present, delivery behavior SHALL be identical to pre-policy AgentTower. + +#### Scenario: No policies means unchanged behavior + +- **WHEN** no user-defined policies exist +- **THEN** deliveries are gated exactly as the FEAT-009 permission model and + global kill switch would gate them + +#### Scenario: Unknown target stays denied + +- **WHEN** the delivery target has role `unknown` +- **THEN** the base layer denies delivery as it does today diff --git a/openspec/changes/routing-policy-layers/tasks.md b/openspec/changes/routing-policy-layers/tasks.md new file mode 100644 index 00000000..91b56bd3 --- /dev/null +++ b/openspec/changes/routing-policy-layers/tasks.md @@ -0,0 +1,38 @@ +# Tasks: Layered Routing/Input Delivery Policies + +> Spec/design proposal. Implementation is sequenced into a later feature; items +> marked **(impl)** belong to that feature, not to this change's merge. + +## 1. Spec authoring (this change) + +- [ ] 1.1 Define the `routing-policy` capability spec: layered resolution, + precedence + merge rules, MVP policy types, decision auditing, and + backward compatibility. +- [ ] 1.2 Cross-check against FEAT-009 (gate + kill switch), FEAT-010 (routes), + and `agent-connection-tiers` tier-independent outbound safety. +- [ ] 1.3 Confirm no requirement governs agent tool calls (router-only scope). + +## 2. Pre-implementation decisions + +- [ ] 2.1 Decide fixed rule-types vs minimal DSL for the first implementing + feature. +- [ ] 2.2 Decide default rate-cap keying (source / target / pair). + +## 3. Implementation **(impl, future feature)** + +- [ ] 3.1 Add a `policies` table (schema bump) keyed by scope (global / agent / + route). +- [ ] 3.2 Implement deterministic resolution (deny-wins, min-wins) in the + existing delivery path. +- [ ] 3.3 Implement MVP policy types: rate cap, require-approval, allow/deny by + source role+capability, target-busy/quiet hold, global kill switch. +- [ ] 3.4 Emit `policy_allowed` / `policy_blocked` / `policy_held` audit events. +- [ ] 3.5 `agenttower policy {list,add,remove,show}` CLI. +- [ ] 3.6 Surface policies + decisions through the FEAT-011 app backend. +- [ ] 3.7 Map FEAT-009 `send_input_allowed` + kill switch onto the base layer; + prove no-policy behavior is unchanged. + +## 4. Validation + +- [ ] 4.1 `openspec validate routing-policy-layers --strict`. +- [ ] 4.2 Confirm each ADDED requirement has at least one `#### Scenario:`.