From 25a12876dc898977d389d40dc46a3113afe9720f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Jun 2026 04:47:22 +0000 Subject: [PATCH 1/9] docs: add Omnigent comparison and derived backlog Add docs/related-work-omnigent.md comparing AgentTower (non-invasive tmux observer/router) with Databricks' Omnigent meta-harness (agent wrapper/runtime), and record the Omnigent-derived feature ideas that fit AgentTower's model in the mvp-feature-sequence "Later Features" backlog and architecture.md V2 Scope. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_015SpYMhCsyyGMEyNrxkWGCc --- docs/architecture.md | 13 ++++ docs/mvp-feature-sequence.md | 26 +++++++ docs/related-work-omnigent.md | 133 ++++++++++++++++++++++++++++++++++ 3 files changed, 172 insertions(+) create mode 100644 docs/related-work-omnigent.md diff --git a/docs/architecture.md b/docs/architecture.md index 84e09512..69405c5c 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -881,6 +881,19 @@ Likely V2 work: - file event adapters - Python-thread and mailbox bridge support if needed - local LLM-assisted classification as an optional classifier plugin +- layered routing/input policy stack (global → per-agent → per-route), + generalizing the per-pane `send_input_allowed` boolean and the global routing + kill switch — see `docs/related-work-omnigent.md` §6 +- multi-operator session sharing over the FEAT-011 `app.*` backend (read-only + "watch" vs. permitted "co-drive") — see `docs/related-work-omnigent.md` §6 +- pluggable discovery/substrate backends (bench container as one backend among + host-only and future remote/cloud benches) — see + `docs/related-work-omnigent.md` §6 + +Two further Omnigent-derived ideas (authenticated remote/mobile surface; agent +network-egress policy) are recorded in `docs/related-work-omnigent.md` §6 but +conflict with the local-first, no-network-listener stance and require an +explicit decision before they enter scope. ## 25. Open Questions diff --git a/docs/mvp-feature-sequence.md b/docs/mvp-feature-sequence.md index 19ec27fa..c368502e 100644 --- a/docs/mvp-feature-sequence.md +++ b/docs/mvp-feature-sequence.md @@ -557,3 +557,29 @@ These are intentionally after FEAT-013: - Host-only tmux discovery. - Optional in-container relay. - Antigravity tmux backend. + +### Related-work-derived backlog (Omnigent) + +The Databricks Omnigent meta-harness (see `docs/related-work-omnigent.md` for +the full comparison) surfaced a few ideas that fit AgentTower's non-invasive +model. They are recorded here as candidate later features, not committed scope: + +- **Layered routing/input policies** — generalize the per-pane + `send_input_allowed` boolean + global kill switch into a global → per-agent → + per-route policy stack (spend/rate-style caps on delivery, ask-before-deliver). + This lands the existing "explicit per-agent permission grants" V2 item. +- **Multi-operator session sharing over the FEAT-011 app backend** — multiple + humans observing the same registry/event stream, read-only "watch" vs. + permitted "co-drive". Natural answer to PRD Open Question #2 (TUI vs. web). +- **Pluggable substrate backends** — treat the bench container as one discovery + backend among several (host-only tmux, future remote/cloud bench) behind the + existing discovery interface; additive to host-only tmux discovery. + +The following two are noted for completeness but conflict with the local-first, +no-network-listener constitution and require an explicit decision before pursuit: + +- **Reachable-from-anywhere / mobile surface** — only behind an authenticated + transport reusing the FEAT-011 contract. +- **Network-egress policy at the boundary** — AgentTower cannot intercept an + unmodified agent's syscalls; at most document an opt-in container-network + recommendation, not implement interception. diff --git a/docs/related-work-omnigent.md b/docs/related-work-omnigent.md new file mode 100644 index 00000000..2cdc5301 --- /dev/null +++ b/docs/related-work-omnigent.md @@ -0,0 +1,133 @@ +# Related Work: Databricks Omnigent + +Status: Draft v0.1 +Date: 2026-06-18 + +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 +features worth pulling into the AgentTower backlog. + +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 worth adopting + +The following ideas fit AgentTower's non-invasive thesis and are recorded in the +backlog (`docs/mvp-feature-sequence.md` "Later Features" and `architecture.md` +§24 V2 Scope). 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. From 6b1be3d1be520dc6bdf6c99362fa943e711bcf9c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Jun 2026 07:41:13 +0000 Subject: [PATCH 2/9] docs: add connection-strength integration tiers model Add docs/integration-tiers.md defining the per-agent connection ladder (Tier 0 tmux floor -> Tier 1 native harness hooks/MCP/stream-json -> Tier 2 Omnigent) with independent inbound/outbound axes. Settle the router-first division of labor: AgentTower stays the router (agents report in via hooks and an AgentTower-hosted MCP server), Omnigent is the puppet master for action-level driving. Adoption of pre-instrumented panes is first-class; both Tier-1 mechanisms bridge via the existing Unix socket so the no-network-listener invariant holds. Cross-reference the new model from related-work-omnigent.md (Omnigent = Tier 2) and add an ordered backlog to mvp-feature-sequence.md. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_015SpYMhCsyyGMEyNrxkWGCc --- docs/integration-tiers.md | 158 ++++++++++++++++++++++++++++++++++ docs/mvp-feature-sequence.md | 22 +++++ docs/related-work-omnigent.md | 5 ++ 3 files changed, 185 insertions(+) create mode 100644 docs/integration-tiers.md diff --git a/docs/integration-tiers.md b/docs/integration-tiers.md new file mode 100644 index 00000000..d6e6eb98 --- /dev/null +++ b/docs/integration-tiers.md @@ -0,0 +1,158 @@ +# AgentTower Integration Tiers + +Status: Draft v0.1 +Date: 2026-06-18 + +This note defines AgentTower's **connection-strength model**: how AgentTower +connects to an agent ranges from a universal, fully non-invasive tmux floor up +to a uniform meta-harness channel, negotiated per agent. It generalizes the +Omnigent comparison (`docs/related-work-omnigent.md`) — Omnigent is **Tier 2** +of this broader architecture, not a standalone bolt-on. + +It records two settled decisions: + +1. **AgentTower is the router, not the puppeteer.** Even at strong tiers, + AgentTower observes and routes; agents report *into* it. It does not drive an + agent's task loop or intercept its tool calls. +2. **Omnigent is the puppet master.** When an agent needs to be driven, + sandboxed, and governed at the action level, that role belongs to Omnigent + (Tier 2). AgentTower hands the agent off; it does not try to become a + competing runtime. + +## 1. The connection-strength ladder + +Per agent, AgentTower negotiates the strongest available channel and always +falls back to the tmux floor. + +| 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 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 | + +Tier 0 is the guaranteed floor and never goes away. Tiers 1–2 are upgrades. + +## 2. Two independent axes + +"Strong integration" is **not** a single switch. The inbound (observation) and +outbound (delivery) axes negotiate **independently** per agent. + +A discovered interactive Claude pane can keep its human-in-the-loop TUI, upgrade +its *events* to Tier 1 via hooks, while its *input* stays Tier 0 paste-buffer. +That asymmetric state is the expected common case, not a degenerate one. + +This split directly retires two pain points baked into the current +architecture: + +- **`architecture.md` §13 conservative classification** — Tier-1 inbound makes + `waiting_for_input` / `completed` *exact* (the harness fires the event) + instead of inferred from scrollback. +- **`architecture.md` §16 idle detection / paste races** — a Tier-1 outbound + delivery path removes the "is the pane ready?" guess. + +## 3. Per-agent integration mode + +A per-agent `integration` field, negotiated at register/launch time: + +- `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 the harness/session cannot support + it. +- `omnigent` — route through Omnigent (Tier 2). + +**The safety layer is tier-independent.** AgentTower's permission gate +(FEAT-009) and the global routing kill switch sit in front of *every* outbound +tier. Upgrading the channel never bypasses the gate or the audit trail; every +delivered message is still recorded in `events.jsonl`. + +## 4. Native harness surfaces (Tier 1) + +**Claude Code** + +- `-p` / `--print` headless mode, with `--output-format stream-json` + + `--input-format stream-json` for a bidirectional structured stdio channel + (managed/headless workers). +- **Hooks** (PreToolUse / PostToolUse / Notification / Stop) — the agent + *pushes* structured lifecycle events out. Primary Tier-1 **inbound** channel. +- **MCP** — AgentTower exposes a server the agent reports into. Primary Tier-1 + channel for self-report (see §6). + +**Codex** + +- Headless `exec` mode, a JSON output mode, and an MCP server mode. Same shape + as Claude. Exact flag spellings to be confirmed when this reaches a spec. + +## 5. The realism constraint: where strong tiers are reachable + +Strong integration generally requires control at session **startup**, because +hooks and MCP wiring load when a session starts and a running TUI cannot be +switched into stream-json mid-flight. AgentTower supports both entry points: + +- **Managed launch (FEAT-013).** AgentTower sets the flags / settings / hooks / + MCP config when it spawns the pane, so it can choose Tier 1 or Tier 2 freely. +- **Adopted, pre-running panes — first-class.** We explicitly support upgrading + adopted panes. The enabler is that **AgentTower ships its own MCP server and + hook scripts** as part of the package, so "pre-instrumenting" a session is + just adding AgentTower to the harness's settings. A pane started that way + lights up Tier 1 on adoption; a pane started without it stays at the Tier 0 + floor (still fully useful). Adoption never *downgrades* an agent — at worst it + stays on the floor. + +So: **tmux is the universal floor; managed launch and pre-instrumented adoption +both unlock Tier 1; Omnigent is the uniform Tier 2 for agents that need +driving.** + +## 6. Router-first wiring (the settled division of labor) + +AgentTower stays the router. Concretely: + +- **Inbound** is the priority and the lowest-risk upgrade: agents *push* events + to AgentTower via hooks, and/or *self-report* via an MCP **server** that + AgentTower exposes (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 — that is the puppet-master role, + reserved for Omnigent. +- **Outbound** at Tier 1 is still *message routing* — delivering a prompt or + notification through whatever structured inbox the harness accepts — not + taking over the agent's task loop, decomposing work, or governing its tool + calls. +- **Action-level driving, sandboxing, and policy enforcement over an agent's + own tool calls is Tier 2 (Omnigent).** When that is wanted, AgentTower hands + the agent to Omnigent and continues to observe/route around it. See + `docs/related-work-omnigent.md` §5–6 for why AgentTower deliberately does not + become the runtime itself. + +### No-network-listener invariant is preserved + +Both Tier-1 mechanisms bridge to the daemon through the **existing Unix socket / +thin client**, so no new listener is introduced: + +- 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); it never opens a network listener of its own. This keeps the +MVP constitution (`architecture.md` §23) intact. + +## 7. Recommended phasing + +1. **Tier-0 floor** — shipped today (pipe-pane + paste-buffer). +2. **Tier-1 inbound via hooks (first real upgrade).** Read-only, lowest risk, + and it retires the conservative-classifier problem on its own. Ship the + AgentTower hook scripts and a `report-event` CLI/socket method. +3. **Tier-1 inbound + self-report via the AgentTower MCP server.** Enables + pre-instrumented adoption. +4. **Tier-1 outbound structured delivery** behind the existing permission gate. +5. **Tier-2 Omnigent hand-off** for agents that need a puppet master + (`docs/related-work-omnigent.md` phases). + +## 8. Non-goals + +- AgentTower does not drive an agent's task loop or intercept its tool calls + (that is Omnigent's puppet-master role). +- AgentTower does not embed or re-implement the Omnigent runner. +- No tier requires a network listener. +- Adoption never downgrades an agent below the tmux floor. diff --git a/docs/mvp-feature-sequence.md b/docs/mvp-feature-sequence.md index c368502e..7acc6771 100644 --- a/docs/mvp-feature-sequence.md +++ b/docs/mvp-feature-sequence.md @@ -558,6 +558,28 @@ These are intentionally after FEAT-013: - Optional in-container relay. - Antigravity tmux backend. +### Connection-strength tiers (router-first integration) + +See `docs/integration-tiers.md`. AgentTower connects to agents on a ladder: +Tier 0 tmux floor (today) → Tier 1 native harness (Claude/Codex hooks + MCP + +stream-json) → Tier 2 Omnigent. Inbound (events) and outbound (delivery) +negotiate independently per agent. AgentTower stays the **router** (agents +report in via hooks / an AgentTower-hosted MCP server); Omnigent is the +**puppet master** for action-level driving. Both managed launch and adoption of +pre-instrumented panes unlock Tier 1; the tmux floor is never removed. +Candidate later features, in recommended order: + +- **Tier-1 inbound via hooks** — ship AgentTower hook scripts + a `report-event` + socket method so harness lifecycle events replace scraped classification + (retires the `architecture.md` §13 conservative-classifier problem). Read-only, + lowest risk; do this first. +- **AgentTower MCP server (stdio → daemon socket)** — enables pre-instrumented + adoption and agent self-report (`report_status`, `request_route`). No network + listener. +- **Tier-1 outbound structured delivery** — behind the existing FEAT-009 + permission gate + kill switch. +- **Tier-2 Omnigent hand-off** — see the Omnigent backlog below. + ### Related-work-derived backlog (Omnigent) The Databricks Omnigent meta-harness (see `docs/related-work-omnigent.md` for diff --git a/docs/related-work-omnigent.md b/docs/related-work-omnigent.md index 2cdc5301..a6ee5d95 100644 --- a/docs/related-work-omnigent.md +++ b/docs/related-work-omnigent.md @@ -23,6 +23,11 @@ Primary sources: Same neighbourhood, opposite ends of the coupling spectrum. AgentTower could even observe panes that happen to be Omnigent-launched sessions. +**Where Omnigent fits in AgentTower's architecture:** it is **Tier 2** of the +connection-strength ladder in `docs/integration-tiers.md`. AgentTower stays the +**router**; when an agent needs to be driven, sandboxed, and governed at the +action level, Omnigent is the **puppet master** AgentTower hands off to. + ## 2. What each tool is **AgentTower** is a local control tower that **observes and routes between From f5c5f8cf1130b17ae79c3cb2b51209a95184c2de Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Jun 2026 08:32:13 +0000 Subject: [PATCH 3/9] =?UTF-8?q?Revert=20"docs:=20add=20Omnigent=20comparis?= =?UTF-8?q?on/tiers"=20=E2=80=94=20redo=20via=20OpenSpec?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert the two direct doc commits (Omnigent comparison + derived backlog, and the connection-strength tiers model). The forward-looking scope was written straight into the canonical docs; it should instead go through an OpenSpec change proposal under openspec/changes/. Recreated there. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_015SpYMhCsyyGMEyNrxkWGCc --- docs/architecture.md | 13 --- docs/integration-tiers.md | 158 ---------------------------------- docs/mvp-feature-sequence.md | 48 ----------- docs/related-work-omnigent.md | 138 ----------------------------- 4 files changed, 357 deletions(-) delete mode 100644 docs/integration-tiers.md delete mode 100644 docs/related-work-omnigent.md diff --git a/docs/architecture.md b/docs/architecture.md index 69405c5c..84e09512 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -881,19 +881,6 @@ Likely V2 work: - file event adapters - Python-thread and mailbox bridge support if needed - local LLM-assisted classification as an optional classifier plugin -- layered routing/input policy stack (global → per-agent → per-route), - generalizing the per-pane `send_input_allowed` boolean and the global routing - kill switch — see `docs/related-work-omnigent.md` §6 -- multi-operator session sharing over the FEAT-011 `app.*` backend (read-only - "watch" vs. permitted "co-drive") — see `docs/related-work-omnigent.md` §6 -- pluggable discovery/substrate backends (bench container as one backend among - host-only and future remote/cloud benches) — see - `docs/related-work-omnigent.md` §6 - -Two further Omnigent-derived ideas (authenticated remote/mobile surface; agent -network-egress policy) are recorded in `docs/related-work-omnigent.md` §6 but -conflict with the local-first, no-network-listener stance and require an -explicit decision before they enter scope. ## 25. Open Questions diff --git a/docs/integration-tiers.md b/docs/integration-tiers.md deleted file mode 100644 index d6e6eb98..00000000 --- a/docs/integration-tiers.md +++ /dev/null @@ -1,158 +0,0 @@ -# AgentTower Integration Tiers - -Status: Draft v0.1 -Date: 2026-06-18 - -This note defines AgentTower's **connection-strength model**: how AgentTower -connects to an agent ranges from a universal, fully non-invasive tmux floor up -to a uniform meta-harness channel, negotiated per agent. It generalizes the -Omnigent comparison (`docs/related-work-omnigent.md`) — Omnigent is **Tier 2** -of this broader architecture, not a standalone bolt-on. - -It records two settled decisions: - -1. **AgentTower is the router, not the puppeteer.** Even at strong tiers, - AgentTower observes and routes; agents report *into* it. It does not drive an - agent's task loop or intercept its tool calls. -2. **Omnigent is the puppet master.** When an agent needs to be driven, - sandboxed, and governed at the action level, that role belongs to Omnigent - (Tier 2). AgentTower hands the agent off; it does not try to become a - competing runtime. - -## 1. The connection-strength ladder - -Per agent, AgentTower negotiates the strongest available channel and always -falls back to the tmux floor. - -| 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 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 | - -Tier 0 is the guaranteed floor and never goes away. Tiers 1–2 are upgrades. - -## 2. Two independent axes - -"Strong integration" is **not** a single switch. The inbound (observation) and -outbound (delivery) axes negotiate **independently** per agent. - -A discovered interactive Claude pane can keep its human-in-the-loop TUI, upgrade -its *events* to Tier 1 via hooks, while its *input* stays Tier 0 paste-buffer. -That asymmetric state is the expected common case, not a degenerate one. - -This split directly retires two pain points baked into the current -architecture: - -- **`architecture.md` §13 conservative classification** — Tier-1 inbound makes - `waiting_for_input` / `completed` *exact* (the harness fires the event) - instead of inferred from scrollback. -- **`architecture.md` §16 idle detection / paste races** — a Tier-1 outbound - delivery path removes the "is the pane ready?" guess. - -## 3. Per-agent integration mode - -A per-agent `integration` field, negotiated at register/launch time: - -- `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 the harness/session cannot support - it. -- `omnigent` — route through Omnigent (Tier 2). - -**The safety layer is tier-independent.** AgentTower's permission gate -(FEAT-009) and the global routing kill switch sit in front of *every* outbound -tier. Upgrading the channel never bypasses the gate or the audit trail; every -delivered message is still recorded in `events.jsonl`. - -## 4. Native harness surfaces (Tier 1) - -**Claude Code** - -- `-p` / `--print` headless mode, with `--output-format stream-json` + - `--input-format stream-json` for a bidirectional structured stdio channel - (managed/headless workers). -- **Hooks** (PreToolUse / PostToolUse / Notification / Stop) — the agent - *pushes* structured lifecycle events out. Primary Tier-1 **inbound** channel. -- **MCP** — AgentTower exposes a server the agent reports into. Primary Tier-1 - channel for self-report (see §6). - -**Codex** - -- Headless `exec` mode, a JSON output mode, and an MCP server mode. Same shape - as Claude. Exact flag spellings to be confirmed when this reaches a spec. - -## 5. The realism constraint: where strong tiers are reachable - -Strong integration generally requires control at session **startup**, because -hooks and MCP wiring load when a session starts and a running TUI cannot be -switched into stream-json mid-flight. AgentTower supports both entry points: - -- **Managed launch (FEAT-013).** AgentTower sets the flags / settings / hooks / - MCP config when it spawns the pane, so it can choose Tier 1 or Tier 2 freely. -- **Adopted, pre-running panes — first-class.** We explicitly support upgrading - adopted panes. The enabler is that **AgentTower ships its own MCP server and - hook scripts** as part of the package, so "pre-instrumenting" a session is - just adding AgentTower to the harness's settings. A pane started that way - lights up Tier 1 on adoption; a pane started without it stays at the Tier 0 - floor (still fully useful). Adoption never *downgrades* an agent — at worst it - stays on the floor. - -So: **tmux is the universal floor; managed launch and pre-instrumented adoption -both unlock Tier 1; Omnigent is the uniform Tier 2 for agents that need -driving.** - -## 6. Router-first wiring (the settled division of labor) - -AgentTower stays the router. Concretely: - -- **Inbound** is the priority and the lowest-risk upgrade: agents *push* events - to AgentTower via hooks, and/or *self-report* via an MCP **server** that - AgentTower exposes (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 — that is the puppet-master role, - reserved for Omnigent. -- **Outbound** at Tier 1 is still *message routing* — delivering a prompt or - notification through whatever structured inbox the harness accepts — not - taking over the agent's task loop, decomposing work, or governing its tool - calls. -- **Action-level driving, sandboxing, and policy enforcement over an agent's - own tool calls is Tier 2 (Omnigent).** When that is wanted, AgentTower hands - the agent to Omnigent and continues to observe/route around it. See - `docs/related-work-omnigent.md` §5–6 for why AgentTower deliberately does not - become the runtime itself. - -### No-network-listener invariant is preserved - -Both Tier-1 mechanisms bridge to the daemon through the **existing Unix socket / -thin client**, so no new listener is introduced: - -- 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); it never opens a network listener of its own. This keeps the -MVP constitution (`architecture.md` §23) intact. - -## 7. Recommended phasing - -1. **Tier-0 floor** — shipped today (pipe-pane + paste-buffer). -2. **Tier-1 inbound via hooks (first real upgrade).** Read-only, lowest risk, - and it retires the conservative-classifier problem on its own. Ship the - AgentTower hook scripts and a `report-event` CLI/socket method. -3. **Tier-1 inbound + self-report via the AgentTower MCP server.** Enables - pre-instrumented adoption. -4. **Tier-1 outbound structured delivery** behind the existing permission gate. -5. **Tier-2 Omnigent hand-off** for agents that need a puppet master - (`docs/related-work-omnigent.md` phases). - -## 8. Non-goals - -- AgentTower does not drive an agent's task loop or intercept its tool calls - (that is Omnigent's puppet-master role). -- AgentTower does not embed or re-implement the Omnigent runner. -- No tier requires a network listener. -- Adoption never downgrades an agent below the tmux floor. diff --git a/docs/mvp-feature-sequence.md b/docs/mvp-feature-sequence.md index 7acc6771..19ec27fa 100644 --- a/docs/mvp-feature-sequence.md +++ b/docs/mvp-feature-sequence.md @@ -557,51 +557,3 @@ These are intentionally after FEAT-013: - Host-only tmux discovery. - Optional in-container relay. - Antigravity tmux backend. - -### Connection-strength tiers (router-first integration) - -See `docs/integration-tiers.md`. AgentTower connects to agents on a ladder: -Tier 0 tmux floor (today) → Tier 1 native harness (Claude/Codex hooks + MCP + -stream-json) → Tier 2 Omnigent. Inbound (events) and outbound (delivery) -negotiate independently per agent. AgentTower stays the **router** (agents -report in via hooks / an AgentTower-hosted MCP server); Omnigent is the -**puppet master** for action-level driving. Both managed launch and adoption of -pre-instrumented panes unlock Tier 1; the tmux floor is never removed. -Candidate later features, in recommended order: - -- **Tier-1 inbound via hooks** — ship AgentTower hook scripts + a `report-event` - socket method so harness lifecycle events replace scraped classification - (retires the `architecture.md` §13 conservative-classifier problem). Read-only, - lowest risk; do this first. -- **AgentTower MCP server (stdio → daemon socket)** — enables pre-instrumented - adoption and agent self-report (`report_status`, `request_route`). No network - listener. -- **Tier-1 outbound structured delivery** — behind the existing FEAT-009 - permission gate + kill switch. -- **Tier-2 Omnigent hand-off** — see the Omnigent backlog below. - -### Related-work-derived backlog (Omnigent) - -The Databricks Omnigent meta-harness (see `docs/related-work-omnigent.md` for -the full comparison) surfaced a few ideas that fit AgentTower's non-invasive -model. They are recorded here as candidate later features, not committed scope: - -- **Layered routing/input policies** — generalize the per-pane - `send_input_allowed` boolean + global kill switch into a global → per-agent → - per-route policy stack (spend/rate-style caps on delivery, ask-before-deliver). - This lands the existing "explicit per-agent permission grants" V2 item. -- **Multi-operator session sharing over the FEAT-011 app backend** — multiple - humans observing the same registry/event stream, read-only "watch" vs. - permitted "co-drive". Natural answer to PRD Open Question #2 (TUI vs. web). -- **Pluggable substrate backends** — treat the bench container as one discovery - backend among several (host-only tmux, future remote/cloud bench) behind the - existing discovery interface; additive to host-only tmux discovery. - -The following two are noted for completeness but conflict with the local-first, -no-network-listener constitution and require an explicit decision before pursuit: - -- **Reachable-from-anywhere / mobile surface** — only behind an authenticated - transport reusing the FEAT-011 contract. -- **Network-egress policy at the boundary** — AgentTower cannot intercept an - unmodified agent's syscalls; at most document an opt-in container-network - recommendation, not implement interception. diff --git a/docs/related-work-omnigent.md b/docs/related-work-omnigent.md deleted file mode 100644 index a6ee5d95..00000000 --- a/docs/related-work-omnigent.md +++ /dev/null @@ -1,138 +0,0 @@ -# Related Work: Databricks Omnigent - -Status: Draft v0.1 -Date: 2026-06-18 - -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 -features worth pulling into the AgentTower backlog. - -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. - -**Where Omnigent fits in AgentTower's architecture:** it is **Tier 2** of the -connection-strength ladder in `docs/integration-tiers.md`. AgentTower stays the -**router**; when an agent needs to be driven, sandboxed, and governed at the -action level, Omnigent is the **puppet master** AgentTower hands off to. - -## 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 worth adopting - -The following ideas fit AgentTower's non-invasive thesis and are recorded in the -backlog (`docs/mvp-feature-sequence.md` "Later Features" and `architecture.md` -§24 V2 Scope). 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. From 3e7e1a49160df7e2b19f1993427a8a7c5134dc7e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Jun 2026 08:35:14 +0000 Subject: [PATCH 4/9] openspec: propose agent connection-strength tier model Add OpenSpec change agent-connection-tiers: proposal, design, tasks, and the agent-integration capability spec delta. Captures the per-agent connection ladder (Tier 0 tmux floor / Tier 1 native harness hooks+MCP / Tier 2 Omnigent hand-off), independent inbound/outbound axes, router-first boundary (AT routes; Omnigent puppet-masters), first-class pre-instrumented adoption, the no-network-listener invariant, and tier-independent outbound safety. Spec/design only; no code. Authored to OpenSpec file conventions by hand (no openspec CLI available in this environment). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_015SpYMhCsyyGMEyNrxkWGCc --- .../changes/agent-connection-tiers/design.md | 153 ++++++++++++++++++ .../agent-connection-tiers/proposal.md | 75 +++++++++ .../specs/agent-integration/spec.md | 138 ++++++++++++++++ .../changes/agent-connection-tiers/tasks.md | 67 ++++++++ 4 files changed, 433 insertions(+) create mode 100644 openspec/changes/agent-connection-tiers/design.md create mode 100644 openspec/changes/agent-connection-tiers/proposal.md create mode 100644 openspec/changes/agent-connection-tiers/specs/agent-integration/spec.md create mode 100644 openspec/changes/agent-connection-tiers/tasks.md 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..854886df --- /dev/null +++ b/openspec/changes/agent-connection-tiers/proposal.md @@ -0,0 +1,75 @@ +# 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 + +The `openspec` CLI is not available in this environment (no binary on PATH; +`npx openspec` could not fetch the package under the network policy). This +change was authored to the OpenSpec file conventions by hand. Run +`openspec validate agent-connection-tiers --strict` when the CLI is available. 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:`. From 4cf3bd595a3ed5e85059f7c2830a1c51c8d66693 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Jun 2026 08:47:56 +0000 Subject: [PATCH 5/9] docs: restore Omnigent notes as research-only reference Restore related-work-omnigent.md and integration-tiers.md as reference material with explicit "research only" banners. They record no committed scope and modify no canonical spec; adopted scope lives under openspec/changes/. Removed the stale "recorded in the backlog" claims (those canonical-doc edits were reverted) and reframed the Omnigent-derived features as candidate scope feeding a future OpenSpec proposal. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_015SpYMhCsyyGMEyNrxkWGCc --- docs/integration-tiers.md | 164 ++++++++++++++++++++++++++++++++++ docs/related-work-omnigent.md | 141 +++++++++++++++++++++++++++++ 2 files changed, 305 insertions(+) create mode 100644 docs/integration-tiers.md create mode 100644 docs/related-work-omnigent.md diff --git a/docs/integration-tiers.md b/docs/integration-tiers.md new file mode 100644 index 00000000..3b6ba03e --- /dev/null +++ b/docs/integration-tiers.md @@ -0,0 +1,164 @@ +# AgentTower Integration Tiers + +Status: Research note (reference only) — v0.1 +Date: 2026-06-18 + +> **Research only.** This is design-context/reference material. It records no +> committed scope and modifies no canonical spec. The connection-tier model +> described here is formalized as a proposal under +> `openspec/changes/agent-connection-tiers/` — that OpenSpec change, not this +> note, is the authoritative source for adopted scope. + +This note defines AgentTower's **connection-strength model**: how AgentTower +connects to an agent ranges from a universal, fully non-invasive tmux floor up +to a uniform meta-harness channel, negotiated per agent. It generalizes the +Omnigent comparison (`docs/related-work-omnigent.md`) — Omnigent is **Tier 2** +of this broader architecture, not a standalone bolt-on. + +It records two settled decisions: + +1. **AgentTower is the router, not the puppeteer.** Even at strong tiers, + AgentTower observes and routes; agents report *into* it. It does not drive an + agent's task loop or intercept its tool calls. +2. **Omnigent is the puppet master.** When an agent needs to be driven, + sandboxed, and governed at the action level, that role belongs to Omnigent + (Tier 2). AgentTower hands the agent off; it does not try to become a + competing runtime. + +## 1. The connection-strength ladder + +Per agent, AgentTower negotiates the strongest available channel and always +falls back to the tmux floor. + +| 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 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 | + +Tier 0 is the guaranteed floor and never goes away. Tiers 1–2 are upgrades. + +## 2. Two independent axes + +"Strong integration" is **not** a single switch. The inbound (observation) and +outbound (delivery) axes negotiate **independently** per agent. + +A discovered interactive Claude pane can keep its human-in-the-loop TUI, upgrade +its *events* to Tier 1 via hooks, while its *input* stays Tier 0 paste-buffer. +That asymmetric state is the expected common case, not a degenerate one. + +This split directly retires two pain points baked into the current +architecture: + +- **`architecture.md` §13 conservative classification** — Tier-1 inbound makes + `waiting_for_input` / `completed` *exact* (the harness fires the event) + instead of inferred from scrollback. +- **`architecture.md` §16 idle detection / paste races** — a Tier-1 outbound + delivery path removes the "is the pane ready?" guess. + +## 3. Per-agent integration mode + +A per-agent `integration` field, negotiated at register/launch time: + +- `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 the harness/session cannot support + it. +- `omnigent` — route through Omnigent (Tier 2). + +**The safety layer is tier-independent.** AgentTower's permission gate +(FEAT-009) and the global routing kill switch sit in front of *every* outbound +tier. Upgrading the channel never bypasses the gate or the audit trail; every +delivered message is still recorded in `events.jsonl`. + +## 4. Native harness surfaces (Tier 1) + +**Claude Code** + +- `-p` / `--print` headless mode, with `--output-format stream-json` + + `--input-format stream-json` for a bidirectional structured stdio channel + (managed/headless workers). +- **Hooks** (PreToolUse / PostToolUse / Notification / Stop) — the agent + *pushes* structured lifecycle events out. Primary Tier-1 **inbound** channel. +- **MCP** — AgentTower exposes a server the agent reports into. Primary Tier-1 + channel for self-report (see §6). + +**Codex** + +- Headless `exec` mode, a JSON output mode, and an MCP server mode. Same shape + as Claude. Exact flag spellings to be confirmed when this reaches a spec. + +## 5. The realism constraint: where strong tiers are reachable + +Strong integration generally requires control at session **startup**, because +hooks and MCP wiring load when a session starts and a running TUI cannot be +switched into stream-json mid-flight. AgentTower supports both entry points: + +- **Managed launch (FEAT-013).** AgentTower sets the flags / settings / hooks / + MCP config when it spawns the pane, so it can choose Tier 1 or Tier 2 freely. +- **Adopted, pre-running panes — first-class.** We explicitly support upgrading + adopted panes. The enabler is that **AgentTower ships its own MCP server and + hook scripts** as part of the package, so "pre-instrumenting" a session is + just adding AgentTower to the harness's settings. A pane started that way + lights up Tier 1 on adoption; a pane started without it stays at the Tier 0 + floor (still fully useful). Adoption never *downgrades* an agent — at worst it + stays on the floor. + +So: **tmux is the universal floor; managed launch and pre-instrumented adoption +both unlock Tier 1; Omnigent is the uniform Tier 2 for agents that need +driving.** + +## 6. Router-first wiring (the settled division of labor) + +AgentTower stays the router. Concretely: + +- **Inbound** is the priority and the lowest-risk upgrade: agents *push* events + to AgentTower via hooks, and/or *self-report* via an MCP **server** that + AgentTower exposes (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 — that is the puppet-master role, + reserved for Omnigent. +- **Outbound** at Tier 1 is still *message routing* — delivering a prompt or + notification through whatever structured inbox the harness accepts — not + taking over the agent's task loop, decomposing work, or governing its tool + calls. +- **Action-level driving, sandboxing, and policy enforcement over an agent's + own tool calls is Tier 2 (Omnigent).** When that is wanted, AgentTower hands + the agent to Omnigent and continues to observe/route around it. See + `docs/related-work-omnigent.md` §5–6 for why AgentTower deliberately does not + become the runtime itself. + +### No-network-listener invariant is preserved + +Both Tier-1 mechanisms bridge to the daemon through the **existing Unix socket / +thin client**, so no new listener is introduced: + +- 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); it never opens a network listener of its own. This keeps the +MVP constitution (`architecture.md` §23) intact. + +## 7. Recommended phasing + +1. **Tier-0 floor** — shipped today (pipe-pane + paste-buffer). +2. **Tier-1 inbound via hooks (first real upgrade).** Read-only, lowest risk, + and it retires the conservative-classifier problem on its own. Ship the + AgentTower hook scripts and a `report-event` CLI/socket method. +3. **Tier-1 inbound + self-report via the AgentTower MCP server.** Enables + pre-instrumented adoption. +4. **Tier-1 outbound structured delivery** behind the existing permission gate. +5. **Tier-2 Omnigent hand-off** for agents that need a puppet master + (`docs/related-work-omnigent.md` phases). + +## 8. Non-goals + +- AgentTower does not drive an agent's task loop or intercept its tool calls + (that is Omnigent's puppet-master role). +- AgentTower does not embed or re-implement the Omnigent runner. +- No tier requires a network listener. +- Adoption never downgrades an agent below the tmux floor. 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. From 1b79ffebb30e1441c23aa5b58cbecc80bf9905f3 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Jun 2026 14:22:23 +0000 Subject: [PATCH 6/9] openspec: add Omnigent-derived change family (B/C/D + roadmap) Add three OpenSpec changes derived from the Omnigent analysis, plus a roadmap tying the family to the existing agent-connection-tiers (A): - routing-policy-layers (B): layered global->agent->route delivery policies; deny-wins/min-wins resolution; decision auditing; FEAT-009 backward compatibility. Capability: routing-policy. - discovery-backends (D): pluggable DiscoveryBackend interface; bench discovery refactored as backend #1 (no behavior change); identity namespacing; per-backend degraded isolation; host-only tmux backend #2. - app-collaboration (C, V2): multi-operator app sessions with observer vs operator roles; shared consistent view; server-side co-drive enforcement; cross-session audit; explicit local-only boundary. E (mobile/remote) recorded as a gated non-goal; F (agent network-egress) as advisory-only. All four changes pass `openspec validate --strict`. Spec/design only; no code. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_015SpYMhCsyyGMEyNrxkWGCc --- openspec/changes/README.md | 51 ++++++++++ .../changes/app-collaboration/.openspec.yaml | 2 + openspec/changes/app-collaboration/README.md | 3 + openspec/changes/app-collaboration/design.md | 91 ++++++++++++++++++ .../changes/app-collaboration/proposal.md | 43 +++++++++ .../specs/app-collaboration/spec.md | 72 ++++++++++++++ openspec/changes/app-collaboration/tasks.md | 37 ++++++++ .../changes/discovery-backends/.openspec.yaml | 2 + openspec/changes/discovery-backends/README.md | 3 + openspec/changes/discovery-backends/design.md | 83 +++++++++++++++++ .../changes/discovery-backends/proposal.md | 44 +++++++++ .../specs/discovery-backends/spec.md | 68 ++++++++++++++ openspec/changes/discovery-backends/tasks.md | 37 ++++++++ .../routing-policy-layers/.openspec.yaml | 2 + .../changes/routing-policy-layers/README.md | 3 + .../changes/routing-policy-layers/design.md | 93 +++++++++++++++++++ .../changes/routing-policy-layers/proposal.md | 47 ++++++++++ .../specs/routing-policy/spec.md | 79 ++++++++++++++++ .../changes/routing-policy-layers/tasks.md | 38 ++++++++ 19 files changed, 798 insertions(+) create mode 100644 openspec/changes/README.md create mode 100644 openspec/changes/app-collaboration/.openspec.yaml create mode 100644 openspec/changes/app-collaboration/README.md create mode 100644 openspec/changes/app-collaboration/design.md create mode 100644 openspec/changes/app-collaboration/proposal.md create mode 100644 openspec/changes/app-collaboration/specs/app-collaboration/spec.md create mode 100644 openspec/changes/app-collaboration/tasks.md create mode 100644 openspec/changes/discovery-backends/.openspec.yaml create mode 100644 openspec/changes/discovery-backends/README.md create mode 100644 openspec/changes/discovery-backends/design.md create mode 100644 openspec/changes/discovery-backends/proposal.md create mode 100644 openspec/changes/discovery-backends/specs/discovery-backends/spec.md create mode 100644 openspec/changes/discovery-backends/tasks.md create mode 100644 openspec/changes/routing-policy-layers/.openspec.yaml create mode 100644 openspec/changes/routing-policy-layers/README.md create mode 100644 openspec/changes/routing-policy-layers/design.md create mode 100644 openspec/changes/routing-policy-layers/proposal.md create mode 100644 openspec/changes/routing-policy-layers/specs/routing-policy/spec.md create mode 100644 openspec/changes/routing-policy-layers/tasks.md diff --git a/openspec/changes/README.md b/openspec/changes/README.md new file mode 100644 index 00000000..27f39598 --- /dev/null +++ b/openspec/changes/README.md @@ -0,0 +1,51 @@ +# 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. + +## Changes + +| ID | Change | Capability | Status | +| --- | --- | --- | --- | +| A | `agent-connection-tiers` | `agent-integration` | proposed (validated) | +| B | `routing-policy-layers` | `routing-policy` | proposed | +| D | `discovery-backends` | `discovery-backends` | proposed | +| C | `app-collaboration` | `app-collaboration` | proposed (V2) | 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..6d414735 --- /dev/null +++ b/openspec/changes/app-collaboration/specs/app-collaboration/spec.md @@ -0,0 +1,72 @@ +# 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: 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 with a closed-set error. +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 a closed-set error 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..93b55f2b --- /dev/null +++ b/openspec/changes/discovery-backends/design.md @@ -0,0 +1,83 @@ +# 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. + +## 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** — existing rows are implicitly `bench:`; the namespacing + must be backward-compatible for stored agents. +- **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 + +- Exact identity-key format and migration for already-registered agents. +- Should backend selection be global config or per-bench/per-host overrides? diff --git a/openspec/changes/discovery-backends/proposal.md b/openspec/changes/discovery-backends/proposal.md new file mode 100644 index 00000000..4c4d04b7 --- /dev/null +++ b/openspec/changes/discovery-backends/proposal.md @@ -0,0 +1,44 @@ +# 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, future remote/cloud benches, and (later) Omnigent +sessions can be peer backends behind one interface — without rewriting the +registry/event/routing stack each time. + +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..b4a11404 --- /dev/null +++ b/openspec/changes/discovery-backends/specs/discovery-backends/spec.md @@ -0,0 +1,68 @@ +# 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 + +### 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..6a8da8db --- /dev/null +++ b/openspec/changes/discovery-backends/tasks.md @@ -0,0 +1,37 @@ +# 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) with + back-compat for stored `bench:` rows. +- [ ] 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..9ee5d0a1 --- /dev/null +++ b/openspec/changes/routing-policy-layers/design.md @@ -0,0 +1,93 @@ +# 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). + +Resolution is **most-specific-wins for allow/deny, most-restrictive-wins for +limits**: a deny at any layer blocks; a numeric cap takes the minimum across +layers. This avoids "which rule won" ambiguity and keeps the global kill switch +absolute (a global deny can never be overridden by a lower layer). + +Rationale: deterministic, explainable, and safe-by-default — broadening requires +an explicit higher-specificity allow, never an implicit one. + +## Decision 2 — Router-appropriate policy types (MVP set) + +- **rate cap** — max N deliveries per target per time window. +- **require-approval** — hold delivery in a pending state until an operator + approves (reuses the FEAT-009 queue states: queued/blocked/approved). +- **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 — Every decision is audited + +Each evaluation 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`. + +## 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..05ca9912 --- /dev/null +++ b/openspec/changes/routing-policy-layers/specs/routing-policy/spec.md @@ -0,0 +1,79 @@ +# 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 resolve delivery policies across three scopes in fixed +precedence — global, then per-agent, then per-route — using deny-wins for +allow/deny rules and most-restrictive-wins (minimum) for numeric limits. A deny +at any layer SHALL block delivery, and a global deny SHALL NOT be overridable by +a lower layer. + +#### 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 policy +decision, naming the outcome (allowed / blocked / held), the deciding layer, and +the matched rule. + +#### 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 + +### 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:`. From 1be04cb65ac4c9e9319606b43014d46ea51f16f7 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Jun 2026 14:27:01 +0000 Subject: [PATCH 7/9] docs/openspec: address Sourcery review on PR #46 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - integration-tiers.md: trim duplicated tier/role narrative now owned by the agent-connection-tiers OpenSpec change; keep only orientation + pointers + the extra comparative context, with the design/spec marked authoritative (avoids future drift). - discovery-backends: make the identity-key migration concrete — a one-time, idempotent, history-preserving stamp of existing rows to backend_id='bench' tied to the schema bump, plus a transition read-time shim for legacy keys. Add a backward-compat scenario and a migration task. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_015SpYMhCsyyGMEyNrxkWGCc --- docs/integration-tiers.md | 206 +++++------------- openspec/changes/discovery-backends/design.md | 32 ++- .../specs/discovery-backends/spec.md | 7 + openspec/changes/discovery-backends/tasks.md | 6 +- 4 files changed, 94 insertions(+), 157 deletions(-) diff --git a/docs/integration-tiers.md b/docs/integration-tiers.md index 3b6ba03e..a597f164 100644 --- a/docs/integration-tiers.md +++ b/docs/integration-tiers.md @@ -1,164 +1,66 @@ -# AgentTower Integration Tiers +# AgentTower Integration Tiers (research note) -Status: Research note (reference only) — v0.1 +Status: Research note (reference only) — v0.2 Date: 2026-06-18 -> **Research only.** This is design-context/reference material. It records no -> committed scope and modifies no canonical spec. The connection-tier model -> described here is formalized as a proposal under -> `openspec/changes/agent-connection-tiers/` — that OpenSpec change, not this -> note, is the authoritative source for adopted scope. +> **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. -This note defines AgentTower's **connection-strength model**: how AgentTower -connects to an agent ranges from a universal, fully non-invasive tmux floor up -to a uniform meta-harness channel, negotiated per agent. It generalizes the -Omnigent comparison (`docs/related-work-omnigent.md`) — Omnigent is **Tier 2** -of this broader architecture, not a standalone bolt-on. - -It records two settled decisions: - -1. **AgentTower is the router, not the puppeteer.** Even at strong tiers, - AgentTower observes and routes; agents report *into* it. It does not drive an - agent's task loop or intercept its tool calls. -2. **Omnigent is the puppet master.** When an agent needs to be driven, - sandboxed, and governed at the action level, that role belongs to Omnigent - (Tier 2). AgentTower hands the agent off; it does not try to become a - competing runtime. - -## 1. The connection-strength ladder +## Orientation: the connection-strength ladder Per agent, AgentTower negotiates the strongest available channel and always -falls back to the tmux floor. +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 (Notification ⇒ `waiting_for_input`, Stop ⇒ `completed`) | structured message delivery the harness accepts | Claude / Codex, per capability | +| **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 | -Tier 0 is the guaranteed floor and never goes away. Tiers 1–2 are upgrades. - -## 2. Two independent axes - -"Strong integration" is **not** a single switch. The inbound (observation) and -outbound (delivery) axes negotiate **independently** per agent. - -A discovered interactive Claude pane can keep its human-in-the-loop TUI, upgrade -its *events* to Tier 1 via hooks, while its *input* stays Tier 0 paste-buffer. -That asymmetric state is the expected common case, not a degenerate one. - -This split directly retires two pain points baked into the current -architecture: - -- **`architecture.md` §13 conservative classification** — Tier-1 inbound makes - `waiting_for_input` / `completed` *exact* (the harness fires the event) - instead of inferred from scrollback. -- **`architecture.md` §16 idle detection / paste races** — a Tier-1 outbound - delivery path removes the "is the pane ready?" guess. - -## 3. Per-agent integration mode - -A per-agent `integration` field, negotiated at register/launch time: - -- `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 the harness/session cannot support - it. -- `omnigent` — route through Omnigent (Tier 2). - -**The safety layer is tier-independent.** AgentTower's permission gate -(FEAT-009) and the global routing kill switch sit in front of *every* outbound -tier. Upgrading the channel never bypasses the gate or the audit trail; every -delivered message is still recorded in `events.jsonl`. - -## 4. Native harness surfaces (Tier 1) - -**Claude Code** - -- `-p` / `--print` headless mode, with `--output-format stream-json` + - `--input-format stream-json` for a bidirectional structured stdio channel - (managed/headless workers). -- **Hooks** (PreToolUse / PostToolUse / Notification / Stop) — the agent - *pushes* structured lifecycle events out. Primary Tier-1 **inbound** channel. -- **MCP** — AgentTower exposes a server the agent reports into. Primary Tier-1 - channel for self-report (see §6). - -**Codex** - -- Headless `exec` mode, a JSON output mode, and an MCP server mode. Same shape - as Claude. Exact flag spellings to be confirmed when this reaches a spec. - -## 5. The realism constraint: where strong tiers are reachable - -Strong integration generally requires control at session **startup**, because -hooks and MCP wiring load when a session starts and a running TUI cannot be -switched into stream-json mid-flight. AgentTower supports both entry points: - -- **Managed launch (FEAT-013).** AgentTower sets the flags / settings / hooks / - MCP config when it spawns the pane, so it can choose Tier 1 or Tier 2 freely. -- **Adopted, pre-running panes — first-class.** We explicitly support upgrading - adopted panes. The enabler is that **AgentTower ships its own MCP server and - hook scripts** as part of the package, so "pre-instrumenting" a session is - just adding AgentTower to the harness's settings. A pane started that way - lights up Tier 1 on adoption; a pane started without it stays at the Tier 0 - floor (still fully useful). Adoption never *downgrades* an agent — at worst it - stays on the floor. - -So: **tmux is the universal floor; managed launch and pre-instrumented adoption -both unlock Tier 1; Omnigent is the uniform Tier 2 for agents that need -driving.** - -## 6. Router-first wiring (the settled division of labor) - -AgentTower stays the router. Concretely: - -- **Inbound** is the priority and the lowest-risk upgrade: agents *push* events - to AgentTower via hooks, and/or *self-report* via an MCP **server** that - AgentTower exposes (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 — that is the puppet-master role, - reserved for Omnigent. -- **Outbound** at Tier 1 is still *message routing* — delivering a prompt or - notification through whatever structured inbox the harness accepts — not - taking over the agent's task loop, decomposing work, or governing its tool - calls. -- **Action-level driving, sandboxing, and policy enforcement over an agent's - own tool calls is Tier 2 (Omnigent).** When that is wanted, AgentTower hands - the agent to Omnigent and continues to observe/route around it. See - `docs/related-work-omnigent.md` §5–6 for why AgentTower deliberately does not - become the runtime itself. - -### No-network-listener invariant is preserved - -Both Tier-1 mechanisms bridge to the daemon through the **existing Unix socket / -thin client**, so no new listener is introduced: - -- 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); it never opens a network listener of its own. This keeps the -MVP constitution (`architecture.md` §23) intact. - -## 7. Recommended phasing - -1. **Tier-0 floor** — shipped today (pipe-pane + paste-buffer). -2. **Tier-1 inbound via hooks (first real upgrade).** Read-only, lowest risk, - and it retires the conservative-classifier problem on its own. Ship the - AgentTower hook scripts and a `report-event` CLI/socket method. -3. **Tier-1 inbound + self-report via the AgentTower MCP server.** Enables - pre-instrumented adoption. -4. **Tier-1 outbound structured delivery** behind the existing permission gate. -5. **Tier-2 Omnigent hand-off** for agents that need a puppet master - (`docs/related-work-omnigent.md` phases). - -## 8. Non-goals - -- AgentTower does not drive an agent's task loop or intercept its tool calls - (that is Omnigent's puppet-master role). -- AgentTower does not embed or re-implement the Omnigent runner. -- No tier requires a network listener. -- Adoption never downgrades an agent below the tmux floor. +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/openspec/changes/discovery-backends/design.md b/openspec/changes/discovery-backends/design.md index 93b55f2b..b37492ef 100644 --- a/openspec/changes/discovery-backends/design.md +++ b/openspec/changes/discovery-backends/design.md @@ -47,6 +47,30 @@ 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 @@ -66,8 +90,9 @@ graceful-degradation requirement from "Docker" to "any backend". - **Refactor risk** — bench backend must be behavior-preserving; cover with the existing FEAT-003/004 integration tests before extraction. -- **Identity migration** — existing rows are implicitly `bench:`; the namespacing - must be backward-compatible for stored agents. +- **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 @@ -79,5 +104,6 @@ graceful-degradation requirement from "Docker" to "any backend". ## Open Questions -- Exact identity-key format and migration for already-registered agents. - 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/specs/discovery-backends/spec.md b/openspec/changes/discovery-backends/specs/discovery-backends/spec.md index b4a11404..bb5263cc 100644 --- a/openspec/changes/discovery-backends/specs/discovery-backends/spec.md +++ b/openspec/changes/discovery-backends/specs/discovery-backends/spec.md @@ -44,6 +44,13 @@ in addition to the existing tmux coordinates. 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 diff --git a/openspec/changes/discovery-backends/tasks.md b/openspec/changes/discovery-backends/tasks.md index 6a8da8db..f3fddee6 100644 --- a/openspec/changes/discovery-backends/tasks.md +++ b/openspec/changes/discovery-backends/tasks.md @@ -25,8 +25,10 @@ - [ ] 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) with - back-compat for stored `bench:` rows. +- [ ] 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. From f66d681b3f73cddce49ca5ee547cae81576d8401 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Jun 2026 14:29:34 +0000 Subject: [PATCH 8/9] openspec: address Copilot review on PR #46 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - routing-policy-layers/design.md: resolve the contradictory resolution semantics — state the model is restrictive-only (deny-wins for allow/deny, min-wins for caps; global deny absolute), dropping the "most-specific-wins" wording that implied a specific allow could broaden. - routing-policy-layers/design.md: correct FEAT-009 queue terminology — require-approval maps onto the `blocked` state released via `queue approve`; there is no `approved` state. - routing-policy-layers/design.md: fix the `architecture.md` inline reference that had wrapped to `architecture. md`. - agent-connection-tiers: add the missing .openspec.yaml + README.md for consistency with the other three changes in the family. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_015SpYMhCsyyGMEyNrxkWGCc --- .../agent-connection-tiers/.openspec.yaml | 2 ++ .../changes/agent-connection-tiers/README.md | 3 ++ .../changes/routing-policy-layers/design.md | 31 ++++++++++++------- 3 files changed, 25 insertions(+), 11 deletions(-) create mode 100644 openspec/changes/agent-connection-tiers/.openspec.yaml create mode 100644 openspec/changes/agent-connection-tiers/README.md 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/routing-policy-layers/design.md b/openspec/changes/routing-policy-layers/design.md index 9ee5d0a1..8de25e13 100644 --- a/openspec/changes/routing-policy-layers/design.md +++ b/openspec/changes/routing-policy-layers/design.md @@ -32,23 +32,32 @@ Policies attach at three scopes and resolve in a fixed order: 2. **per-agent** — keyed by target `agent_id` (and optionally source). 3. **per-route** — keyed by `route_id` (FEAT-010). -Resolution is **most-specific-wins for allow/deny, most-restrictive-wins for -limits**: a deny at any layer blocks; a numeric cap takes the minimum across -layers. This avoids "which rule won" ambiguity and keeps the global kill switch -absolute (a global deny can never be overridden by a lower layer). - -Rationale: deterministic, explainable, and safe-by-default — broadening requires -an explicit higher-specificity allow, never an implicit one. +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 in a pending state until an operator - approves (reuses the FEAT-009 queue states: queued/blocked/approved). +- **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). + 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 From 55da8252402f538f5ad4f355b54376eda2da92ec Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Jun 2026 14:35:28 +0000 Subject: [PATCH 9/9] openspec: address Codex + 2nd Copilot review on PR #46 routing-policy-layers: - reword Layered policy resolution to most-restrictive-wins across all layers (deny-wins/min-wins); scope order is attribution-only, not outcome - name audit event tokens policy_allowed/policy_blocked/policy_held and scope policy audit to decisions where a user-defined policy participates, so the no-policy default path stays byte-identical (no extra events.jsonl) app-collaboration: - add default-observer + explicit server-side operator promotion (no client self-select; pre-role clients treated as observer); clarify it is a coordination control under same-host-UID, not a privilege boundary - name the rejection error: FEAT-011 permission_denied with details.reason="observer_read_only" (no new code) discovery-backends: - drop Omnigent from the opening scope statement (Omnigent is the Tier-2 hand-off in agent-connection-tiers, not a discovery backend) agent-connection-tiers: - rephrase the tooling note generically (no environment-specific detail) openspec/changes/README.md: - add owning bench per change (py-bench) per AGENTS.md Single-Bench Ownership; note app-collaboration's Flutter UI is a separate flutter-bench follow-up All four changes pass `openspec validate --strict`. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_015SpYMhCsyyGMEyNrxkWGCc --- openspec/changes/README.md | 21 ++++++++---- .../agent-connection-tiers/proposal.md | 7 ++-- .../specs/app-collaboration/spec.md | 34 ++++++++++++++++--- .../changes/discovery-backends/proposal.md | 7 ++-- .../changes/routing-policy-layers/design.md | 19 +++++++---- .../specs/routing-policy/spec.md | 33 +++++++++++++----- 6 files changed, 89 insertions(+), 32 deletions(-) diff --git a/openspec/changes/README.md b/openspec/changes/README.md index 27f39598..661ddcf7 100644 --- a/openspec/changes/README.md +++ b/openspec/changes/README.md @@ -41,11 +41,20 @@ proceed in parallel; C builds on the app backend and ideally on B. 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 | Status | -| --- | --- | --- | --- | -| A | `agent-connection-tiers` | `agent-integration` | proposed (validated) | -| B | `routing-policy-layers` | `routing-policy` | proposed | -| D | `discovery-backends` | `discovery-backends` | proposed | -| C | `app-collaboration` | `app-collaboration` | proposed (V2) | +| 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/proposal.md b/openspec/changes/agent-connection-tiers/proposal.md index 854886df..515cd4b6 100644 --- a/openspec/changes/agent-connection-tiers/proposal.md +++ b/openspec/changes/agent-connection-tiers/proposal.md @@ -69,7 +69,6 @@ implement against a settled model instead of ad-hoc per-harness hacks. ## Tooling note -The `openspec` CLI is not available in this environment (no binary on PATH; -`npx openspec` could not fetch the package under the network policy). This -change was authored to the OpenSpec file conventions by hand. Run -`openspec validate agent-connection-tiers --strict` when the CLI is available. +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/app-collaboration/specs/app-collaboration/spec.md b/openspec/changes/app-collaboration/specs/app-collaboration/spec.md index 6d414735..4abcbe2d 100644 --- a/openspec/changes/app-collaboration/specs/app-collaboration/spec.md +++ b/openspec/changes/app-collaboration/specs/app-collaboration/spec.md @@ -18,6 +18,29 @@ credential. - **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 @@ -33,14 +56,17 @@ converge on identical state. ### Requirement: Server-side co-drive enforcement AgentTower SHALL permit mutating `app.*` methods only for `operator` sessions -and SHALL reject mutating calls from `observer` sessions with a closed-set error. -Enforcement SHALL be server-side and SHALL NOT depend on client cooperation. +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 a closed-set error and performs no - mutation +- **THEN** AgentTower rejects the call with `permission_denied` / + `details.reason = "observer_read_only"` and performs no mutation #### Scenario: Operator mutation is allowed diff --git a/openspec/changes/discovery-backends/proposal.md b/openspec/changes/discovery-backends/proposal.md index 4c4d04b7..ec2bcba9 100644 --- a/openspec/changes/discovery-backends/proposal.md +++ b/openspec/changes/discovery-backends/proposal.md @@ -7,9 +7,10 @@ AgentTower's discovery is hardwired to one substrate: bench containers found via 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, future remote/cloud benches, and (later) Omnigent -sessions can be peer backends behind one interface — without rewriting the -registry/event/routing stack each time. +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 diff --git a/openspec/changes/routing-policy-layers/design.md b/openspec/changes/routing-policy-layers/design.md index 8de25e13..f9a634f5 100644 --- a/openspec/changes/routing-policy-layers/design.md +++ b/openspec/changes/routing-policy-layers/design.md @@ -63,13 +63,18 @@ layer can only narrow delivery, never silently re-open it. All default to the *least-surprising* behavior: absent any policy, deliveries behave exactly as FEAT-009 today. -## Decision 3 — Every decision is audited - -Each evaluation 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`. +## 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 diff --git a/openspec/changes/routing-policy-layers/specs/routing-policy/spec.md b/openspec/changes/routing-policy-layers/specs/routing-policy/spec.md index 05ca9912..825bf009 100644 --- a/openspec/changes/routing-policy-layers/specs/routing-policy/spec.md +++ b/openspec/changes/routing-policy-layers/specs/routing-policy/spec.md @@ -8,11 +8,17 @@ agent's own tool calls. ### Requirement: Layered policy resolution -AgentTower SHALL resolve delivery policies across three scopes in fixed -precedence — global, then per-agent, then per-route — using deny-wins for -allow/deny rules and most-restrictive-wins (minimum) for numeric limits. A deny -at any layer SHALL block delivery, and a global deny SHALL NOT be overridable by -a lower layer. +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 @@ -51,9 +57,13 @@ an agent's own shell, file, or network actions. ### Requirement: Policy decision auditing -AgentTower SHALL write an audit record to `events.jsonl` for every policy -decision, naming the outcome (allowed / blocked / held), the deciding layer, and -the matched rule. +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 @@ -61,6 +71,13 @@ the matched rule. - **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