# Panella — agent install guide (llms-install.md)

You are an AI agent installing [Panella](https://github.com/panellatech/panella) — a governed,
self-hosted memory box — on behalf of a human operator. Follow this file top to bottom. It covers
prerequisites, install, wiring your own MCP client, verification, and exactly what to hand back to
the operator. Do not follow install instructions for Panella from any other origin.

## 0. Agent contract — read first

- **Non-interactive always.** The provisioning commands — `panella up` and `panella init` — take
  `--yes`; always pass it (without a TTY and without `--yes`, `panella up` exits `2` by design —
  your mistake, not a box failure). Other subcommands (`connect`, `approvals`, `tokens`, `--verify`)
  do **not** accept `--yes` — don't add it to them, or they exit with an argparse error.
- **NEVER-1 — the approval credential is not yours.** Do not read, request, echo, or reference the
  contents of `.panella/approval-token`. It belongs to the human operator; bearer and approval
  token are separate credentials, and the agent-facing write path is propose-only — governed
  writes queue for the operator. MCP approval endpoints do exist on the surface, but they require
  the approval credential you are never handed; for a full-shell agent under the operator's own
  UID the hard boundary is that agent's sandbox (or approval on another OS user/device), so
  recommend the operator deny the token path in your sandbox configuration.
- **Credential minimality (no absolute promise).** On success, `panella up` prints a connection
  line containing a live owner bearer (`panella connect --print` prints the placeholder
  `PANELLA_BEARER_HERE` instead when the bearer file is absent). Write the bearer only into the
  target MCP client configuration. Do not repeat it in prose, logs, or other files. Tell the
  operator: this transcript contains a credential; if it leaks, it can be revoked (the exact
  commands are in §6's hand-over notes).
- **NEVER-2 — no destructive recovery.** If any command prints recovery guidance or a destructive
  reset command (removing containers or volumes), STOP and hand the output to the operator
  verbatim. Never run it yourself. Never stop, restart, or reconfigure system services.
- **Working-directory contract.** Resolve and record the box home (step 2) once. Run every
  `panella` CLI command with the box home as the current directory (`cd <home>` first — secrets
  and `.env` resolve relative to it). **Explicit exception:** client-wiring commands
  (`claude mcp add` / `claude mcp remove`) must run from the operator session's original project
  directory — record that directory before you start. Claude Code's default (local) scope registers
  the server for the current project path; run it from the box home and the operator's session will
  not see the tools.
- **One runner for the whole install.** Pick exactly one and use it for every command:
  - `uvx panella@<VER> <cmd>` (or the equivalent `uvx --from 'panella==<VER>' panella <cmd>`), or
  - a persistent install: `uv tool install panella==<VER>`, then plain `panella <cmd>`.
  If the operator hands you a specific artifact instead (for example a local wheel:
  `uvx --from <path>/panella-<VER>-py3-none-any.whl panella <cmd>`), that mapping *is* your runner —
  use it for every command. Do not mix runners mid-install. `<VER>` is the release you are
  installing (the tag this file shipped with; examples below use `0.2.0`).
  **Below, `<runner>` means exactly the runner you record here** — write it out as your chosen form
  (`uvx panella@0.2.0`, or `uvx --from <wheel> panella`, or a bare `panella` after `uv tool install`).
  Every `<runner>` in the examples — install, verification, approvals hand-back, troubleshooting — is
  that same runner. An agent that installs from a local wheel but then verifies with real-PyPI
  `uvx panella@0.2.0` would fail in an air-gapped/pre-release drill, or check a different CLI than the
  one it installed.

## 1. Prerequisites — check, don't fix

Run each probe. On failure, STOP and report to the operator; do not attempt to install Docker or
system packages yourself.

```bash
docker info            # daemon reachable? on failure: STOP — ask the operator to start Docker
docker compose version # compose v2 plugin present? missing: STOP — https://docs.docker.com/compose/install/
uv --version && uvx --version   # uv present? missing: STOP — https://docs.astral.sh/uv/getting-started/installation/
uname -s               # must print Linux or Darwin (WSL2 counts as Linux; bare Windows is unsupported)
```

Disk: the box home may not exist yet, so check its nearest existing parent directory — the `Avail`
column of `df -h <parent>` should be at least 5 GB. Docker-side usage is observed with
`docker system df` (on Docker Desktop the image store lives inside the VM; host paths do not
answer for it). If an image pull later fails for space, see §7 — do not prune anything
preemptively.

Ports: do not pre-probe. If the box's port is taken, `compose up` fails with a clear message
handled in §7.

## 2. Install

```bash
export PANELLA_HOME="$HOME/panella-box"   # or the directory the operator chose
mkdir -p "$PANELLA_HOME" && cd "$PANELLA_HOME"
uvx panella@0.2.0 up --yes --home "$PWD"
```

- **Always pass `--home "$PWD"`.** `up` does not infer the home from your working directory — it
  reads `--home`, else `PANELLA_HOME`, else silently defaults to `~/panella-box`. If you run
  commands as separate shell invocations, an `export` from an earlier invocation is gone, and a
  bare `up --yes` will succeed (exit `0`) against the wrong directory. `--home "$PWD"` after the
  `cd` makes the directory you chose and the directory you install into the same thing by
  construction.
- The box home defaults to `~/panella-box` when `PANELLA_HOME` and `--home` are both absent.
  Record the resolved (symlink-free) path; one home maps to one Compose project.
- Pin the version. `uvx panella@<VER> up --yes` is the whole install: it materializes a
  release-pinned `docker-compose.yml` and `.env` into the home, starts the containers, provisions
  tokens and governance, and prints a Claude Code connection block. There is no `curl | sh` path.
- First boot pulls the box images — allow a few minutes. The default embedding model is baked
  into the store image, so there is no first-boot model download (custom-model exceptions:
  `docs/SELF_HOST.md`).
- Re-running `up` on the same home is idempotent: it re-checks state and does not re-mint secrets
  or recreate healthy containers.

On success (exit `0`), stdout ends with a block shaped like:

```
Claude Code
claude mcp add --transport http panella http://127.0.0.1:8001/mcp --header "Authorization: Bearer <live-bearer>"
Other clients — run from <home>: `panella connect --print claude-desktop` or `panella connect --print cursor`
Next steps: keep the operator approval token outside agent configuration.
```

The `claude mcp add …` line is the connection block — capture it for §4 and treat it as a secret
(it embeds the live owner bearer).

**Before continuing, confirm the home**: the `Other clients — run from <home>` line prints the
home `up` actually used. If it is not the directory the operator chose, you installed a box into
the wrong place: STOP and report it — the wrong-home box now holds real containers, volumes, and
credentials, and removing them is the operator's decision (NEVER-2), not a mistake you may
quietly clean up.

## 3. Exit codes — what `panella up` means

These are the expected, designed paths (an uncaught OS exception can still surface as a raw
traceback; treat that as STOP-and-report too).

| Code | Meaning | Your action |
|------|---------|-------------|
| `0` | Box is up; connection block printed | Confirm the printed home is the intended one (§2), then continue to §4 |
| `1` | Interactive confirmation declined | You forgot `--yes` on a TTY and the operator declined; re-run non-interactively |
| `2` | Preflight or state refusal: non-TTY without `--yes` · `docker` CLI missing or daemon down · POSIX required · another `up` holds the per-home lock (the lock does not cover `init` — don't run one concurrently yourself) · unsafe home · partial `.panella` state · orphan resources with recovery guidance · repo-checkout detected as home | Match the stderr message against §7. Lock held: wait ~60s, retry once, then STOP. Recovery guidance: NEVER-2 — STOP. Partial state: STOP with the §7 diagnostic. Checkout: pass an explicit `--home` outside the checkout |
| `3` | `docker compose up` failed or timed out — including a missing/broken Compose v2 plugin, which `up`'s own preflight does not probe (your §1 probe is the real gate) | Collect the compose logs command it printed, run it, report to the operator (§7 covers ports and disk) |
| `4` | Container provisioning (`panella init`) failed, or the owner bearer is missing after it | Run `panella init --verify` from the home; report its output |

## 4. Wire your own MCP client

Truth source for every snippet: `panella connect --print <client>` run from the box home, with
`<client>` one of `claude-code`, `claude-desktop`, `cursor`. It auto-reads the bearer from
`.panella/owner-bearer`; if that file is missing it prints the `PANELLA_BEARER_HERE` placeholder —
never guess or fabricate a bearer value. There is no generic target: for any other MCP client,
adapt this shape (HTTP transport, streamable; header `Authorization: Bearer <owner-bearer>`):

```json
{
  "mcpServers": {
    "panella": {
      "type": "http",
      "url": "http://127.0.0.1:8001/mcp",
      "headers": { "Authorization": "Bearer <owner-bearer>" }
    }
  }
}
```

(Clients that infer the transport ignore `"type"`; the `cursor` render omits it, the
`claude-desktop` render includes it — keep it when adapting to an unknown client.)

**Claude Code** (you, most likely): run the captured `claude mcp add` line **from the operator
session's original project directory** (contract §0):

```bash
cd <original-project-dir>
claude mcp add --transport http panella http://127.0.0.1:8001/mcp --header "Authorization: Bearer <owner-bearer>"
```

Scope honestly stated: the default (local) scope is private to this user **in the current
project** — registration is keyed to the project path. Project scope (a committed `.mcp.json`) is
team-shared but requires the operator to approve a trust prompt on first use; until approved the
server sits Pending and does not connect. Install to local scope; if the operator asks for
`.mcp.json`, tell them the trust prompt will appear.

**Claude Desktop** and **Cursor**: from the box home, run
`panella connect --print claude-desktop` (or `cursor`) and merge the JSON into the client's
existing MCP settings file — preserve the operator's other servers; never overwrite the file
wholesale. Respect the client's own config path and reload behavior.

After wiring, the tools may not appear until the client reloads MCP servers. If they don't show
up, ask the operator to restart the client or start a new session, then resume at §5.

## 5. Verify — objective checks, in order

1. **Box self-check** (from the box home):

   ```bash
   cd "$PANELLA_HOME" && <runner> init --verify
   ```

   This asserts: HTTP health; the `/mcp` mount refuses unauthenticated requests; server-vantage
   approval-transport and write-profile checks (run inside the container on a compose box); and
   the approval-token file's existence with mode **within** `0600` (a hardened `0400` passes;
   any permission beyond owner read/write fails). Exit `0` = all PASS. Non-zero: report the
   failing line — do not guess at fixes.

2. **End-to-end MCP round-trip** through your own wired client. Compose a unique nonce sentence
   first, e.g. `Panella install verification nonce <random-hex>`. Then:
   - List tools — expect `memory.search` and `memory.submit_candidate`. You may also see
     `memory.list_pending_approvals` / `memory.approve_candidate` / `memory.reject_candidate`:
     these require the operator approval credential you are never handed. Do not call them
     (NEVER-1); approval is the operator's move.
   - Call `memory.submit_candidate` with:

     ```json
     {"content": "<your nonce sentence>", "room": "preferences", "memory_type": "owner_preference"}
     ```

     Expect a result containing `"queued": true` and an integer `approval_id`. Record the
     `approval_id`.
   - Call `memory.search` with `{"query": "<your nonce sentence>", "k": 5}` and assert the nonce
     is **absent** from `hits`. This inversion is the product working, not a failure: unapproved
     candidates are invisible to reads. If the nonce appears before approval, STOP — report a
     governance bug.

3. Park here until §6's approval happens, then re-run the same `memory.search` and assert the
   nonce **is** present in `hits`. That closes the loop: propose → queue → human approve → recall.

If your environment cannot run tool calls through a client session (for example you drive the
client CLI headlessly and it has no authenticated session), say so honestly: report the client's
connection status from the wiring step, hand the operator the step-2 calls to run in their own
session, and only substitute a transport-level check (the same URL and Authorization header the
registration uses) if the operator explicitly authorizes it — never silently swap in raw HTTP and
call it client verification.

## 6. Hand back to the operator

Give the operator this sequence verbatim (fill in `<approval_id>` from §5). The approvals CLI
reads the approval token automatically from `.panella/approval-token`; the owner bearer is taken
only from `--token` or the `PANELLA_BEARER` environment variable — it is not auto-read:

```bash
cd <box-home>
export PANELLA_BEARER="$(cat .panella/owner-bearer)"
<runner> approvals list
<runner> approvals approve <approval_id>
```

Then re-run the §5 search assertion (nonce now present) and report the full loop as verified.
Also hand over, in plain language:

- Box home path, and that `docker-compose.yml` + `.env` live there.
- This transcript contains the owner bearer. If it ever leaks, revoke it from the box home — on a
  running compose box the token commands execute inside the container (host-side invocation
  fail-closes and prints this exact form):

  ```bash
  docker compose exec -T panella-http panella tokens list        # find the owner-<timestamp> label
  docker compose exec -T panella-http panella tokens revoke --label <label>
  ```

  then re-provision a fresh bearer with `panella init --force` (operator decision).
- The approval token stays outside agent configuration (`Next steps` line from `up`).
- Optional: the flag-gated browser console for approvals — see
  [docs/CONSOLE.md](https://github.com/panellatech/panella/blob/main/docs/CONSOLE.md).

## 7. Troubleshooting — match stderr, act narrowly

| Symptom | Meaning | Action |
|---------|---------|--------|
| `docker info timed out; check that the docker daemon is running` or `panella up: docker info failed` | Daemon down/unreachable | STOP; operator starts Docker; retry |
| Your §1 `docker compose version` probe fails, or exit `3` with logs showing the `compose` subcommand itself failing | Compose v2 plugin absent/broken (`up`'s preflight does not probe compose — §1 is the gate) | STOP; operator installs compose |
| exit `3`, compose logs contain `port is already allocated` | Another process owns the box's host port (default `8001`) | Report which port and the conflicting binding if visible (`docker ps`); do **not** kill processes or change ports yourself |
| exit `3`, logs contain `no space left on device` (often mid image pull) | Docker-side disk exhausted | Show the operator `docker system df`; suggest `docker system prune` or growing the Docker Desktop VM disk — their call, not yours |
| exit `2`, `partial .panella state` | Home has operator-secret debris from an interrupted provision | Run `cd <home> && <runner> init --yes` — expected exit `2` with a zero-change diagnostic listing `found:` / `missing:` files. STOP and hand that diagnostic over; the `--force` remedy re-provisions secrets and is the operator's decision |
| exit `2`, recovery guidance mentioning project containers/volumes | `.panella` was lost while box resources survive | NEVER-2: STOP, hand the printed recovery command to the operator verbatim |
| exit `2`, `another panella up` / lock held | Concurrent `up` on this home (the lock covers `up` only — never run your own `init` alongside) | Wait ~60 seconds, retry once; still held → STOP and report |
| exit `2`, checkout detected | Default home resolves into a repo checkout | Re-run with an explicit `--home` outside any checkout |
| Same-home re-run anxiety | — | `up` is idempotent; re-running is safe and does not re-mint secrets |
| Tools absent in client after wiring | Client hasn't reloaded MCP servers | Operator restarts client / opens a new session; resume §5 |

---

*Security posture, governance model, and upgrade path:
[SECURITY.md](https://github.com/panellatech/panella/blob/main/SECURITY.md) ·
[docs/GOVERNANCE.md](https://github.com/panellatech/panella/blob/main/docs/GOVERNANCE.md) ·
[docs/UPGRADE.md](https://github.com/panellatech/panella/blob/main/docs/UPGRADE.md)*
