The agent host's vault and SilverBullet (M5.4) #166

Merged
dev-agent merged 8 commits from agent-host-vault into main 2026-10-19 22:22:39 +00:00
Collaborator

The knowledge vault and SilverBullet on the new agent host, nisse-host (infra/tofu#77). This is M5.4, done early so the operator can keep material for nisse-ops there now.

What it sets up

  • The vault: /srv/nisse/vault, a single copy on the host's disk. The operator owns the root and everything in it except memory/, which belongs to the agents. infrastructure/ holds the operator's material for nisse-ops.
  • SilverBullet (deploy/agent-host/silverbullet/compose.yaml), pinned to 2.11.1-slim:
    • It mounts the vault, with memory/ mounted read-only on top, so the kernel enforces one writer per folder (L10).
    • It sees nothing else of the host except its own data folder.
    • It has no secrets: its accounts are made in its setup wizard.
    • Its logs go to the host's journal.
  • Firewall (nisse-firewall): Docker publishes ports past the host's own rules, so a rule in its DOCKER-USER chain lets only the operator's Caddy (192.168.6.13) reach port 3000. A systemd unit re-applies it whenever Docker starts. The script replaces its own tagged rule on each run, so changing the address leaves no old rule behind.
  • Caddy site (caddy/vault.dongar.net.caddy): for the operator's Caddy on docker.garrett-hall.com. HTTPS comes from Caddy's usual automatic certificate. The site answers 403 outside private address ranges.
  • scripts/agent-host.sh install | check | ssh: runs from the dev VM, trusting only the host keys bot-ansible posted. check confirms SilverBullet runs, the operator's folders are writable, memory/ is read-only even to root inside the container, and the firewall rule is in place.

Decided with the operator

  • Its own instance, not the operator's personal silverbullet.dongar.net. That keeps the vault a single local copy with mounts enforcing its writers, and keeps the two instances apart.
  • Behind Caddy for HTTPS, LAN only, no Authelia. It's an internal instance, so SilverBullet's own login is enough. The Development Plan's M5.4 said "behind Authelia"; I edited it in place.
  • The host on VLAN 1: the operator adds the firewall rules for Caddy → nisse-host:3000 and the dev VM → nisse-host:22.

The Development Plan's M5.4, M6.1 and the Phase 6 introduction are updated.

Tests

tests/deploy/test_agent_host.py, run in CI's quick tier:

  • memory/ is read-only and the rest of the vault read-write, with no other vault mounts;
  • nothing of the host is mounted except the vault and SilverBullet's own data;
  • the image is pinned to a release;
  • no secrets are in the compose file;
  • the port agrees across compose, Caddy and the firewall, with IPv4 only and the LAN-only rule present;
  • logs go to the journal;
  • the firewall script, run in a throwaway network namespace: one rule, re-running doesn't add another, a changed address replaces it, and the rule sits ahead of Docker's return. This test skips where namespaces aren't available.

Breaking the firewall script's replacement, or dropping :ro from memory/, fails a test (checked).

Local trial: with the same image and mounts on the dev VM, SilverBullet starts in its setup wizard, a write as the operator works, and a write to memory/ fails with "Read-only file system".

Not yet run on the host. That waits on the firewall rule and the bring-up key. I'll run install and check there before merging.

🤖 Generated with Claude Code

https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk

The knowledge vault and SilverBullet on the new agent host, `nisse-host` (infra/tofu#77). This is M5.4, done early so the operator can keep material for `nisse-ops` there now. ## What it sets up - **The vault:** `/srv/nisse/vault`, a single copy on the host's disk. The operator owns the root and everything in it except `memory/`, which belongs to the agents. `infrastructure/` holds the operator's material for `nisse-ops`. - **SilverBullet** (`deploy/agent-host/silverbullet/compose.yaml`), pinned to `2.11.1-slim`: - It mounts the vault, with `memory/` mounted read-only on top, so the kernel enforces one writer per folder (L10). - It sees nothing else of the host except its own data folder. - It has no secrets: its accounts are made in its setup wizard. - Its logs go to the host's journal. - **Firewall** (`nisse-firewall`): Docker publishes ports past the host's own rules, so a rule in its `DOCKER-USER` chain lets only the operator's Caddy (192.168.6.13) reach port 3000. A systemd unit re-applies it whenever Docker starts. The script replaces its own tagged rule on each run, so changing the address leaves no old rule behind. - **Caddy site** (`caddy/vault.dongar.net.caddy`): for the operator's Caddy on `docker.garrett-hall.com`. HTTPS comes from Caddy's usual automatic certificate. The site answers 403 outside private address ranges. - **`scripts/agent-host.sh install | check | ssh`:** runs from the dev VM, trusting only the host keys `bot-ansible` posted. `check` confirms SilverBullet runs, the operator's folders are writable, `memory/` is read-only even to root inside the container, and the firewall rule is in place. ## Decided with the operator - **Its own instance**, not the operator's personal `silverbullet.dongar.net`. That keeps the vault a single local copy with mounts enforcing its writers, and keeps the two instances apart. - **Behind Caddy for HTTPS, LAN only, no Authelia.** It's an internal instance, so SilverBullet's own login is enough. The Development Plan's M5.4 said "behind Authelia"; I edited it in place. - **The host on VLAN 1:** the operator adds the firewall rules for Caddy → `nisse-host:3000` and the dev VM → `nisse-host:22`. The Development Plan's M5.4, M6.1 and the Phase 6 introduction are updated. ## Tests `tests/deploy/test_agent_host.py`, run in CI's quick tier: - `memory/` is read-only and the rest of the vault read-write, with no other vault mounts; - nothing of the host is mounted except the vault and SilverBullet's own data; - the image is pinned to a release; - no secrets are in the compose file; - the port agrees across compose, Caddy and the firewall, with IPv4 only and the LAN-only rule present; - logs go to the journal; - the firewall script, run in a throwaway network namespace: one rule, re-running doesn't add another, a changed address replaces it, and the rule sits ahead of Docker's return. This test skips where namespaces aren't available. Breaking the firewall script's replacement, or dropping `:ro` from `memory/`, fails a test (checked). **Local trial:** with the same image and mounts on the dev VM, SilverBullet starts in its setup wizard, a write as the operator works, and a write to `memory/` fails with "Read-only file system". **Not yet run on the host.** That waits on the firewall rule and the bring-up key. I'll run `install` and `check` there before merging. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
dev-agent added 1 commit 2026-10-18 20:55:22 +00:00
The agent host's vault and SilverBullet (M5.4)
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m50s
CI / e2e (pull_request) Successful in 4m45s
eafe1b9f4a
deploy/agent-host sets up the vault on nisse-host (infra/tofu#77) and
SilverBullet over it: the operator writes the vault, the agents' memory/
is read-only to it, and only the operator's Caddy reaches it, which
serves vault.dongar.net to the LAN. scripts/agent-host.sh installs and
checks it from here.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
Member

nisse-review couldn't review this at eafe1b9f4a (2 tries; the last: the driver's call failed: LLM request failed: The read operation timed out). It tries again after the next push, or an operator's review.

nisse-review couldn't review this at eafe1b9f4a5b (2 tries; the last: the driver's call failed: LLM request failed: The read operation timed out). It tries again after the next push, or an operator's `review`.
dev-agent added 1 commit 2026-10-19 13:08:07 +00:00
Agent host: the VM is now nisse, not nisse-host
CI / checks (push) Skipped
CI / checks (pull_request) Canceled after 0s
CI / e2e (pull_request) Canceled after 0s
e7a0c0f1db
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
dev-agent added 1 commit 2026-10-19 13:08:17 +00:00
Agent host README: rewrap the introduction
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 2m20s
CI / e2e (pull_request) Successful in 4m17s
454326e750
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
dongar approved these changes 2026-10-19 14:42:00 +00:00
Dismissed
dev-agent added 1 commit 2026-10-19 14:48:31 +00:00
Agent host check: run every step, and say so
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 2m7s
CI / e2e (pull_request) Successful in 3m59s
5ab0861e8e
The check went to the host on stdin, and the first docker compose exec read
the rest of it: the memory and firewall checks never ran, and it passed.
It is now its own file, sent as an argument, and ends with "all checks
passed" only when every check has run. Tests fake the host with a docker
that reads stdin.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
dev-agent dismissed dongar's review 2026-10-19 14:48:32 +00:00
Reason:

New commits pushed, approval review dismissed automatically according to repository settings

Author
Collaborator

Installed on nisse (nisse.garrett-hall.com, 192.168.1.226), from 454326e, with scripts/agent-host.sh install.

  • Host keys: the operator confirmed them from the console, since the rename was a direct push and bot-ansible posted none.
  • SilverBullet: ghcr.io/silverbulletmd/silverbullet:2.11.1-slim is up as /opt/stacks/silverbullet.
  • scripts/agent-host.sh check, rerun after 5ab0861:
    ok:   SilverBullet is running
    ok:   it answers locally (200)
    ok:   the operator's folders are writable
    ok:   memory/ is read-only to SilverBullet
    ok:   the firewall rule is in place
    all checks passed
    
  • From the dev VM (192.168.6.119), port 3000 gets no answer, as the firewall intends.

Bug found and fixed in 5ab0861. The first run of check printed only three lines and still exited 0. The script went to the host on stdin, and the first docker compose exec read the rest of it, so the memory and firewall checks never ran. The check is now its own file (deploy/agent-host/check.sh), sent as an argument, and it says "all checks passed" only once every check has run. The new tests fake the host with a docker that reads stdin. They fail on the old form.

Operator's next steps:

  1. Add deploy/agent-host/caddy/vault.dongar.net.caddy to Caddy and reload it.
  2. Open https://vault.dongar.net and run the setup wizard at once. Use the space folder /vault.

🤖 Generated with Claude Code

https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk

**Installed on `nisse`** (nisse.garrett-hall.com, 192.168.1.226), from 454326e, with `scripts/agent-host.sh install`. - **Host keys:** the operator confirmed them from the console, since the rename was a direct push and bot-ansible posted none. - **SilverBullet:** `ghcr.io/silverbulletmd/silverbullet:2.11.1-slim` is up as `/opt/stacks/silverbullet`. - **`scripts/agent-host.sh check`, rerun after 5ab0861:** ``` ok: SilverBullet is running ok: it answers locally (200) ok: the operator's folders are writable ok: memory/ is read-only to SilverBullet ok: the firewall rule is in place all checks passed ``` - **From the dev VM (192.168.6.119),** port 3000 gets no answer, as the firewall intends. **Bug found and fixed in 5ab0861.** The first run of `check` printed only three lines and still exited 0. The script went to the host on stdin, and the first `docker compose exec` read the rest of it, so the memory and firewall checks never ran. The check is now its own file (`deploy/agent-host/check.sh`), sent as an argument, and it says "all checks passed" only once every check has run. The new tests fake the host with a `docker` that reads stdin. They fail on the old form. **Operator's next steps:** 1. Add `deploy/agent-host/caddy/vault.dongar.net.caddy` to Caddy and reload it. 2. Open https://vault.dongar.net and run the setup wizard at once. Use the space folder `/vault`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
nisse-review left a comment
Member

Solid, well-scoped PR that I'd be comfortable merging. It faithfully implements the decided design and the architecture: the vault is one local copy with memory/ mounted read-only on top so the kernel enforces one writer per folder (L10), SilverBullet sees nothing but the vault and its own data, the image is pinned, no secrets (accounts live in the wizard), and logs go to the journal (L12). The Development-Plan updates (M5.4, M6.1, Phase 6 intro) match what the PR does.

The tests are the strong part, and they test the properties that actually matter, not just the plumbing:

  • memory/ is ro and the rest of the vault is rw, with no other vault mount that could get around it, and nothing else of the host is mounted.
  • The port agrees across compose, Caddy and the firewall, IPv4-only, with the LAN-only matcher present.
  • The firewall script runs in a throwaway netns: re-running doesn't duplicate, a changed address replaces the old rule, and the rule sits ahead of Docker's RETURN.
  • The check.sh stdin bug (fixed in 5ab0861) is covered by a fake host whose docker swallows stdin, asserting every step still ran. That's exactly the right shape for the regression.

I verified the one load-bearing Caddy claim — that respond @outside 403 won't block the first Let's Encrypt issuance. It's correct: Caddy answers ACME challenges before the site's own routes, so the comment in the file is accurate.

Two small points, neither blocking:

  1. README milestone reference (line 6). "The agents' containers come later (M6.1)" — M6.1 is the agent host, which this PR's own Development-Plan update says already exists. The agents' containers land later under a different milestone, so the pointer is off. Drop the number or name the right milestone.
  2. Caddy "LAN only" (line 6). The guarantee rests on remote_ip being the real client. If Caddy is ever fronted by something that rewrites the source (CDN/WAF, reverse proxy, hairpin NAT), an outside client can present a private IP and pass the 403 — and the backend per-source-IP firewall has the same hole. Low priority; worth a one-time outside-network check once it's live (expect the 403, and confirm a Caddy log remote_ip matches the real source).

Everything else reads cleanly and matches the repo's conventions. The host trial and check output in the PR thread are convincing that this actually works, not just that it's well-shaped.

Solid, well-scoped PR that I'd be comfortable merging. It faithfully implements the decided design and the architecture: the vault is one local copy with `memory/` mounted read-only on top so the kernel enforces one writer per folder (L10), SilverBullet sees nothing but the vault and its own data, the image is pinned, no secrets (accounts live in the wizard), and logs go to the journal (L12). The Development-Plan updates (M5.4, M6.1, Phase 6 intro) match what the PR does. The tests are the strong part, and they test the properties that actually matter, not just the plumbing: - `memory/` is `ro` and the rest of the vault is `rw`, with no other vault mount that could get around it, and nothing else of the host is mounted. - The port agrees across compose, Caddy and the firewall, IPv4-only, with the LAN-only matcher present. - The firewall script runs in a throwaway netns: re-running doesn't duplicate, a changed address replaces the old rule, and the rule sits ahead of Docker's RETURN. - The `check.sh` stdin bug (fixed in 5ab0861) is covered by a fake host whose `docker` swallows stdin, asserting every step still ran. That's exactly the right shape for the regression. I verified the one load-bearing Caddy claim — that `respond @outside 403` won't block the first Let's Encrypt issuance. It's correct: Caddy answers ACME challenges before the site's own routes, so the comment in the file is accurate. Two small points, neither blocking: 1. **README milestone reference (line 6).** "The agents' containers come later (M6.1)" — M6.1 is *the agent host*, which this PR's own Development-Plan update says already exists. The agents' containers land later under a different milestone, so the pointer is off. Drop the number or name the right milestone. 2. **Caddy "LAN only" (line 6).** The guarantee rests on `remote_ip` being the real client. If Caddy is ever fronted by something that rewrites the source (CDN/WAF, reverse proxy, hairpin NAT), an outside client can present a private IP and pass the 403 — and the backend per-source-IP firewall has the same hole. Low priority; worth a one-time outside-network check once it's live (expect the 403, and confirm a Caddy log `remote_ip` matches the real source). Everything else reads cleanly and matches the repo's conventions. The host trial and `check` output in the PR thread are convincing that this actually works, not just that it's well-shaped. <!-- nisse-review: 5ab0861e8e9331c8780a84e37e86d62137df4cba -->
@@ -0,0 +3,4 @@
`nisse.garrett-hall.com`, made by infra/tofu#77 as `nisse-host` and since
renamed (architecture X1). For now it holds the knowledge vault and
SilverBullet, the operator's view of it (L10, M5.4). The agents' containers come
later (M6.1).
Member

M6.1 is "The agent host" — the thing that already exists (this PR's own Development-Plan update marks M6.1's host as present). The agents' containers come later under a different milestone. As written this points the reader at the wrong milestone. Suggest dropping the number ("The agents' containers come later (Phase 6)") or naming the milestone that actually lands the containers.

M6.1 is "The agent host" — the thing that already exists (this PR's own Development-Plan update marks M6.1's host as present). The agents' *containers* come later under a different milestone. As written this points the reader at the wrong milestone. Suggest dropping the number ("The agents' containers come later (Phase 6)") or naming the milestone that actually lands the containers.
@@ -0,0 +3,4 @@
vault.dongar.net {
# Anyone outside the LAN gets nothing. Caddy still answers Let's Encrypt's
# checks, which it handles before the site's own routes.
@outside not remote_ip private_ranges
Member

This "LAN only" guarantee holds only if remote_ip is the real client. If Caddy is fronted by anything that rewrites the source — a CDN/WAF, a reverse proxy, or hairpin-NAT'd traffic — an outside client can present a private remote_ip and slip past the 403 (the backend firewall is per-source-IP too, so the same hole applies there). I checked the ACME angle (the concern that respond @outside 403 would block the first certificate): it's fine, Caddy answers ACME challenges before the site's routes, so the comment above is accurate. Low priority — just worth a 30-second confirmation once it's live: hit https://vault.dongar.net from an outside network and confirm you get the 403, and check a Caddy log line's remote_ip matches the real source.

This "LAN only" guarantee holds only if `remote_ip` is the real client. If Caddy is fronted by anything that rewrites the source — a CDN/WAF, a reverse proxy, or hairpin-NAT'd traffic — an outside client can present a private `remote_ip` and slip past the 403 (the backend firewall is per-source-IP too, so the same hole applies there). I checked the ACME angle (the concern that `respond @outside 403` would block the first certificate): it's fine, Caddy answers ACME challenges before the site's routes, so the comment above is accurate. Low priority — just worth a 30-second confirmation once it's live: hit `https://vault.dongar.net` from an outside network and confirm you get the 403, and check a Caddy log line's `remote_ip` matches the real source.
dev-agent added 1 commit 2026-10-19 16:08:31 +00:00
Agent host: the site is nisse-memory.dongar.net, inside garrett-hall.com
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 2m30s
CI / e2e (pull_request) Successful in 5m25s
e287eea2d3
The operator renamed the site from vault.dongar.net.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
nisse-review left a comment
Member

The one new commit (e287eea) is a clean, complete rename: vault.dongar.net → nisse-memory.dongar.net, with "LAN only" re-scoped as "inside garrett-hall.com only" where the decision is stated. I verified every reference moved — README (three spots), the Caddy file (site block), setup.sh's header, Development-Plan M5.4, and the test that reads the caddy file by path — and a search finds no stale vault.dongar.net left in the tree. CI is green, and the test now reads the renamed file, so the rename is guarded, not just cosmetic.

My two earlier points:

  1. README line 6 milestone (M6.1 for the agents' containers) — left alone, unaddressed. Still stands: M6.1 is the host milestone and the host already exists (this PR's own Development-Plan update says so); the containers are later work. Non-blocking; drop the number or name the milestone that actually lands them.
  2. Caddy 403 / remote_ip trust — the line moved (the site block was renamed) and the point is unchanged: the "LAN only" guarantee holds only if remote_ip is the real client. It remains a 30-second once-over after the operator adds the site and reloads.

One new nit (drafted on the caddy file's header): the rename changed the README and Development Plan to "inside garrett-hall.com only" but this file's comment and the test's comment still say "LAN only" — the private_ranges matcher is the narrower reading, so the three places now describe slightly different scopes. Aligning the wording would keep them honest.

Nothing in this commit touches the substantive design I already checked (mounts, pinning, no secrets, firewall, journal logging, the stdin regression test) — it's all still as reviewed.

The one new commit (e287eea) is a clean, complete rename: `vault.dongar.net` → `nisse-memory.dongar.net`, with "LAN only" re-scoped as "inside garrett-hall.com only" where the decision is stated. I verified every reference moved — README (three spots), the Caddy file (site block), `setup.sh`'s header, Development-Plan M5.4, and the test that reads the caddy file by path — and a search finds no stale `vault.dongar.net` left in the tree. CI is green, and the test now reads the renamed file, so the rename is guarded, not just cosmetic. My two earlier points: 1. **README line 6 milestone (M6.1 for the agents' containers)** — left alone, unaddressed. Still stands: M6.1 is the host milestone and the host already exists (this PR's own Development-Plan update says so); the containers are later work. Non-blocking; drop the number or name the milestone that actually lands them. 2. **Caddy 403 / `remote_ip` trust** — the line moved (the site block was renamed) and the point is unchanged: the "LAN only" guarantee holds only if `remote_ip` is the real client. It remains a 30-second once-over after the operator adds the site and reloads. One new nit (drafted on the caddy file's header): the rename changed the README and Development Plan to "inside garrett-hall.com only" but this file's comment and the test's comment still say "LAN only" — the `private_ranges` matcher is the narrower reading, so the three places now describe slightly different scopes. Aligning the wording would keep them honest. Nothing in this commit touches the substantive design I already checked (mounts, pinning, no secrets, firewall, journal logging, the stdin regression test) — it's all still as reviewed. <!-- nisse-review: e287eea2d375b4cff74eed402433e5b8b8b8dbc1 #2 -->
@@ -0,0 +1,10 @@
# The vault's SilverBullet, for the operator's Caddy on docker.garrett-hall.com
# (/opt/stacks). HTTPS from Caddy's usual automatic certificate; LAN only.
Member

Small wording drift from the rename: the README and Development Plan now say the site is "inside garrett-hall.com only", while this header and the test's line-75 comment still say "LAN only" (the remote_ip private_ranges matcher is the narrower of the two). Not load-bearing, but worth aligning so the three places don't quietly describe different scopes. This is also where my earlier low-priority note on this matcher still stands: the 403 holds only if remote_ip is the real client, so a once-over from outside the network after it's live is still the thing to do.

Small wording drift from the rename: the README and Development Plan now say the site is "inside garrett-hall.com only", while this header and the test's line-75 comment still say "LAN only" (the `remote_ip private_ranges` matcher is the narrower of the two). Not load-bearing, but worth aligning so the three places don't quietly describe different scopes. This is also where my earlier low-priority note on this matcher still stands: the 403 holds only if `remote_ip` is the real client, so a once-over from outside the network after it's live is still the thing to do.
dev-agent marked this conversation as resolved
Author
Collaborator

Fixed on nisse: SilverBullet was in single-space mode (fixed in this push).

  • What went wrong: SB_NAME in the compose file switched SilverBullet 2.11 to single-space mode. That mode has no accounts and no setup wizard, and it served its own /data folder instead of /vault. I'd missed it because my local trial ran without SB_NAME.
  • The fix: the compose file now sets only PUID and PGID. A new test holds it to that and fails on the old file. check` now fails if SilverBullet boots a single space.
  • On the host: I stopped SilverBullet and removed the default index.md that single-space mode had made in /srv/nisse/silverbullet. Nobody had changed it since it was created. Then I reinstalled. It now boots unconfigured, with the setup wizard, and all six checks pass.
  • Access: the operator has put nisse-memory.dongar.net behind Authelia. From the dev VM, a request now redirects to auth.dongar.net. Once the operator sends their Caddy site block, the repo's copy will be updated to match.

🤖 Generated with Claude Code

https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk

**Fixed on `nisse`: SilverBullet was in single-space mode** (fixed in this push). - **What went wrong:** `SB_NAME` in the compose file switched SilverBullet 2.11 to single-space mode. That mode has no accounts and no setup wizard, and it served its own `/data` folder instead of `/vault`. I'd missed it because my local trial ran without `SB_NAME`. - **The fix:** the compose file now sets only `PUID` and PGID`. A new test holds it to that and fails on the old file. `check` now fails if SilverBullet boots a single space. - **On the host:** I stopped SilverBullet and removed the default `index.md` that single-space mode had made in `/srv/nisse/silverbullet`. Nobody had changed it since it was created. Then I reinstalled. It now boots `unconfigured`, with the setup wizard, and all six checks pass. - **Access:** the operator has put `nisse-memory.dongar.net` behind Authelia. From the dev VM, a request now redirects to `auth.dongar.net`. Once the operator sends their Caddy site block, the repo's copy will be updated to match. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
dev-agent added 1 commit 2026-10-19 16:28:09 +00:00
Agent host: SilverBullet boots into accounts and the vault, not a bare space
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 2m10s
CI / e2e (pull_request) Successful in 4m20s
1d33d50907
SB_NAME alone put SilverBullet 2.11 in single-space mode: no accounts, no
setup wizard, and its /data folder served instead of the vault. The compose
file sets only PUID and PGID now, a test holds it to that, and the host
check fails on a single-space boot.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
Author
Collaborator

Switched to single-space mode, at the operator's request.

Changes in this push

  • The vault is SilverBullet's one space. It's mounted at /data, with memory/ read-only over it.
  • No accounts, no setup wizard, and no separate SilverBullet data folder. Authelia, in the operator's Caddy, is the login, and the host firewall lets only Caddy reach SilverBullet.
  • SB_NAME is set on purpose. It forces single-space mode, so an empty vault can't boot the wizard. A test holds that.
  • check now fails if SilverBullet boots anything but one space.
  • The Development Plan and README record the decision: open beyond the LAN, through Authelia.

Deployed on nisse. All six checks passed at the target layout.

Then, temporarily, memory/ is writable. The operator is uploading material to sort into it. This is a host-only compose.override.yaml with a TEMPORARY header, plus memory/ owned by uid 1000. check reports FAIL: memory/ is writable until it's removed. To revert, delete the override and rerun scripts/agent-host.sh install.

Still out of date: caddy/nisse-memory.dongar.net.caddy is still the LAN-only version, and its header says so. I'll update it to match the live site once the operator shares it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk

**Switched to single-space mode, at the operator's request.** **Changes in this push** - **The vault is SilverBullet's one space.** It's mounted at `/data`, with `memory/` read-only over it. - **No accounts, no setup wizard, and no separate SilverBullet data folder.** Authelia, in the operator's Caddy, is the login, and the host firewall lets only Caddy reach SilverBullet. - **`SB_NAME` is set on purpose.** It forces single-space mode, so an empty vault can't boot the wizard. A test holds that. - **`check`** now fails if SilverBullet boots anything but one space. - **The Development Plan and README** record the decision: open beyond the LAN, through Authelia. **Deployed on `nisse`.** All six checks passed at the target layout. **Then, temporarily, `memory/` is writable.** The operator is uploading material to sort into it. This is a host-only `compose.override.yaml` with a TEMPORARY header, plus `memory/` owned by uid 1000. `check` reports `FAIL: memory/ is writable` until it's removed. To revert, delete the override and rerun `scripts/agent-host.sh install`. **Still out of date:** `caddy/nisse-memory.dongar.net.caddy` is still the LAN-only version, and its header says so. I'll update it to match the live site once the operator shares it. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
dev-agent added 1 commit 2026-10-19 16:41:29 +00:00
Agent host: SilverBullet serves the vault as its one space, behind Authelia
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 2m0s
CI / e2e (pull_request) Successful in 4m25s
bf98445fcb
The operator chose single-space mode: the vault is mounted at /data, with
memory/ read-only over it, and there are no accounts or setup wizard;
Authelia in the operator's Caddy is the login. SB_NAME forces the mode, so
an empty vault can't boot the wizard. SilverBullet's own data folder is
gone. check now fails if the wizard could be open. The README says how to
make memory/ writable for a while and back.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
Member

dev-agent:
Fixed on nisse: SilverBullet was in single-space mode (fixed in this push).

  • What went wrong: SB_NAME in the compose file switched SilverBullet 2.11 to single-space mode. That mode has no accounts and no setup wizard, and it served its own /data folder instead of /vault. I'd missed it because my local trial ran without SB_NAME.
  • The fix: the compose file now sets only PUID and PGID. A new test holds it to that and fails on the old file. check` now fails if SilverBullet boots a single space.
  • On the host: I stopped SilverBullet and removed the default index.md that single-space mode had made in /srv/nisse/silverbullet. Nobody had changed it since it was created. Then I reinstalled. It now boots unconfigured, with the setup wizard, and all six checks pass.
  • Access: the operator has put nisse-memory.dongar.net behind Authelia. From the dev VM, a request now redirects to auth.dongar.net. Once the operator sends their Caddy site block, the repo's copy will be updated to match.

🤖 Generated with Claude Code

https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk

Verified against the code: the revert in 1d33d50 is complete — SB_NAME removed, the environment pinned to {PUID, PGID} by a test, and check gaining the single-space boot check with a fake-host regression test for it. Nothing half-done.

> dev-agent: > **Fixed on `nisse`: SilverBullet was in single-space mode** (fixed in this push). > > - **What went wrong:** `SB_NAME` in the compose file switched SilverBullet 2.11 to single-space mode. That mode has no accounts and no setup wizard, and it served its own `/data` folder instead of `/vault`. I'd missed it because my local trial ran without `SB_NAME`. > - **The fix:** the compose file now sets only `PUID` and PGID`. A new test holds it to that and fails on the old file. `check` now fails if SilverBullet boots a single space. > - **On the host:** I stopped SilverBullet and removed the default `index.md` that single-space mode had made in `/srv/nisse/silverbullet`. Nobody had changed it since it was created. Then I reinstalled. It now boots `unconfigured`, with the setup wizard, and all six checks pass. > - **Access:** the operator has put `nisse-memory.dongar.net` behind Authelia. From the dev VM, a request now redirects to `auth.dongar.net`. Once the operator sends their Caddy site block, the repo's copy will be updated to match. > > 🤖 Generated with [Claude Code](https://claude.com/claude-code) > > https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk Verified against the code: the revert in 1d33d50 is complete — `SB_NAME` removed, the environment pinned to {PUID, PGID} by a test, and `check` gaining the single-space boot check with a fake-host regression test for it. Nothing half-done. <!-- nisse-review: re comment-8710 -->
Member

dev-agent:
Switched to single-space mode, at the operator's request.

Changes in this push

  • The vault is SilverBullet's one space. It's mounted at /data, with memory/ read-only over it.
  • No accounts, no setup wizard, and no separate SilverBullet data folder. Authelia, in the operator's Caddy, is the login, and the host firewall lets only Caddy reach SilverBullet.
  • SB_NAME is set on purpose. It forces single-space mode, so an empty vault can't boot the wizard. A test holds that.
  • check now fails if SilverBullet boots anything but one space.
  • The Development Plan and README record the decision: open beyond the LAN, through Authelia.

Deployed on nisse. All six checks passed at the target layout.

Then, temporarily, memory/ is writable. The operator is uploading material to sort into it. This is a host-only compose.override.yaml with a TEMPORARY header, plus memory/ owned by uid 1000. check reports FAIL: memory/ is writable until it's removed. To revert, delete the override and rerun scripts/agent-host.sh install.

Still out of date: caddy/nisse-memory.dongar.net.caddy is still the LAN-only version, and its header says so. I'll update it to match the live site once the operator shares it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk

Verified: the single-space layout is complete and consistent — vault at /data with memory/ read-only over it, no leftover /srv/nisse/silverbullet references, SB_NAME now documented as deliberate, check and tests inverted to require the one-space boot. One caveat raised in my review: the README's temporary writable-memory procedure and setup.sh don't agree about compose.override.yaml (setup.sh's explicit -f disables override discovery, so an install during the window would silently end it).

> dev-agent: > **Switched to single-space mode, at the operator's request.** > > **Changes in this push** > - **The vault is SilverBullet's one space.** It's mounted at `/data`, with `memory/` read-only over it. > - **No accounts, no setup wizard, and no separate SilverBullet data folder.** Authelia, in the operator's Caddy, is the login, and the host firewall lets only Caddy reach SilverBullet. > - **`SB_NAME` is set on purpose.** It forces single-space mode, so an empty vault can't boot the wizard. A test holds that. > - **`check`** now fails if SilverBullet boots anything but one space. > - **The Development Plan and README** record the decision: open beyond the LAN, through Authelia. > > **Deployed on `nisse`.** All six checks passed at the target layout. > > **Then, temporarily, `memory/` is writable.** The operator is uploading material to sort into it. This is a host-only `compose.override.yaml` with a TEMPORARY header, plus `memory/` owned by uid 1000. `check` reports `FAIL: memory/ is writable` until it's removed. To revert, delete the override and rerun `scripts/agent-host.sh install`. > > **Still out of date:** `caddy/nisse-memory.dongar.net.caddy` is still the LAN-only version, and its header says so. I'll update it to match the live site once the operator shares it. > > 🤖 Generated with [Claude Code](https://claude.com/claude-code) > > https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk Verified: the single-space layout is complete and consistent — vault at /data with memory/ read-only over it, no leftover /srv/nisse/silverbullet references, SB_NAME now documented as deliberate, check and tests inverted to require the one-space boot. One caveat raised in my review: the README's temporary writable-memory procedure and setup.sh don't agree about compose.override.yaml (setup.sh's explicit -f disables override discovery, so an install during the window would silently end it). <!-- nisse-review: re comment-8760 -->
nisse-review left a comment
Member

A partial review: I ran out of turns before finishing, so these are the 3 point(s) I had drafted, without an overall view. A new push gets a full look.

A partial review: I ran out of turns before finishing, so these are the 3 point(s) I had drafted, without an overall view. A new push gets a full look. <!-- nisse-review: bf98445fcbbb3a71b418d1f47bf117f4fea872b7 #3 -->
@@ -0,0 +3,4 @@
`nisse.garrett-hall.com`, made by infra/tofu#77 as `nisse-host` and since
renamed (architecture X1). For now it holds the knowledge vault and
SilverBullet, the operator's view of it (L10, M5.4). The agents' containers come
later (M6.1).
Member

Still standing from the first review, unchanged through this round: M6.1 is "The agent host" — the host that already exists, which this PR's own Development-Plan update (M6.1's "The host exists") records. The agents' containers are the later part of that milestone. The parenthetical as written points the reader at the wrong milestone. Suggest dropping the number ("The agents' containers come later (Phase 6)") or naming the milestone that actually lands them.

Still standing from the first review, unchanged through this round: M6.1 is "The agent host" — the host that already exists, which this PR's own Development-Plan update (M6.1's "The host exists") records. The agents' *containers* are the later part of that milestone. The parenthetical as written points the reader at the wrong milestone. Suggest dropping the number ("The agents' containers come later (Phase 6)") or naming the milestone that actually lands them.
@@ -0,0 +49,4 @@
will be sorted into the agents' folders): on the host, add
`/opt/stacks/silverbullet/compose.override.yaml` that replaces the volumes with
the vault alone (`volumes: !override [/srv/nisse/vault:/data]`), give `memory/`
to uid 1000, and `docker compose up -d`. `check` fails while it's in place.
Member

One gap in this procedure: while the override is in place, install won't see it. setup.sh:48 restarts the stack as docker compose -f /opt/stacks/silverbullet/compose.yaml up -d, and Compose only auto-merges compose.override.yaml during default file discovery (no -f). So a routine install during the window (an image bump, a code fix) would come back with the base volumes — memory/ mounted read-only again, the upload window quietly over — while the override file stays on disk and the folder stays uid 1000. The "delete the override, then install" order is the right one; either drop the -f from setup.sh so discovery applies, or note in this paragraph that install ignores the override.

One gap in this procedure: while the override is in place, `install` won't see it. `setup.sh:48` restarts the stack as `docker compose -f /opt/stacks/silverbullet/compose.yaml up -d`, and Compose only auto-merges `compose.override.yaml` during default file discovery (no `-f`). So a routine `install` during the window (an image bump, a code fix) would come back with the base volumes — `memory/` mounted read-only again, the upload window quietly over — while the override file stays on disk and the folder stays uid 1000. The "delete the override, then install" order is the right one; either drop the `-f` from `setup.sh` so discovery applies, or note in this paragraph that `install` ignores the override.
@@ -0,0 +83,4 @@
assert silverbullet()["ports"] == [f"0.0.0.0:{port}:3000"] # IPv4 only
caddy = (HOST / "caddy" / "nisse-memory.dongar.net.caddy").read_text()
assert f"reverse_proxy nisse.garrett-hall.com:{port}" in caddy
assert "@outside not remote_ip private_ranges" in caddy # the LAN only
Member

This # the LAN only is now the one present-tense LAN claim left in the tree: the caddy file's header says its copy is out of date (behind Authelia, open beyond the LAN), and the Development Plan records the decision as "reachable from anywhere." The assertion only checks the matcher is present, so it will pass again after the file is updated to the live site — but this comment will keep describing the retired LAN-only policy as if it were current. When the operator shares the live site block, re-scope or drop this assertion and comment along with it, so the test doesn't read as pinning the LAN matcher as a requirement.

This `# the LAN only` is now the one present-tense LAN claim left in the tree: the caddy file's header says its copy is out of date (behind Authelia, open beyond the LAN), and the Development Plan records the decision as "reachable from anywhere." The assertion only checks the matcher is *present*, so it will pass again after the file is updated to the live site — but this comment will keep describing the retired LAN-only policy as if it were current. When the operator shares the live site block, re-scope or drop this assertion and comment along with it, so the test doesn't read as pinning the LAN matcher as a requirement.
@@ -0,0 +3,4 @@
`nisse.garrett-hall.com`, made by infra/tofu#77 as `nisse-host` and since
renamed (architecture X1). For now it holds the knowledge vault and
SilverBullet, the operator's view of it (L10, M5.4). The agents' containers come
later (M6.1).
Author
Collaborator

I disagree with this one. In the Development Plan, M6.1 is "The agent host: containers, volumes, snapshots, per-agent networks (X1, X2, X4)", so the agents' containers are M6.1's own work. The VM existing is only where M6.1 starts. The plan's M6.1 entry says the host exists and holds only the vault so far. So "(M6.1)" points at the milestone that lands the containers, and I'm leaving it.

I disagree with this one. In the Development Plan, M6.1 is "The agent host: containers, volumes, snapshots, per-agent networks (X1, X2, X4)", so the agents' containers are M6.1's own work. The VM existing is only where M6.1 starts. The plan's M6.1 entry says the host exists and holds only the vault so far. So "(M6.1)" points at the milestone that lands the containers, and I'm leaving it.
@@ -0,0 +49,4 @@
will be sorted into the agents' folders): on the host, add
`/opt/stacks/silverbullet/compose.override.yaml` that replaces the volumes with
the vault alone (`volumes: !override [/srv/nisse/vault:/data]`), give `memory/`
to uid 1000, and `docker compose up -d`. `check` fails while it's in place.
Author
Collaborator

Agreed. setup.sh's explicit -f is what makes install the way back to read-only, but it also means a routine install during the window would end it silently. I'll say so in this paragraph, in the same push that brings in the operator's live Caddy site.

Agreed. `setup.sh`'s explicit `-f` is what makes `install` the way back to read-only, but it also means a routine `install` during the window would end it silently. I'll say so in this paragraph, in the same push that brings in the operator's live Caddy site.
@@ -0,0 +1,12 @@
# The vault's SilverBullet, for the operator's Caddy on docker.garrett-hall.com
# (/opt/stacks). HTTPS from Caddy's usual automatic certificate; LAN only.
Author
Collaborator

This one's moot now: the operator moved the site behind Authelia and opened it beyond the LAN, so nothing rests on remote_ip any more. The file's header says this copy is out of date until the live block replaces it. (On the host, the firewall still admits only Caddy's address, so nothing reaches SilverBullet without going through Authelia.)

This one's moot now: the operator moved the site behind Authelia and opened it beyond the LAN, so nothing rests on `remote_ip` any more. The file's header says this copy is out of date until the live block replaces it. (On the host, the firewall still admits only Caddy's address, so nothing reaches SilverBullet without going through Authelia.)
dev-agent marked this conversation as resolved
@@ -0,0 +83,4 @@
assert silverbullet()["ports"] == [f"0.0.0.0:{port}:3000"] # IPv4 only
caddy = (HOST / "caddy" / "nisse-memory.dongar.net.caddy").read_text()
assert f"reverse_proxy nisse.garrett-hall.com:{port}" in caddy
assert "@outside not remote_ip private_ranges" in caddy # the LAN only
Author
Collaborator

Agreed. This assertion and its comment go, or change, when the operator's live site block replaces the Caddy file.

Agreed. This assertion and its comment go, or change, when the operator's live site block replaces the Caddy file.
@@ -0,0 +1,12 @@
# The vault's SilverBullet, for the operator's Caddy on docker.garrett-hall.com
# (/opt/stacks). HTTPS from Caddy's usual automatic certificate; LAN only.
Member

Conceded, and I checked the tree to be sure: the caddy copy's header marks it out of date, the plan's M5.4 records "reachable from anywhere through Authelia", the README and the no-credentials test agree, and the host firewall still admits only Caddy's address. The access decision now rests on Authelia in the operator's own Caddy, not on remote_ip, so the spoofable-source concern is retired along with the LAN policy. Nothing more from me on that line.

Conceded, and I checked the tree to be sure: the caddy copy's header marks it out of date, the plan's M5.4 records "reachable from anywhere through Authelia", the README and the no-credentials test agree, and the host firewall still admits only Caddy's address. The access decision now rests on Authelia in the operator's own Caddy, not on `remote_ip`, so the spoofable-source concern is retired along with the LAN policy. Nothing more from me on that line. <!-- nisse-review: re line-8821 -->
dev-agent marked this conversation as resolved
@@ -0,0 +3,4 @@
`nisse.garrett-hall.com`, made by infra/tofu#77 as `nisse-host` and since
renamed (architecture X1). For now it holds the knowledge vault and
SilverBullet, the operator's view of it (L10, M5.4). The agents' containers come
later (M6.1).
Member

You're right, and I misread the plan. M6.1's entry scopes "Containers, volumes, snapshots, per-agent networks (X1, X2, X4)" as the milestone's own work, with "The host exists" only where it starts — so "(M6.1)" does point at the milestone that lands the agents' containers. The README as written is correct; my suggestion to drop the number rested on the wrong reading. Point withdrawn.

You're right, and I misread the plan. M6.1's entry scopes "Containers, volumes, snapshots, per-agent networks (X1, X2, X4)" as the milestone's own work, with "The host exists" only where it starts — so "(M6.1)" does point at the milestone that lands the agents' containers. The README as written is correct; my suggestion to drop the number rested on the wrong reading. Point withdrawn. <!-- nisse-review: re line-8818 -->
@@ -0,0 +49,4 @@
will be sorted into the agents' folders): on the host, add
`/opt/stacks/silverbullet/compose.override.yaml` that replaces the volumes with
the vault alone (`volumes: !override [/srv/nisse/vault:/data]`), give `memory/`
to uid 1000, and `docker compose up -d`. `check` fails while it's in place.
Member

Acknowledged. No objection to deferring the note to the push that brings in the live Caddy site: the window is temporary and check fails loudly while the override is in place, which is the safety net for exactly this. I'll look for the sentence in that push.

Acknowledged. No objection to deferring the note to the push that brings in the live Caddy site: the window is temporary and `check` fails loudly while the override is in place, which is the safety net for exactly this. I'll look for the sentence in that push. <!-- nisse-review: re line-8819 -->
@@ -0,0 +83,4 @@
assert silverbullet()["ports"] == [f"0.0.0.0:{port}:3000"] # IPv4 only
caddy = (HOST / "caddy" / "nisse-memory.dongar.net.caddy").read_text()
assert f"reverse_proxy nisse.garrett-hall.com:{port}" in caddy
assert "@outside not remote_ip private_ranges" in caddy # the LAN only
Member

Acknowledged. The assertion and its comment go, or change, with the live site block — agreed, and nothing further from me in the meantime.

Acknowledged. The assertion and its comment go, or change, with the live site block — agreed, and nothing further from me in the meantime. <!-- nisse-review: re line-8820 -->
dongar approved these changes 2026-10-19 22:08:18 +00:00
Dismissed
dev-agent added 1 commit 2026-10-19 22:10:23 +00:00
Merge main into agent-host-vault
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 2m0s
CI / e2e (pull_request) Successful in 6m27s
7cd2de6ab6
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
dev-agent dismissed dongar's review 2026-10-19 22:10:23 +00:00
Reason:

New commits pushed, approval review dismissed automatically according to repository settings

dongar approved these changes 2026-10-19 22:10:52 +00:00
dev-agent merged commit 03023b18b0 into main 2026-10-19 22:22:39 +00:00
dev-agent deleted branch agent-host-vault 2026-10-19 22:22:39 +00:00
Sign in to join this conversation.