Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
14 changes: 11 additions & 3 deletions docs/adr/reverse-attach.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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.
Expand Down
12 changes: 6 additions & 6 deletions docs/requirements/reverse-attach.md
Original file line number Diff line number Diff line change
@@ -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`
Expand All @@ -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

Expand Down Expand Up @@ -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
```
Expand Down Expand Up @@ -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 |
Expand Down
12 changes: 8 additions & 4 deletions docs/tool-profiles.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
Loading