diff --git a/docs/adr/fleet-grouping-and-connection-model.md b/docs/adr/fleet-grouping-and-connection-model.md new file mode 100644 index 0000000..3cb80b0 --- /dev/null +++ b/docs/adr/fleet-grouping-and-connection-model.md @@ -0,0 +1,172 @@ +# ADR: Fleet as usage-based logical grouping + the Studio connection model + +- **Status:** Proposed +- **Date:** 2026-08-14 +- **Author:** Orca (`ecs-claude`) +- **Related:** [Deployment control plane (ADR-2)](./deployment-control-plane.md), + [Runtime identity & context (per-Fleet managing identity)](./runtime-identity-context.md), + and — in `openabdev/openab` — [Reverse MCP-over-ACP over WebSocket](https://github.com/openabdev/openab/blob/main/docs/adr/acp-server-websocket-reverse-mcp.md). + +--- + +## 1. Context + +Two gaps surfaced while wiring a real operator (Brett's laptop) to Studio: + +1. **A "fleet" is currently a physical cluster.** The fleet-binding config + (`fleets.toml`) is an array of `[[fleet]]` tables keyed by `cluster` + (`FleetBindings::for_cluster`, first-match-wins). A fleet therefore *is* a + `(cluster → managing credential)` mapping. Two agents in the same cluster — + e.g. `oab-prod-orca` and `oab-prod-mira`, both services in cluster `oab` — + cannot be presented or managed as separate fleets, because the only grouping + axis is the cluster and they share it (and share one credential/account). + +2. **There is no defined way for an *agent* to connect to Studio's `oab-mcp`.** + Studio spawns `oab-mcp` as a **stdio subprocess** (a private pipe between the + desktop shell and the sidecar); nothing else can attach to it. Operators asked + how a local or remote agent could drive the fleet through Studio. + +This ADR decides both: what a **fleet** is, and how an **agent connects**. + +## 2. Decision — Part A: Fleet is a usage-based logical group + +**A fleet is a named logical grouping of agents, defined by its members, not by +its cluster.** Grouping is decoupled from credential. + +- **Grouping** is by *usage* (what the agents are for), chosen by the operator. +- **Credential** is a *consequence* of where the members physically live + (cluster / account), not the grouping key. Multiple fleets may share a cluster + and credential while remaining distinct fleets. + +### Schema — `[fleet.]`, explicit members + +`fleets.toml` moves from `[[fleet]]` (array, `name` as a field) to +`[fleet.]` (a map keyed by name). The name becomes the primary key — +unique, self-documenting, and the fleet's identity in the UI. + +```toml +# ~/.config/oab-studio/fleets.toml + +[fleet.orca] +members = ["oab-prod-orca"] # explicit member list (Decision: option (a)) +region = "ap-east-2" +profile = "oab-fleet" +expected_principal = "arn:aws:iam::504190915686:user/oab-fleet-laptop" + +[fleet.mira] +members = ["oab-prod-mira"] +region = "ap-east-2" +profile = "oab-fleet" +expected_principal = "arn:aws:iam::504190915686:user/oab-fleet-laptop" +``` + +This lets `orca` and `mira` be **two distinct fleets that share the `oab` +cluster and one credential** — exactly the "group by usage, not by cluster" the +operator asked for. + +- **Membership (a): explicit member list** — ship first. Full operator control, + no external dependency. +- **Membership (b): tag selector** (e.g. `select = { usage = "prod" }` over ECS + tags) — deferred follow-up; needs a tagging convention on agents so new agents + auto-join a fleet. + +### Credential resolution + +A fleet's managing credential comes from its `profile`/`region` (as today), now +scoped to the fleet rather than a cluster. A member's cluster/account is derived +from its service (all members of a fleet are expected to be co-located in one +account for v1; a fleet spanning accounts is a follow-up). `runtime_context` +(ADR: per-Fleet managing identity) reports the effective principal **per fleet**. + +## 3. Decision — Part B: two connection models, both first-class + +An agent connects to the fleet in one of **two distinct ways** — not a primary + +fallback, but two capabilities for different scenarios: + +### (i) Reverse MCP — attach to a *running* Studio + +The single way for an agent (local **or** remote) to operate **through a running +Studio instance**. Studio becomes an ACP WebSocket **client** that serves its +`oab-mcp` tool surface over the **outbound `/acp` WS it already holds**; OpenAB +core proxies those tools to the agent (the mechanism is Accepted + as-built in +openab #1447, first used for browser control). + +- Solves NAT / can't-listen (Studio dials out; no inbound port). +- The agent uses **Studio's** running `oab-mcp` instance and **Studio's + identity/session** — no separate AWS credentials to provision on the agent + (avoids re-introducing the silent-credential-fallback class of bug). +- **Human-in-the-loop:** the agent's fleet operations are visible in Studio's UI. +- Cost: Studio must implement the ACP-WS-client "serve" mode, and the fleet is + only reachable while Studio is running. + +### (ii) Headless standalone `oab-mcp` — no Studio + +For **CI, scripts, and headless agents** that manage the fleet **without Studio +running**. The agent spawns its **own** `oab-mcp` (stdio) using its own +credentials. Studio provides a **"Copy MCP config"** action that emits a +ready-to-paste stdio MCP server spec: + +```json +{ + "command": "", + "args": [], + "env": { + "OAB_CLUSTER": "oab", + "OAB_FLEETS_CONFIG": "~/.config/oab-studio/fleets.toml", + "AWS_PROFILE": "oab-fleet", + "AWS_REGION": "ap-east-2" + } +} +``` + +This is an **independent** instance (own creds, own process), deliberately not a +fallback for (i) — it serves the no-Studio case. + +### Current usage — how Orca connects *today* + +The always-on ECS agent (Orca) is the concrete first consumer, and the two paths +land at different times, so this is explicit: + +- **Now (raw AWS):** Orca already drives cluster `oab` under its task role + (`openab-orca-task-role` @ `504190915686` / `ap-east-2`) — no Studio, no + `oab-mcp`. This is how the fleet was operated before Studio. +- **Near-term (headless `oab-mcp`, path (ii)):** provision the `oab-mcp` binary + into Orca's runtime and register it as an MCP server with `OAB_CLUSTER=oab` + (task role is the ambient credential — no profile needed). Orca then has the + full fleet tool surface. **This is available with only that setup — no new + Studio code.** +- **Target (reverse MCP, path (i)):** Orca attaches to a *running* Studio through + OpenAB core, using Studio's identity, with ops visible in Studio's UI — once + the serve mode is built. + +Step-by-step for each is in the **[agent connection guide](../connecting-an-agent.md)**. + +## 4. Consequences + +- `fleets.toml` schema change (`[[fleet]]` → `[fleet.]` + `members`); a + parse/compat path or a one-time migration is needed. +- `studio-cp` `FleetBinding`/`FleetBindings` and the resolution seam move from + `for_cluster` to fleet-by-name / member→fleet lookup; `oab-mcp` keys credential + selection on the governing fleet, not the cluster. +- The **config panel** (ADR: per-Fleet managing identity, slices A+B+C) evolves: + list fleets by name, roster filtered to a fleet's members, switch by fleet + identity (not cluster). The `fleets.toml` editor already added in that work + carries over. +- Studio gains an **ACP-WS-client serve mode** for (i), and a **Copy MCP config** + action for (ii). + +## 5. Open questions + +1. **Reverse-MCP auth/scoping** — exposing fleet-control (incl. writes: + `deploy_apply`/`scale`/`delete`) over a relay needs an auth token scoping + *which* agent may attach and *what* it may do. Threat model TBD. +2. **Fleet ↔ reverse-MCP session mapping** — does an attached agent see all + fleets, or is a session bound to one fleet? +3. **Membership (b)** — tag-selector grouping + the `usage` tagging convention. +4. **Cross-account fleets** — v1 assumes a fleet's members share one account; + spanning accounts (multiple credentials in one fleet) is later work. +5. **Migration** — auto-convert existing `[[fleet]]` (cluster-keyed) files, or + require a manual rewrite with a clear error. + +This is a stub to align direction (both decisions are made); implementation lands +in slices, and the open questions are resolved as those slices are designed. diff --git a/docs/connecting-an-agent.md b/docs/connecting-an-agent.md new file mode 100644 index 0000000..20256ae --- /dev/null +++ b/docs/connecting-an-agent.md @@ -0,0 +1,82 @@ +# Connecting an agent to an OAB fleet + +How an AI agent gets the OAB fleet tool surface (`deploy_list`, `deploy_get`, +`get_agent_states`, `deploy_events`, `deploy_apply`, `deploy_scale`, +`deploy_delete`, `runtime_context`, `fleet_config`, `fleet_config_write`). + +There are two ways, for two different scenarios (see the +[Fleet grouping & connection model ADR](./adr/fleet-grouping-and-connection-model.md)): + +- **(A) Headless `oab-mcp`** — the agent runs its **own** `oab-mcp`, no Studio. + For CI, scripts, and always-on agents. **Available today.** +- **(B) Reverse MCP** — the agent attaches to a **running Studio** through OpenAB + core, using Studio's identity. **Target; the serve mode is not built yet.** + +--- + +## Worked example: connecting **Orca** (the always-on ECS agent) + +Orca runs 24/7 in AWS ECS with an IAM **task role** (`openab-orca-task-role` @ +`504190915686` / `ap-east-2`) that already reaches cluster `oab`. Three levels, +increasing capability: + +### 0. Raw AWS — already working + +Orca can observe and drive the cluster with the `aws` CLI/SDK under its task role +today (this is how the fleet was diagnosed and managed before Studio existed). No +setup. It just lacks the ergonomic `oab-mcp` tool surface. + +### A. Headless `oab-mcp` — near-term, small setup + +Give Orca the `oab-mcp` tools by running `oab-mcp` as an MCP server in its runtime: + +1. **Provision the binary.** Put `oab-mcp` on Orca's image/host — bundled from + Studio's release, or built from `crates/oab-mcp` (note: the workspace + statically links the full aws-sdk; build with `-j1`, it OOMs otherwise). +2. **Credentials.** Orca needs no profile — its **task role** is the ambient + credential chain, and `oab-mcp` uses `aws_config::load_defaults`. (A laptop + would instead set `AWS_PROFILE=oab-fleet`.) +3. **Register the MCP server** in the agent's MCP config: + + ```json + { + "mcpServers": { + "oab": { + "command": "/path/to/oab-mcp", + "env": { "OAB_CLUSTER": "oab", "AWS_REGION": "ap-east-2" } + } + } + } + ``` + - Fleet bindings are optional here: with the task role, calls resolve directly + to `oab` @ `504190915686`. To pin a binding, add + `"OAB_FLEETS_CONFIG": "/path/to/fleets.toml"`. +4. **Verify.** Have the agent call `runtime_context` — it should report + `principal = …assumed-role/openab-orca-task-role`, `account = 504190915686`, + `region = ap-east-2`. Then `deploy_list` should list `oab-prod-orca` / + `oab-prod-mira`. + +This is an **independent** `oab-mcp` instance (Orca's own creds, own process) — +it does not touch, and does not need, a running Studio. + +### B. Reverse MCP through Studio — target (not yet built) + +Once Studio implements the ACP-WS-client **serve** mode, Orca attaches to a +**running** Studio through OpenAB core and drives **Studio's** `oab-mcp` instance +using **Studio's** identity, with its operations visible in Studio's UI. No AWS +credentials provisioned on Orca; no inbound port (Studio dials out). This is the +model for "the operator sits at Studio, a remote agent assists through it." It is +gated on the reverse-MCP auth/scoping design (ADR §5). + +Until then, use **(A)** for programmatic/headless operation, or **(0)** raw AWS. + +--- + +## Laptop / local agent (summary) + +- **Headless:** same as (A), but set `AWS_PROFILE=oab-fleet` (the laptop profile + wired in the fleet-credential setup) instead of relying on a task role. Studio's + **Copy MCP config** action emits this JSON ready to paste. +- **Reverse MCP:** a local agent attaches to the local running Studio the same way + a remote one does (once the serve mode ships) — co-located, but still through + the same reverse-MCP path for uniformity.