Add owner-mediated bwrap execution boundary
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a06def-6490-7033-8448-2eab2d12ed44
This commit is contained in:
parent
877676d1f1
commit
d79e3fe358
23 changed files with 1321 additions and 86 deletions
|
|
@ -17,6 +17,14 @@ Optional (SaaS): `estimate_cost(profile, duration) → MeterQuote`
|
|||
Optional (checkpoints): `supports_snapshots()`, `snapshot(handle)`,
|
||||
`restore_from_snapshot(profile, snapshot_meta, inputs, host)`
|
||||
|
||||
Optional (owner-mediated execution): `supports_execution()`, then
|
||||
`execute(handle, command, credential_route_refs, execution_context,
|
||||
timeout_seconds, max_output_bytes)`. The default implementation fails closed.
|
||||
`ext.bwrap` is the reference implementation. An executing extension must
|
||||
validate that its workspace belongs to the exact sandbox handle, sanitize the
|
||||
child environment, use an argument vector, enforce the requested bounds, and
|
||||
must not fall back to a host workspace.
|
||||
|
||||
### Base class
|
||||
|
||||
```python
|
||||
|
|
@ -37,6 +45,7 @@ Reference implementations:
|
|||
| `ext.saas-stub` | `saas_stub.py` | Metered stub + metadata snapshots |
|
||||
| `ext.e2b` | `e2b.py` | E2B cloud adapter |
|
||||
| `ext.modal` | `modal.py` | Modal cloud adapter |
|
||||
| `ext.bwrap` | `bwrap.py` | Local namespaces + owner-mediated execution |
|
||||
|
||||
## Registration
|
||||
|
||||
|
|
@ -111,4 +120,4 @@ Implement `estimate_cost` and `meter_actual` on `SandboxExtension`. Register wit
|
|||
| Packer build orchestration from `create` | Future WP |
|
||||
| Daytona OSS cloud adapter | Future WP |
|
||||
| fin-hub billing export | Future |
|
||||
| Cross-host snapshot transfer | Future |
|
||||
| Cross-host snapshot transfer | Future |
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ sandboxer create \
|
|||
--profile profile.agent-dev \
|
||||
--input repo=/path/to/workspace \
|
||||
--actor agt \
|
||||
--project glas-harness
|
||||
--project glas-harness \
|
||||
--session-id session-123 \
|
||||
--run-id run-456
|
||||
```
|
||||
|
||||
## Response fields (ready state)
|
||||
|
|
@ -22,28 +24,31 @@ sandboxer create \
|
|||
| `reachability.remote_dir` | sand-boxer | Workspace root on remote host |
|
||||
| `state` | sand-boxer | Lifecycle state (`ready`, etc.) |
|
||||
|
||||
## Two reachability modes
|
||||
## Two execution modes
|
||||
|
||||
Not every sandbox has an SSH hop. `ext.compose-ssh` / `ext.vm-packer`
|
||||
always run remote and populate `reachability.ssh` + `reachability.
|
||||
remote_dir`; glas-harness execs tools over that SSH channel. `ext.bwrap`
|
||||
(SAND-WP-0013) runs same-host and never populates `reachability.ssh` —
|
||||
instead it populates `reachability.pid` (the placeholder process holding
|
||||
the bwrap namespaces) and `reachability.workspace_dir`. glas-harness
|
||||
execs tools by entering that pid's namespaces directly
|
||||
(`nsenter --target <pid> --mount --pid --net --uts --ipc -- <cmd>`, no
|
||||
tunnel needed) rather than opening an SSH channel. `reachability/enrich.
|
||||
build_reachability_report()` returns both an `ssh_one_liner` and a
|
||||
`local_exec_hint`; exactly one is non-null depending on which mode the
|
||||
resolved extension uses. Consumers should branch on which field is
|
||||
populated, not on profile id, since routing can fall back between
|
||||
extensions.
|
||||
the bwrap namespaces) and `reachability.workspace_dir` as evidence. These
|
||||
fields are not a consumer attach contract. `build_reachability_report()`
|
||||
returns `execution.mode: owner-mediated`; glas-harness sends an exec request
|
||||
to the resident sand-boxer owner service. Direct `nsenter` is unsupported.
|
||||
|
||||
The request repeats the exact actor/project/session/run identity used at
|
||||
create, carries an argument vector (never a shell command string), and may
|
||||
carry only value-free credential catalog route references. sand-boxer refuses
|
||||
identity mismatch, non-ready or expired state, concurrent execution, and every
|
||||
extension without an owner execution implementation. It never retries against
|
||||
the host source checkout.
|
||||
|
||||
## Ownership
|
||||
|
||||
| Concern | Owner |
|
||||
|---------|-------|
|
||||
| Sandbox provision / teardown | sand-boxer |
|
||||
| In-namespace command broker, workspace cwd, timeout/output bounds | sand-boxer |
|
||||
| Tool call parsing and policies | glas-harness |
|
||||
| SSH / tunnel reachability setup | glas-harness + ops-bridge |
|
||||
| Agent memory and session state | glas-harness |
|
||||
|
|
@ -62,4 +67,5 @@ one-liner), then destroys.
|
|||
|
||||
- Tool schemas and approval flows
|
||||
- Channel bridges (Slack, email, etc.)
|
||||
- Subagent orchestration
|
||||
- Subagent orchestration
|
||||
- Provider credential acquisition or injection (the selected rein owns it)
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ Extends the `build-agent` self-register pattern: generic sandbox identities carr
|
|||
| `recreate` | Destroy and reprovision from stored seed | **Yes** |
|
||||
| `destroy` | Idempotent teardown | **Yes** |
|
||||
| `snapshot` / `restore` | Checkpoint workspace | **Yes** (compose-ssh, saas-stub) |
|
||||
| `exec` | Run command in sandbox | Harness-owned via SSH (glas-harness) |
|
||||
| `exec` | Owner-mediated command in a local bwrap sandbox | **Yes** (`ext.bwrap` only) |
|
||||
|
||||
HTTP surface (optional v0; CLI calls core library directly):
|
||||
|
||||
|
|
@ -107,6 +107,7 @@ HTTP surface (optional v0; CLI calls core library directly):
|
|||
- `PATCH /v1/sandboxes/{id}/ttl` — extend TTL
|
||||
- `POST /v1/sandboxes/expire` — TTL reap (query `apply=true`)
|
||||
- `GET /v1/sandboxes/{id}/reachability` — enriched descriptor + SSH one-liner
|
||||
- `POST /v1/sandboxes/{id}/exec` — authenticated owner-mediated bwrap command
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -133,6 +134,12 @@ Tunnel metadata is enriched from profile `reachability` and environment:
|
|||
sand-boxer **does not** bring tunnels up. Consumers use ops-bridge (MCP or
|
||||
`bridge` CLI) to attach SSH routes; the descriptor is a pointer only.
|
||||
|
||||
For `ext.bwrap`, reachability is not an invitation to enter the recorded PID.
|
||||
The report contains `execution.mode: owner-mediated`; callers use `sandboxer
|
||||
exec` when running as the sandbox owner or the authenticated HTTP exec endpoint
|
||||
when calling the resident owner service. Direct consumer-side `nsenter` is not
|
||||
supported.
|
||||
|
||||
`secret_refs` from `profile.setup` are resolved at the provision boundary and
|
||||
passed to the extension handle — they never appear on `SandboxStatus` or State
|
||||
Hub events.
|
||||
|
|
@ -159,6 +166,8 @@ consumer:
|
|||
|
||||
sand-boxer records attribution on every lifecycle event. It does not interpret
|
||||
agent intent or authorize the caller — flex-auth owns authorization when enforced.
|
||||
An exec request must nevertheless repeat this block exactly. Any actor, project,
|
||||
session, or run mismatch is refused before extension dispatch.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -170,6 +179,8 @@ Each extension implements:
|
|||
provision(profile, inputs, placement) → SandboxHandle
|
||||
wait_ready(handle) → Reachability
|
||||
teardown(handle) → CleanupReport
|
||||
supports_execution?() → bool
|
||||
execute?(handle, argv, credential_route_refs, execution_context, bounds) → CommandResult
|
||||
estimate_cost?(profile, duration) → MeterQuote # optional; SaaS only
|
||||
```
|
||||
|
||||
|
|
@ -210,6 +221,9 @@ sand-boxer commits to:
|
|||
from Railiance01 production
|
||||
4. **Observable lifecycle** — every transition attributed to `adm` / `agt` / `atm`
|
||||
5. **Honest limits** — allowed tool paths can be abused by compromised agents
|
||||
6. **Fail-closed local execution** — exact identity/state/TTL binding, owner-managed
|
||||
workspace validation, sanitized environment, bounded duration/output, and no
|
||||
host-checkout or alternate-extension fallback
|
||||
|
||||
sand-boxer does **not** provide intent-aware egress filtering in v1.
|
||||
|
||||
|
|
@ -226,4 +240,4 @@ sand-boxer does **not** provide intent-aware egress filtering in v1.
|
|||
| SSH certificates | ops-warden |
|
||||
| Workstream / task state | state-hub |
|
||||
|
||||
See `docs/integrations/` for per-sibling contracts.
|
||||
See `docs/integrations/` for per-sibling contracts.
|
||||
|
|
|
|||
84
docs/runbooks/profile-bwrap-local.md
Normal file
84
docs/runbooks/profile-bwrap-local.md
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
# Runbook: profile.bwrap-local owner execution
|
||||
|
||||
`profile.bwrap-local` copies the requested repository into an owner-managed
|
||||
workspace and starts a bubblewrap namespace with network default-deny. Commands
|
||||
must cross the sand-boxer owner boundary; direct namespace entry is unsupported.
|
||||
|
||||
## Create with governed identity
|
||||
|
||||
```bash
|
||||
sandboxer create \
|
||||
--profile profile.bwrap-local \
|
||||
--input repo=/path/to/repository \
|
||||
--actor agt \
|
||||
--project glas-harness \
|
||||
--session-id session-123 \
|
||||
--run-id run-456
|
||||
```
|
||||
|
||||
Keep the returned sandbox id. The source path is copied; it is not mounted into
|
||||
the namespace.
|
||||
|
||||
## Execute as the local owner
|
||||
|
||||
```bash
|
||||
sandboxer exec SANDBOX_ID \
|
||||
--actor agt \
|
||||
--project glas-harness \
|
||||
--session-id session-123 \
|
||||
--run-id run-456 \
|
||||
-- python3 -c 'from pathlib import Path; print(Path.cwd())'
|
||||
```
|
||||
|
||||
The result records the bound identity, profile and extension, command name,
|
||||
exit/timeout outcome, workspace, and declared network posture. Captured stdout
|
||||
and stderr default to 256 KiB per stream and the timeout defaults to 15 minutes.
|
||||
|
||||
Credential values are never command options or environment assignments. A rein
|
||||
that owns credential acquisition may receive only a catalog identifier, for
|
||||
example `--credential-route-ref rein-openweights-openrouter-approle`; inside the
|
||||
sandbox that appears as the value-free JSON list
|
||||
`SANDBOXER_CREDENTIAL_ROUTE_REFS`. The selected rein resolves and cleans up the
|
||||
credential through its approved owner-fronted route.
|
||||
|
||||
## Resident owner API
|
||||
|
||||
The high-risk HTTP route is disabled unless the service process receives
|
||||
`SANDBOXER_EXEC_TOKEN` through its approved service credential delivery path.
|
||||
Before provisioning that credential, locate its custody route with `warden route
|
||||
find "sand-boxer owner execution service credential" --json`; do not paste it
|
||||
into a shell history, workplan, State Hub, or sandbox mount.
|
||||
|
||||
Call `POST /v1/sandboxes/SANDBOX_ID/exec` with a bearer token and this body:
|
||||
|
||||
```json
|
||||
{
|
||||
"command": ["python3", "-V"],
|
||||
"consumer": {
|
||||
"actor": "agt",
|
||||
"project": "glas-harness",
|
||||
"session_id": "session-123",
|
||||
"run_id": "run-456"
|
||||
},
|
||||
"credential_route_refs": [],
|
||||
"timeout_seconds": 30,
|
||||
"max_output_bytes": 65536
|
||||
}
|
||||
```
|
||||
|
||||
The bearer authenticates access to the owner service; exact consumer identity
|
||||
matching additionally binds the command to the existing sandbox grant.
|
||||
|
||||
## Destroy
|
||||
|
||||
```bash
|
||||
sandboxer destroy SANDBOX_ID
|
||||
```
|
||||
|
||||
Destroy kills the namespace process group and removes the copied workspace.
|
||||
|
||||
For a non-secret create/execute/source-absence/destroy proof, run:
|
||||
|
||||
```bash
|
||||
make smoke-bwrap-exec
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue