feat: decompose pct_create into atomic create + agent-driven install; add scoped destructive window
Closes the two remaining open points from the auto-continuation work.
1. Atomic pct_create (observability, the bigger of the two):
pct_create used to bundle create + apt install + post_install script into
one black-box multi-minute SSH call — the agent got back a single opaque
success/fail with no way to see (or fix) which step actually broke.
Removed the whole post-create provisioning block (and the now-dead
provisionScript/sanitizePkgs helpers + their tests). pct_create is now
create + start + register ONLY — fast, and its result is fed back to the
agent via auto-continuation almost immediately. The agent installs
packages and runs setup as its OWN sequence of `run` calls against the new
lxc:<hostname>, observing each command's real output and able to diagnose
and retry exactly the step that failed — the same recovery loop already
proven for the general case, now applied to installs too, instead of
requiring a separate black-box mechanism.
- services/post_install removed from the pct_create params struct and
from the MCP tool schema/SOUL.md docs.
- SOUL.md: explains the new flow, moves the Docker CLI gotcha and DNS
troubleshooting guidance to be steps the agent runs itself.
2. Scoped destructive window (targeted autonomy for recovery):
Verified live in the previous session that a destructive recovery (a
failed destroy needing stop-then-destroy on the same container) required
TWO separate typed confirmations for what was clearly one recovery
action. Added a narrow, TARGET-scoped 15-minute grant
(destructive_window.agent:<id>.target:<slug> in autonomy_settings,
shared key format across cmd/nomos and internal/mcp) that opens only
after an EXPLICIT typed confirmation (never loose assent) or an explicit
button-approval of a destructive step, and only ever covers further
destructive commands against that SAME target. A different target always
needs its own fresh confirmation — this narrows risk instead of loosening
it globally, unlike broadening the general assent window to cover
destructive actions would have.
- cmd/nomos/store.go: openDestructiveWindow/destructiveWindowActive/
executionTarget.
- cmd/nomos/agent.go: opens the window when a typed confirmation grants a
destructive chat-assent execution.
- internal/mcp/server.go: `run` tool checks the window before gating a
destructive command; auto-runs if active.
- internal/httpapi/phase3.go: DecideApproval opens the same window when a
destructive execution is approved via the button/API, for parity with
the chat-assent path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -89,15 +89,30 @@ of what a command does.
|
||||
|
||||
Before calling `request_execution`:
|
||||
- Check risk class via `get_entity` on the target
|
||||
- `pct_create` — `config_mutation`: provisions a new LXC AND installs its service in one
|
||||
approved step. Set `target` to the Proxmox HOST slug (e.g. `host:strong`), not the new
|
||||
container name. `params` is a JSON string: vmid (unused id), hostname, cores, memory (MB),
|
||||
disk_gb, ip (CIDR), gw, storage, template (omit to auto-pick newest debian on the host),
|
||||
privileged, nesting, mounts, and — to actually deliver a working service —
|
||||
`services` ([]apt packages) and `post_install` (shell run inside the container, e.g. a
|
||||
`git clone && docker compose up -d`). Prefer one pct_create with services+post_install
|
||||
over pct_create followed by many pct_exec approvals. Once approved, the LXC entity is
|
||||
created in the DB with `hosts` relationships and `state: provisioning`.
|
||||
- `pct_create` — `config_mutation`: **ATOMIC** — creates and starts a new LXC, nothing
|
||||
more. Set `target` to the Proxmox HOST slug (e.g. `host:strong`), not the new container
|
||||
name. `params` is a JSON string: vmid (unused id), hostname, cores, memory (MB), disk_gb,
|
||||
ip (CIDR), gw, bridge, storage, template (omit to auto-pick newest debian on the host),
|
||||
privileged, nesting, mounts. **No `services`/`post_install` — those were removed.** Once
|
||||
approved, the LXC entity is created in the DB with `hosts` relationships and
|
||||
`state: provisioning`.
|
||||
- **You install the service yourself, one step at a time, via `run` against the new
|
||||
`lxc:<hostname>` target — do NOT try to cram everything into pct_create.** This is
|
||||
deliberate: a single giant install script gave you back one opaque success/fail for a
|
||||
multi-minute black box, with no way to see (or fix) which specific step broke. Issuing
|
||||
your own `run` calls — `apt-get update`, `apt-get install -y docker.io`, the install
|
||||
script, the verify curl — means you see each command's real output and can diagnose and
|
||||
retry exactly the thing that failed, the same way you'd work at a real shell. You will
|
||||
be automatically re-invoked with pct_create's result (see "Automatic continuation"
|
||||
below) — don't poll, don't wait for the operator, just start issuing the install steps
|
||||
once you see it succeeded.
|
||||
- **DNS/network right after boot**: a fresh container's network can take a few seconds to
|
||||
come up. If your first `apt-get update` fails with a DNS/connectivity error, don't
|
||||
immediately blame the gateway (the pre-flight already validated that) — first retry
|
||||
after a short wait (`sleep 5`), and if it's still failing, check `/etc/resolv.conf`
|
||||
inside the container and fall back to a public resolver
|
||||
(`printf 'nameserver 1.1.1.1\n' > /etc/resolv.conf`) before concluding the network
|
||||
config itself is wrong.
|
||||
- **vmid**: omit or set 0 — a free cluster id is assigned automatically. Never reuse an
|
||||
existing container's id.
|
||||
- **networking — DHCP is the default, static is the exception**: use `"ip":"dhcp"` unless
|
||||
@@ -121,23 +136,21 @@ Before calling `request_execution`:
|
||||
"gateway unreachable, don't guess a different one, find a real neighbor or use DHCP"
|
||||
message — instead of a multi-minute hang or silent retry loop. If you see that error,
|
||||
the fix is to find a real neighbor's config or switch to DHCP, not to try a third guess.
|
||||
- If you set a static CIDR anyway and the *DNS resolver itself* (not the gateway) is the
|
||||
problem, the provisioner self-heals to a public resolver — but that only helps once the
|
||||
gateway/bridge are actually correct.
|
||||
- **Docker — CRITICAL**: Debian's `docker.io` package installs the Docker
|
||||
**daemon** but NOT the `docker` **CLI binary** on Debian 13 (trixie). The
|
||||
TypeType installer (and any script that calls `docker`) will fail with
|
||||
"command not found". Do NOT rely on `docker.io` alone. Instead:
|
||||
- Put `docker.io` in `services` (provides the engine + dependencies)
|
||||
- In `post_install`, FIRST install Docker CE CLI via
|
||||
"command not found". Do NOT rely on `docker.io` alone. Instead, as separate
|
||||
observable `run` steps against the new container:
|
||||
- `apt-get install -y docker.io` (provides the engine + dependencies)
|
||||
- THEN install Docker CE CLI via
|
||||
`curl -fsSL https://get.docker.com | sh` (provides the `docker` CLI +
|
||||
compose plugin), THEN run your installer.
|
||||
- Example post_install:
|
||||
`curl -fsSL https://get.docker.com | sh && docker compose version && curl -fsSL https://raw.githubusercontent.com/Priveetee/TypeType/main/scripts/install-stack.sh | bash && curl -fsS http://localhost:8080/health`
|
||||
compose plugin) — check its output before continuing.
|
||||
- THEN the actual install script (e.g. the service's own installer).
|
||||
- `docker-compose-plugin` is NOT in Debian's repos — always get it from
|
||||
get.docker.com.
|
||||
- **verify**: end `post_install` by confirming the service actually answers (e.g.
|
||||
`curl -fsS http://localhost:<port>/` ), so a green result means it truly works.
|
||||
- **verify**: your LAST step should confirm the service actually answers (e.g.
|
||||
`curl -fsS http://localhost:<port>/`), so a green result means it truly works — only
|
||||
report success to the operator once you've seen this pass.
|
||||
- If `destructive` or `config_mutation`: escalate to operator
|
||||
- If `reversible_low` with validated pattern: auto-act allowed
|
||||
|
||||
@@ -180,8 +193,16 @@ in chat), the system:
|
||||
install packages, edit configs, start services, etc. — no need to stop and
|
||||
re-ask for each step.
|
||||
3. `read_only` commands always auto-run (no approval needed, no window).
|
||||
4. `destructive` commands **never** auto-run — they always need an explicit
|
||||
typed confirmation ("I confirm ..."), even during an assent window.
|
||||
4. `destructive` commands **never** auto-run via the general assent window —
|
||||
they always need an explicit typed confirmation ("I confirm ...") or the
|
||||
operator clicking Approve on a card that says DESTRUCTIVE.
|
||||
5. **After that confirmation**, a short 15-minute window opens scoped to that
|
||||
ONE target — further destructive commands against the SAME target auto-run
|
||||
without asking again. This exists for multi-step destructive recovery
|
||||
(e.g. a destroy failed because the container was still running: you need
|
||||
`stop` then `destroy`, both destructive, same container — one confirmation
|
||||
should cover finishing that sequence). A different target ALWAYS needs its
|
||||
own fresh confirmation — the window never generalizes across targets.
|
||||
|
||||
**Your job after approval:** carry out the full plan. If a step fails, think
|
||||
about why, try an alternative approach, and continue. Only surface to the
|
||||
|
||||
Reference in New Issue
Block a user