Skip to content
Open
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
2 changes: 1 addition & 1 deletion DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ where agents run.
| P1 | **A space per agent.** Each agent's lanes (inbox, peer, outbox) are its own | So no agent can read another's mail or write into another's outbox | One sandbox per workspace; `instances/<slug>/` lanes created at launch | One sandbox per agent **[docs]**. Our tooling creates each agent's lane directories on the host and passes them to that sandbox alone as `bind` mounts in `--driver-config-json` **[docs: `docs/how-it-works/sandboxes/runtimes.mdx:117-131`]**. This works on the Docker and Podman drivers only, and only after the operator enables it on the gateway (§6). OpenShell does not stop two sandboxes from being given the same host path, so exclusivity is our tooling's job, and `verify` checks it (§5) **[code: no such check in `crates/openshell-driver-docker/src/lib.rs:3844-3910`]** |
| P2 | **An identity the agent cannot forge** | The runtime binds a sender to its outbox. The address must come from the host, never from the agent | The slug, reported by sandy | The sandbox **name**, read by our tooling on the host and mapped to `<name>@<fleet_domain>`. The name is a DNS-1123 label of at most 19 characters, unique per workspace, and it survives stop, start and restart **[docs: `workspaces.mdx:178-190`, `sandboxes/overview.mdx:36-48`] [code: `crates/openshell-server/src/grpc/validation.rs:241`, `grpc/mod.rs:140`, `persistence/tests.rs:1027`]**. The agent cannot rename it: the API has no rename RPC, and a sandbox's credential reaches only supervisor RPCs **[code: `proto/openshell.proto`; `architecture/gateway.md:379-385`]**. **Changed in phase 0:** a deleted name can be created again with a new ID, and names are unique only within one workspace. So `membership.json` pins `(workspace, name, id)`, `verify` fails when the ID changes, and one fleet uses one OpenShell workspace. Name reuse is intended: "Names can be reused, so use them only for display and logging" **[docs: `docs/extensibility/supervisor-middleware/operations.mdx:27`]** |
| P3 | **Read-only mounts.** Inbound lanes, the connector and the agent's instructions are out of the agent's reach | So a compromised agent can't forge an inbound message or rewrite its own policy | Feature mounts; a write fails with `EROFS` | Two independent layers. (1) **The mount flag.** A `bind` mount with `read_only: true` (the default) becomes a Docker `:ro` bind on the agent's container **[code: `lib.rs:3745-3781`, `lib.rs:5770`]**. The agent can't remount it: the container runs with every capability dropped and `no-new-privileges` **[code: `lib.rs:5784-5797`]**. The host, and the router's own container, keep writing to the source directory, as with sandy. (2) **Landlock.** The filesystem policy lists `inbound/`, `peer/`, the roster and the payload as `read_only` and the outbox as `read_write`. Every other path is inaccessible, and the policy is locked at creation **[docs: `docs/security/best-practices.mdx:32,156-161`]**. The policy applies to mount targets like any other path **[code: `crates/openshell-sandbox/src/sandbox/linux/landlock.rs:224` ff.]**. The host's writes appear inside a running sandbox with no delay: the notice the router wrote is listed inside the running receiver **[verified: L1, 2026-09-30, `docs/POC-REPORT.md`, Unknown 1]**. A write gets `EROFS`, not `EACCES`: the kernel checks the read-only mount before Landlock's hooks run, on every write path (open, truncate, create, unlink, rename) **[verified: L1, 2026-09-30, `docs/POC-REPORT.md`, Unknown 2 and Pass criterion 3]**. Two pitfalls. If the policy's `read_only` and `read_write` lists are both empty, there is no Landlock policy layer at all **[code: `landlock.rs:241-242`]**. And a mount under a `read_write` parent, such as the workdir, gets write access from Landlock, so only the `:ro` flag protects it. Mount targets therefore stay outside every `read_write` path |
| P4 | **A supervised process** that injects deliveries into the session and is restarted if it dies | Otherwise delegations only surface when the agent happens to check its inbox | The manifest's `entry`, run by sandy's relay supervisor | **Changed in phase 0.** OpenShell supervises only one process, the main process: the command after `--` **[docs: `sandboxes/overview.mdx:23`]**. `sandbox exec` is not a supervisor **[docs: `overview.mdx:331`]**. So the main process is **our wrapper** on the read-only payload. It starts `inbox-delivery` in its own restart loop, then runs `claude`. On `main`, `--restart-policy on-failure` replaces the whole runtime if the wrapper exits. Release `v0.1.2` predates that flag, and there the exit is terminal **[docs: `overview.mdx:36-48`; `v0.1.2:crates/openshell-driver-docker/README.md:83`]**. The daemon injects over Claude Code's own Unix socket. OpenShell's seccomp filter leaves `AF_UNIX` alone, and its Landlock code scopes no sockets **[code: `crates/openshell-sandbox/src/sandbox/linux/seccomp.rs:187-215,855-890`]**. **New piece:** the daemon finds its session through `AMAP_DELIVERY_SESSION_SOURCE`, and sandy's lister reads tmux panes, which don't exist here. This deployment ships its own lister, which emits the single `claude` process's socket, read from a record that a SessionStart hook on the payload writes from `CLAUDE_CODE_MESSAGING_SOCKET`, and its key file, which is the session's own peer key `$HOME/.claude/sessions/<pid>.<id>.key` that Claude Code writes (the path amap-deploy-sandy's `payload/handoff-sessions` reads at `eeecad0`). The hook never writes `CLAUDE_CODE_MESSAGING_TOKEN`: that token proves a connection comes from the session's own child, which the daemon is not (amap-connector-claude `37875a5`). End-to-end injection is **[verified: L1, 2026-09-30, `docs/POC-REPORT.md`, Pass criterion 2 and Unknown 4]**: one agent tasked the other, the daemon reported `delivered`, and the reply reached the sender. L1 ran with the hook's record as the key file; the peer key replaces it and is checked in L2. A receiver set to refuse yields `held`, never a false `delivered` |
| P4 | **A supervised process** that injects deliveries into the session and is restarted if it dies | Otherwise delegations only surface when the agent happens to check its inbox | The manifest's `entry`, run by sandy's relay supervisor | **Changed in phase 0.** OpenShell supervises only one process, the main process: the command after `--` **[docs: `sandboxes/overview.mdx:23`]**. `sandbox exec` is not a supervisor **[docs: `overview.mdx:331`]**. So the main process is **our wrapper** on the read-only payload. It starts `inbox-delivery` in its own restart loop, then runs `claude`. On `main`, `--restart-policy on-failure` replaces the whole runtime if the wrapper exits. Release `v0.1.2` predates that flag, and there the exit is terminal **[docs: `overview.mdx:36-48`; `v0.1.2:crates/openshell-driver-docker/README.md:83`]**. The daemon injects over Claude Code's own Unix socket. OpenShell's seccomp filter leaves `AF_UNIX` alone, and its Landlock code scopes no sockets **[code: `crates/openshell-sandbox/src/sandbox/linux/seccomp.rs:187-215,855-890`]**. **New piece:** the daemon finds its session through `AMAP_DELIVERY_SESSION_SOURCE`, and sandy's lister reads tmux panes, which don't exist here. This deployment ships its own lister, which emits the single `claude` process's socket, read from a record that a SessionStart hook on the payload writes from `CLAUDE_CODE_MESSAGING_SOCKET`, and its key file, which is the session's own peer key `$HOME/.claude/sessions/<pid>.<id>.key` that Claude Code writes (the path amap-deploy-sandy's `payload/handoff-sessions` reads at `ea931af`). The hook never writes `CLAUDE_CODE_MESSAGING_TOKEN`: that token proves a connection comes from the session's own child, which the daemon is not (amap-connector-claude `37875a5`). End-to-end injection is **[verified: L1, 2026-09-30, `docs/POC-REPORT.md`, Pass criterion 2 and Unknown 4]**: one agent tasked the other, the daemon reported `delivered`, and the reply reached the sender. L1 ran with the hook's record as the key file; the peer key replaces it and is checked in L2. A receiver set to refuse yields `held`, never a false `delivered` |
| P5 | **Operator-owned membership.** Joining the fleet is a security decision | An agent must never add itself | Include/exclude rules, evaluated by sandy at each launch; `selected.json` | Our tooling decides, from the fleet policy, which OpenShell sandboxes get lanes. A sandbox without lanes is simply not in the fleet. Optionally, a gateway interceptor bound to `CreateSandbox` and `DeleteSandbox` does it **[docs: `docs/extensibility/gateway-interceptors.mdx:28-42`] [code: `crates/openshell-gateway-interceptors/src/routes.rs:18,21`]**. Its `post_commit` phase is fail-open, so it can miss events **[docs: `gateway-interceptors.mdx:135`]**. Polling `openshell sandbox list` or `WatchSandbox` stays as the backstop **[docs: `proto/openshell.proto:83,631`]** |

AMAP's own rule that **a sender is bound to its outbox** does most of the
Expand Down
2 changes: 1 addition & 1 deletion docs/TUTORIAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ printed. It refuses a checkout with local changes, naming it. Without
|---|---|
| amap-router-local | `e43dbba` |
| amap-connector-claude | `37875a5` |
| amap-deploy-sandy | `eeecad0` |
| amap-deploy-sandy | `ea931af` |
| amap-spec | not pinned |

This repository finds the siblings by looking beside itself; the variables
Expand Down
2 changes: 1 addition & 1 deletion siblings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"siblings": [
{"name": "amap-router-local", "url": "https://github.com/proofpoint/amap-router-local", "commit": "e43dbba7ac1a44f06b4fbc5dc8c2ffb2777363c0"},
{"name": "amap-connector-claude", "url": "https://github.com/proofpoint/amap-connector-claude", "commit": "37875a5ddd55a266a1545c5a03c8ce3bf3018b9e"},
{"name": "amap-deploy-sandy", "url": "https://github.com/proofpoint/amap-deploy-sandy", "commit": "eeecad0b5674c5771d0ed366f01b5e31144d2e89"},
{"name": "amap-deploy-sandy", "url": "https://github.com/proofpoint/amap-deploy-sandy", "commit": "ea931af5b683004c5dec6531358f7c651de91e53"},
{"name": "amap-spec", "url": "https://github.com/proofpoint/amap-spec", "commit": null}
]
}