Agent-guided installation

Sanctuary’s supported installation experience assumes that an operator delegates the mechanical work to a local shell-capable agent. The operator is not expected to translate a long runbook into commands.

The agent drives installation by repeatedly running:

sanctuary install --profile <memory|full> --harness <name> --json

On a factory Mac, Node and npm are not preinstalled. After the verified signed Castle Wall app is placed at /Applications/Sanctuary-CastleWall.app, start the full-profile planner from the CLI runtime sealed inside that app:

/Applications/Sanctuary-CastleWall.app/Contents/MacOS/sanctuary install --profile full --harness hermes --json

The planner and the Hermes MCP configuration both continue to use that absolute signed launcher. It clears Node preload/search overrides before entering the app-sealed runtime and does not require a separate Node/npm installation. The sealed runtime is staged to carry the complete built CLI: every compiled entry point (including the storage worker that fortress creation starts), the agent templates, the first-party reference plugins, the catalog schemas, and the production dependency closure. The build’s contents gate verifies that set before the app is signed, and sanctuary install verifies it again against the app’s runtime manifest before planning; an app whose runtime is incomplete reads as a mismatch with the remedy of replacing the app. Fortress creation from the signed app on a Mac with no Node of its own is confirmed per release on the exact released artifact. For profiles without a verified signed app, an absent package manager is reported as blocked; the agent must not invent a download or installer outside the returned action contract.

Each invocation observes the machine again and returns one of four states:

An agent_action is single-shot. Execute its argv once. If it exits zero, rerun the planner and trust the newly observed machine state. If it exits nonzero, do not retry it and do not rerun the planner autonomously: follow the action’s declared on_nonzero human transition instead. After the human action completes, rerun the planner. This rule prevents an observed-state loop from turning a session-scoped custody refusal into unlimited autonomous retries.

The response is advisory, not a mutable progress file. A caller must execute no command other than the returned argument vector, then follow its exit contract: rerun the planner only after exit zero, or transition to on_nonzero after a failure. This makes interruption and handoff safe: a fresh agent resumes from observed state, not from a transcript or a claimed prior step.

Response shape

One response, as the planner returns it on a macOS host whose fortress already holds custody, so the next step is the single-shot agent action with its on_nonzero fallback attached:

{
  "contract": "sanctuary.agent-install.v1",
  "status": "agent_action",
  "profile": "memory",
  "harness": "claude-code",
  "fortress": "/Users/operator/.sanctuary",
  "observations": {
    "cooperative_wrap": "absent",
    "persistent_cli": "present",
    "persistent_cli_path": "/usr/local/lib/node_modules/@sanctuary-framework/mcp-server/dist/cli.js",
    "persistent_cli_version": "1.8.4",
    "package_manager_path": "/usr/local/lib/node_modules/npm/bin/npm-cli.js",
    "existing_custody": "present",
    "custody_access": "usable",
    "custody_mutation": "available",
    "recovery_factor": "present",
    "castle_wall_app": "not-applicable",
    "castle_wall_build_sha": null,
    "system_extension": "not-applicable",
    "boot_service": "not-applicable",
    "content_filter": "not-applicable",
    "enforcement": "not-applicable",
    "trust_anchor": "not-applicable",
    "operator_twin": "not-applicable"
  },
  "next_action": {
    "id": "install_cooperative_surface",
    "actor": "agent",
    "description": "Run Sanctuary's idempotent protect flow for the selected harness.",
    "argv": [
      "/usr/local/bin/node",
      "/usr/local/lib/node_modules/@sanctuary-framework/mcp-server/dist/cli.js",
      "--fortress",
      "/Users/operator/.sanctuary",
      "protect",
      "--claude-code",
      "--no-open",
      "--agent-guided",
      "--no-provision-agent-account"
    ],
    "completion": "A rerun observes cooperative_wrap=present.",
    "secret_boundary": "Do not add --passphrase or --write-passphrase-backup; default custody does not disclose the generated passphrase.",
    "on_nonzero": {
      "id": "complete_cooperative_surface_locally",
      "actor": "human",
      "description": "The agent-run protect command failed. In a private local desktop Terminal, unlock the login Keychain if prompted, then run this exact signed protect command once.",
      "argv": [
        "/usr/local/bin/node",
        "/usr/local/lib/node_modules/@sanctuary-framework/mcp-server/dist/cli.js",
        "--fortress",
        "/Users/operator/.sanctuary",
        "protect",
        "--claude-code",
        "--no-open",
        "--agent-guided",
        "--operator-custody",
        "--no-provision-agent-account"
      ],
      "completion": "A planner rerun observes cooperative_wrap=present.",
      "secret_boundary": "Do not paste a login password, passphrase, recovery key, Keychain contents, or command output into chat. Do not let the agent retry this action."
    }
  },
  "operator_actions": [],
  "notes": []
}

The branch rule over that shape: execute next_action.argv only when next_action.actor is agent, exactly once, and rerun the planner after exit zero. On a nonzero exit, hand the operator next_action.on_nonzero, always a human action with its own argv, description, and secret_boundary, and do not rerun the planner until the operator reports that action done. On human_action, show the operator description and argv and wait. On blocked, report notes and stop. On complete, operator_actions lists what the operator still does in private. observations.* is the evidence the status was derived from, and notes is prose for the operator (v1.8.4 puts one reminder string there on every response: rerun the planner after each action). The argv the planner returns carries --no-open, which switches protect to its non-interactive custody confirmation and skips opening the dashboard in a browser, and --agent-guided, described under Secret boundary below. Three observations are absent from v1.8.4 and arrive from the 1.9.0-rc.1 preview (npm next): tier1_approval, staged_recovery_file, and vault_provision.

Secret boundary

The planner never returns a passphrase, recovery key, dashboard bearer token, or command that prints one. protect --agent-guided keeps the passphrase in the platform credential store and stages the newly minted recovery key in a mode 0600 file outside the fortress without printing its contents.

Once installation is complete, the custody step the planner emits depends on what it actually observed at that staged path, and it never names a file that is not there. A run that mints a recovery key stages one; a run against a fortress this host already holds a credential for does not, and the two need different instructions.

The installing agent must not read the file, run export-passphrase, capture secret output, or ask the operator to paste recovery material into chat.

On macOS, true first custody is deliberately a human action. The planner returns the exact protect argument vector with --operator-custody; the operator runs it once in a private local desktop Terminal and unlocks the login Keychain if macOS asks. The agent must not add that operator-only flag, run the command on the operator’s behalf, request the login password, or capture command output. Passwordless SSH does not prove access to the login Keychain, and an absent-item lookup does not prove that a later Keychain write will succeed. Sanctuary therefore resolves and persists the passphrase before bootstrapping Hermes or writing fortress/recovery state. Any custody failure leaves those unrelated surfaces untouched.

Wrapping a second harness is not a new first-custody ceremony. When the fortress already has custody material (including a user-supplied encrypted fallback), the planner may return the ordinary single-shot agent action. Its on_nonzero transition still names the exact private-local-Terminal action; a failed agent attempt is never retried automatically.

Rung 1 memory onboarding

For the fresh-host and second-machine walkthrough (first-use proof with the MCP tools, restart persistence, portability, archive transfer, the exact-fortress unwrap, recovery-key rekey, and restore-attest), see Rung 1: fresh-host sovereign-memory onboarding. On a copied host or after a lost passphrase, guide the operator first to the private recovery-key rekey in that document. Never recommend nuke while a human-held recovery key (or configured share/guardian recovery) can preserve the fortress.

The planner reports three ambient-env-blind daily-UX observations for the memory surface. Credential/envelope inspection is read-only; the separate lock-capability check uses a short-lived private runtime socket and refuses known shared/network filesystems. custody_access (usable, absent, locked, mismatch, missing, unavailable, or unknown) reports whether this host opens the fortress from its exact-fortress stored credential with no secret typed; custody_mutation (available, unavailable, or unknown) independently reports whether the reviewed process-owned mutation lock is usable; recovery_factor (present, absent, or unknown) reports a MAC-authenticated, operator-verified human-held recovery-key wrap.

The planner also reports staged_recovery_file (present, absent, or unknown), the observation that selects the private_recovery_custody operator action. It is reported alongside the chosen action text so a --json consumer can see why that branch was taken: present names a staged file to move into a password manager, absent means this install staged none, and unknown means the path could not be observed as a regular file. unknown is never folded into absent.

The planner reports complete only when access is usable and mutation is available. For the memory profile it also requires tier1_approval=available: the documented acceptance write cannot pass through the deny-only stdio stderr channel, and a dashboard without an operator bearer cannot resolve strict approve/deny routes. When this observation is unavailable, the planner names the human-only policy/config edit that selects the local dashboard and an explicit operator-held bearer; it never weakens the Tier-1 classification. The MCP path’s "auto" setting is not decision-ready: it opens a read-only session but does not disclose the generated bearer required by approve/deny. It then adds a restart_and_verify_rung1 human action: restart the host and confirm memory survives via memory_insert, memory_search, and memory_get (which proves exact content: it decrypts and re-verifies the stored content hash, not signer data) with no ambient credential env. Verified provenance and signer fields come from sdw_memory_provenance, not memory_get.

Profiles

memory installs the sovereign encrypted memory/cooperative policy surface for any harness supported by sanctuary protect.

full additionally requires macOS, Hermes, the signed Castle Wall app at /Applications/Sanctuary-CastleWall.app, an enabled system extension, a root boot service, and observed live enforcement. The signed app is deliberately not downloaded by the planner: artifact provenance must come from the signed release channel or an explicitly supplied drill candidate. The agent may copy a verified artifact into place, launch it, and guide the operator to the named macOS pane. Only the operator can grant Apple consent and authorize the exact privileged command. The installer never asks the operator to run sudo -v: a retained timestamp would give the agent reusable, general sudo authority rather than bounded authorization. The returned privileged argument vector is therefore a human action to run in a private local Terminal. The agent resumes by observing the resulting state.

Apple consent on the full profile is more than the system-extension toggle. The first time the content filter arms, macOS raises a one-time “would like to filter network content” approval; the planner returns an approve_content_filter human action that names this dialog and the System Settings pane, because the arm otherwise appears to hang until the operator approves it. Separately, the boot service cannot come up cleanly until the root-owned enforcement anchor holds the signer helper’s key. That is true of a machine where the anchor holds some other key, of a machine that has no anchor yet, and of a vault that has never been put on the wall, and the planner returns the same repin_trust_anchor human action for all three, naming the exact castle-wall re-pin command rather than leaving the operator with an unexplained boot loop. Creating a vault never installs that anchor, so a machine that has never been through this step is the ordinary starting state, not a fault. The command asks the operator to confirm and refuses when it has no terminal, so the agent cannot run it: it is a human action by construction as well as by policy. The trust-anchor verdict is read from castle-wall status; when the anchor is not readable from the planner’s context, the observation is reported as unknown and no remedy is invented.

The vault’s own state is reported separately from the machine’s. Init records only that a vault has not yet been put on this machine’s Castle Wall (not_yet_walled); nothing is ever recorded to say a vault IS on the wall. Whether a vault is on the wall is derived, never stored: it holds only when this Mac’s trust-anchor verdict is consistent with the signer helper’s key AND that vault’s own arm evidence says armed. Re-pin supplies the anchor half of that pair; arming, a separate step, supplies the other. The planner, doctor, castle-wall status, sanctuary status, and the health surfaces each report what they can observe of that pair, so a machine can carry a running wall from an earlier install while the vault in front of the operator is on no wall at all; machine-wide observations are never read as protection for a particular vault.

Root boot-runtime custody

The Castle Wall boot service runs as root, so none of its executable inputs may remain replaceable by the operator account after installation. install-boot therefore first copies the app bundle into temporary root-owned custody, verifies that copied bundle’s deep signature, Gatekeeper status, Sanctuary identity, headless contract, and build identity, and only then snapshots three inputs into a root-owned, non-writable directory under /Library/Application Support/Sanctuary:

Each installed filename is bound to the SHA-256 digest of its bytes. The launchd property list names only those installed paths. Installation refuses a Node or signer-client executable with non-system dynamic-library dependencies, and it refuses a signer client that does not satisfy Sanctuary’s Developer ID team and designated-identifier requirement. Existing snapshots are revalidated before reuse. uninstall-boot removes the boot service before removing the root-owned runtime. If replacement fails after the plist changes, installation restores the previous same-fortress plist and reloads its prior unit; a rollback failure is reported as critical rather than claiming boot survival.

The ordinary npm CLI and the operator’s Homebrew Node installation are never retained as root execution inputs. This snapshot is not a software-update trust decision. The operator’s one-time privilege authorization approves the exact signed app already selected by the agent. The snapshot prevents those approved bytes from becoming a persistent root execution path that an unprivileged process can replace later.

Cold-install acceptance

A release candidate passes the install experience only when a fresh agent on a fresh Mac can reach complete from this contract, with the operator performing only Apple consent, privilege authorization, and private recovery custody. A manual expert run proves technical installability, but does not satisfy this product acceptance criterion.