diff --git a/README.md b/README.md index 3948cac..a31f0fb 100644 --- a/README.md +++ b/README.md @@ -150,7 +150,8 @@ sandboxed agent never gets `/mcp`; it gets a **reverse attach** with the `deskto ## Lending this Mac to a sandboxed agent (reverse attach) -An agent in an `openab-pty` session cannot reach this Mac — the pod has no egress by design. So +An agent in an `openab-pty` session cannot reach this Mac — the pod has no tailnet egress by design +(it does reach the internet, for its model API, git and package registries). So **this Mac dials the pod** and serves MCP over that socket with a narrowed tool list. Design: [`docs/adr/reverse-attach.md`](docs/adr/reverse-attach.md); wire contract: openab-pty `CLIENT-CONTRACT.md` §9. diff --git a/docs/adr/reverse-attach.md b/docs/adr/reverse-attach.md index ef80ed6..66710ba 100644 --- a/docs/adr/reverse-attach.md +++ b/docs/adr/reverse-attach.md @@ -15,7 +15,9 @@ Connect. Three facts about the sandbox constrain the answer: 1. The shell runs as `uid 1000` with no host credentials and a read-only rootfs. openab-pty's - standing invariant is that **the shell never holds a credential**. + standing invariant is that **the shell holds no host or cloud credential**. (It is not + credential-free: the agent CLI's own model login lives in the container, and a session may + carry others, such as git.) 2. The tailscale sidecar is the pod's only tailnet identity and it is **inbound-only**: tailnet → sidecar → loopback runtime, via `tailscale serve`. 3. The sidecar runs `--tun=userspace-networking` on both k8s and ECS. There is no tun device, @@ -38,7 +40,12 @@ why we start from the opposite premise. ## Decision -**The sandbox gets no egress at all. The Mac dials the pod.** +**The sandbox gets no tailnet egress. The Mac dials the pod.** + +"Egress" in this ADR means **tailnet** egress. The pod's ordinary internet egress is open by +default — the agent CLI needs it for its model API, git remotes and package registries, and the +ECS tasks run with `assignPublicIp: true`. Restricting it is a deployment choice (an egress +allowlist), not something this design provides. 1. **openab-pty runtime** gains an inbound endpoint `WS /tools/attach` on its existing loopback listener, reached through the sidecar's existing `tailscale serve` exactly like @@ -58,7 +65,8 @@ why we start from the opposite premise. `key` and `mouse` each reach the desktop user's shell, so `desktop` is shell-equivalent and the earlier rationale — "no `exec` because the agent already has a shell" — described a convenience, not a restriction. The profile was renamed to say so; the pod-side isolation (no - egress, credential-less shell) is unaffected and remains the boundary this ADR claims. A real + tailnet egress — internet egress is open by default — and a shell without host credentials) + is unaffected and remains the boundary this ADR claims. A real narrower tier must be built from tools that cannot reach a shell (`observe`: `sys_info` + `screenshot`; `browser`: Playwright with a per-grant throwaway profile), and the hands themselves are isolated only by lending a dedicated machine or VM. diff --git a/docs/requirements/reverse-attach.md b/docs/requirements/reverse-attach.md index 74c576f..7bf5e4a 100644 --- a/docs/requirements/reverse-attach.md +++ b/docs/requirements/reverse-attach.md @@ -1,4 +1,4 @@ -# Requirement — reverse attach: macmini dials the sandbox, the sandbox has no egress +# Requirement — reverse attach: macmini dials the sandbox, the sandbox has no tailnet egress > Recorded 2026-09-24. Implements **Phase 4** of > [`connect-closed-loop.md`](connect-closed-loop.md): "Connect's agent (in the `openab-pty` @@ -14,8 +14,8 @@ key / osascript / exec) so the agent can drive the Mac while the human watches i The sandbox shell has **no network identity of its own** (`uid 1000`, no host creds, read-only rootfs); the tailscale sidecar is the pod's only tailnet identity and it is **inbound-only** (tailnet → sidecar → loopback runtime). Every earlier idea started from "give the sandbox an -egress path". This document starts from the opposite premise: **the sandbox gets no egress at -all — macmini connects *in*.** +egress path". This document starts from the opposite premise: **the sandbox gets no tailnet +egress — macmini connects *in*.** (Internet egress is a separate matter and is open by default.) ## Rejected — every egress-based variant shares one root flaw @@ -66,7 +66,7 @@ on the instance-mcp side. │ │ (sha256 verifier)│ │ inbound │ └────────────────────────┘ │ │ └──────────────────┘ │ only └──────────────────────────────┘ │ sidecar: inbound only — UNCHANGED │ ▲ - │ no egress of any kind │ │ "lend my Mac to this + │ no tailnet egress (internet: default) │ │ "lend my Mac to this └──────────────────────────────────────────┘ │ session, 1h" — human, │ in OpenAB Connect ``` @@ -196,8 +196,8 @@ remainder of the grant TTL. |---|---| | sandbox needs a tailnet egress path | **none needed**; sidecar unchanged | | credential in the pod that a compromised shell could steal | **none** — pod holds a sha256 verifier only; a verifier cannot be used to connect anywhere | -| compromised agent reaches other tailnet nodes | **impossible** — no egress to bypass through | -| compromised agent uses macmini's full shell via `exec` | **blocked by profile** — the `sandbox` profile omits `exec`, or requires a human tap in Connect (the human is already watching the screen) | +| compromised agent reaches other tailnet nodes | **no path**, provided the k8s node itself does not run `tailscaled` (otherwise apply openab-pty `networkpolicy-no-tailnet-egress.yaml`). The pod still reaches the internet by default | +| compromised agent uses macmini's full shell via `exec` | **not blocked by removing `exec`.** Superseded (#45): GUI control is a shell, so `desktop` (formerly `sandbox`) is shell-equivalent; only `observe` is a boundary. See `docs/tool-profiles.md` | | compromised agent calls the tools it *was* granted | **residual, by design** — giving an agent hands carries this in every design; the difference is the hands are exactly as large as the human chose, for as long as they chose | | macmini's attach credential leaks | attacker could serve a fake `/tools/attach`? No — the credential authenticates macmini *to the pod*; a leaked one lets an attacker impersonate macmini to that pod, not reach macmini. Mint per-attach, TTL'd, revoke by deleting the verifier | | **the k8s node itself is a tailnet member** (homelab k3s boxes often are) | **out of scope, must be documented as an assumption** — pods ride the node's tailscale routing (`ip rule … lookup 52` → `tailscale0`, masqueraded as the node) and reach every tailnet peer regardless of the sidecar. Measured on p1 in 4a: with the host's tailscaled running the shell reached macmini/black over their tailnet IPs; with it stopped, no path at all. Deployment rule: **no tailscaled on the node**; if unavoidable, an egress `NetworkPolicy` denying `100.64.0.0/10` + `fd7a:115c:a1e0::/48`. Fargate has no such path | diff --git a/docs/tool-profiles.md b/docs/tool-profiles.md index 5d0344e..ececf4b 100644 --- a/docs/tool-profiles.md +++ b/docs/tool-profiles.md @@ -17,10 +17,12 @@ a profile is a managed policy, a grant attaches it to a principal (the session), > `exec*`, but that takes away a convenient entry point, not a privilege > ([#45](https://github.com/openabdev/instance-mcp/issues/45)). When you grant full control, lend > a **dedicated computer** (a Linux hands node, a throwaway machine or VM), not the one you work -> on. Every served tool is classified `observe` (reads only), `act` (changes state) or `shell` -> (reaches the desktop user's shell); the boundary tests fail on any **unclassified** tool, on any +> on. Every **local** tool is classified `observe` (reads only), `act` (changes state) or `shell` +> (reaches the desktop user's shell); the boundary tests fail on any **unclassified** local tool, on any > profile that claims to be narrower than a shell while allowing a `shell` tool, and on `observe` > holding anything but `observe` tools (macOS `ProfileBoundaryTests`, Linux `profile_tests`). +> Upstream `browser_*` tools are governed by their own allowlist (`desktop` gets 15, `observe` +> none) and are not in that classification yet; they must be before a `browser` tier ships. ## 1. Available profiles @@ -106,8 +108,10 @@ change anything. **`observe` protects the computer, not the agent.** Every screenshot goes into the agent's context. If the screen shows a hostile page or message, its text can act as a prompt injection against the agent — which cannot act on this computer under `observe`, but still has its own shell -in the pod, its model API, and whatever credentials the session holds (for example git). Treat what -is on screen as untrusted input to the agent. +in the pod, whatever credentials the session holds (for example git), and **outbound internet +access by default**: it can send whatever it reads to any host. Treat what is on screen as +untrusted input to the agent; deployments that need to contain this should give the pod an egress +allowlist (model API, git remotes, package registries). ## 3. What each profile loses against the previous one