diff --git a/docs/docs/designs/drafts/agentcompute.md b/docs/docs/designs/drafts/agentcompute.md index e31c1db..fc33f69 100644 --- a/docs/docs/designs/drafts/agentcompute.md +++ b/docs/docs/designs/drafts/agentcompute.md @@ -94,14 +94,15 @@ policy, and the deployment form of the server. Those follow the prototype. impaired links) between instances in a sandbox. - An agent can take screenshots of and send keyboard and pointer input to a guest desktop. -- Sandboxes expire. Nothing an agent forgets outlives its TTL. +- Sandboxes expire unless an explicitly allowlisted operator pins them. - The vocabulary is small enough to be discoverable through `search_api` and stable enough that agents' saved programs keep working. ## Non-goals -- Durable workloads. Anything meant to survive the day belongs in OpenTofu - or CAPI, not here. +- Durable workloads. Operator pins can retain an interactive machine while + iterating, but are not backups or reusable images. Durable workloads belong + in OpenTofu or CAPI; reusable images require recipes under `images/`. - Generality. No second-user abstraction over hypervisors; the two backends (Incus, Lume) are named and their differences are exposed, not hidden. - Cluster configuration. Storage pools, cluster-wide networks, and profiles @@ -203,13 +204,21 @@ shape is whatever `describe_api` reports from the Go types. | Capability | Arguments | Returns | Notes | | --- | --- | --- | --- | -| `sandbox.create` | `name?`, `platform?` (`incus` default, `mac`), `ttl_minutes?` | `{name, platform, expires_at, network}` | Creates an Incus project (or Lume name prefix) named after the sandbox, plus its `default` NAT'd network. Default TTL 240 minutes. | -| `sandbox.list` | — | `list[{name, platform, created_at, expires_at, instances: int}]` | | -| `sandbox.get` | `name` | `{name, platform, created_at, expires_at, instances: list[...], networks: list[...]}` | One call for an agent to re-orient. | -| `sandbox.extend` | `name`, `ttl_minutes` | `{expires_at}` | Extends from now. | -| `sandbox.delete` | `name` | `{}` | Records expiry, then destroys owned resources in dependency order. A partial failure remains discoverable for reaper retry; an unknown sandbox returns `AgentError`. | - -Expired sandboxes are deleted by a reaper inside `agentcompute`. +| `sandbox.create` | `name?`, `platform?` (`incus` default, `mac`), `ttl_minutes?`, `pinned?` (false) | `{name, platform, expires_at, pinned, pinned_by, network}` | Creates an Incus project or Lume sidecar. Incus also creates its `default` NAT network. Default TTL 240 minutes; pinning requires an operator identity. | +| `sandbox.list` | — | `{items: list[{name, platform, created_at, expires_at, pinned, pinned_by, instances: int}]}` | | +| `sandbox.get` | `name` | `{name, platform, created_at, expires_at, pinned, pinned_by, instances: list[...], networks: list[...]}` | One call for an agent to re-orient. | +| `sandbox.extend` | `name`, `ttl_minutes` | `{expires_at}` | Extends from now within the existing TTL limit; pins ignore the deadline. | +| `sandbox.pin` | `name`, `pinned` | `{pinned, pinned_by}` | Operator-only. Unpinning restores the existing expiry, which may already have passed. | +| `sandbox.delete` | `name` | `{}` | Records expiry and clears any pin, then destroys owned resources in dependency order. A partial failure remains discoverable for reaper retry; an unknown sandbox returns `AgentError`. | + +Expired, unpinned sandboxes are deleted by a reaper inside `agentcompute`. +`sandbox.pin_identities` is an exact subject allowlist, empty by default; +unauthorized pin/create-pinned/unpin calls return `AgentError` without changes. +Both backends persist `pinned`, `pinned_by`, and `pinned_at` alongside expiry: +Incus in `user.agentcompute.*` project config, Lume in its sidecar. Every scan +logs pinned sandboxes at INFO, including the operator and pin timestamp. +Pins survive restart and leave normal operations usable after expiry. +Re-pinning preserves attribution; unpinning clears it without extending TTL. #### `image` — the curated catalog diff --git a/docs/docs/runbooks/agentcompute-service.md b/docs/docs/runbooks/agentcompute-service.md index 61d08cb..6ae2a95 100644 --- a/docs/docs/runbooks/agentcompute-service.md +++ b/docs/docs/runbooks/agentcompute-service.md @@ -52,8 +52,8 @@ tailnet. The Incus trust entry is the dedicated, unrestricted `agentcompute01` client. Never substitute `bootstrap-admin`, broaden an image-build or CI certificate, or reuse one of those identities. The MCP service accepts the static named bearer -identity `omp`; possession of its token grants every currently registered -capability. +identity `omp`; it can manage every sandbox. Pin and unpin additionally require +that subject in `sandbox.pin_identities`, which is empty by default. The Phase 2 live restricted-certificate check is the evidence for this exception: the `image-build` certificate saw an empty filtered list for @@ -84,6 +84,32 @@ sessions share it. The reaper scans once at startup and every 30 seconds. Do not run a second durable service or a long-lived STDIO process against the same sandbox set. +## Retain an interactive sandbox + +After installing a version with `sandbox.pin` support, add +`sandbox.pin_identities: [omp]` in the fleet-managed server configuration and +deploy it through the normal configuration workflow. Do not edit the managed +host file. An empty allowlist disables both pin and unpin for every identity; +existing pins remain pinned until explicitly unpinned or deleted. + +Discover the signatures with `search_api` and `describe_api`, then create with +`sandbox.create(name="keep", ttl_minutes=60, pinned=True)` or pin an existing +live sandbox with `sandbox.pin(name="keep", pinned=True)`. Incus and Mac +sandboxes use the same policy. Verify `pinned: true` and `pinned_by: "omp"` +in `sandbox.get` or `sandbox.list`. Each startup/30-second scan logs the +sandbox name, operator, and pin timestamp at INFO with `expires_at ignored`. + +Pinning does not change `expires_at` or lift the configured TTL maximum. +To release a pin, call `sandbox.pin(name="keep", pinned=False)`. +**If its expiry has already passed, the next scan deletes the sandbox and all +its instances, snapshots, and published images.** Extend it before unpinning +if you need more time. To destroy it immediately, use `sandbox.delete`. +Verify the sandbox disappears from the list after cleanup. + +A pin is not a backup or catalog promotion. Reusable images still require +recipes in `GilmanLab/agentcompute/images/`; `instance.publish` images remain +sandbox-local and are deleted with their sandbox. + ## Materialize deployment inputs Keep plaintext in one owner-only temporary directory. The trap removes local