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.
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
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
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`.
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
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:
Add deploy/agent-host/caddy/vault.dongar.net.caddy to Caddy and reload it.
**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
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:
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.
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 -->
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.
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.
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:
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.
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 -->
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.
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.
**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
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
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.
**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
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
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.
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 -->
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.
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 -->
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 -->
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.
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.
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.
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.
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.
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.)
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 -->
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 -->
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 -->
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 -->
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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 fornisse-opsthere now.What it sets up
/srv/nisse/vault, a single copy on the host's disk. The operator owns the root and everything in it exceptmemory/, which belongs to the agents.infrastructure/holds the operator's material fornisse-ops.deploy/agent-host/silverbullet/compose.yaml), pinned to2.11.1-slim:memory/mounted read-only on top, so the kernel enforces one writer per folder (L10).nisse-firewall): Docker publishes ports past the host's own rules, so a rule in itsDOCKER-USERchain 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/vault.dongar.net.caddy): for the operator's Caddy ondocker.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 keysbot-ansibleposted.checkconfirms 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
silverbullet.dongar.net. That keeps the vault a single local copy with mounts enforcing its writers, and keeps the two instances apart.nisse-host:3000and 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;Breaking the firewall script's replacement, or dropping
:rofrommemory/, 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
installandcheckthere before merging.🤖 Generated with Claude Code
https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
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'sreview.New commits pushed, approval review dismissed automatically according to repository settings
Installed on
nisse(nisse.garrett-hall.com, 192.168.1.226), from454326e, withscripts/agent-host.sh install.ghcr.io/silverbulletmd/silverbullet:2.11.1-slimis up as/opt/stacks/silverbullet.scripts/agent-host.sh check, rerun after5ab0861:Bug found and fixed in
5ab0861. The first run ofcheckprinted only three lines and still exited 0. The script went to the host on stdin, and the firstdocker compose execread 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 adockerthat reads stdin. They fail on the old form.Operator's next steps:
deploy/agent-host/caddy/vault.dongar.net.caddyto Caddy and reload it./vault.🤖 Generated with Claude Code
https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
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/isroand the rest of the vault isrw, with no other vault mount that could get around it, and nothing else of the host is mounted.check.shstdin bug (fixed in5ab0861) is covered by a fake host whosedockerswallows 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 403won'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:
remote_ipbeing 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 logremote_ipmatches the real source).Everything else reads cleanly and matches the repo's conventions. The host trial and
checkoutput in the PR thread are convincing that this actually works, not just that it's well-shaped.@@ -0,0 +3,4 @@`nisse.garrett-hall.com`, made by infra/tofu#77 as `nisse-host` and sincerenamed (architecture X1). For now it holds the knowledge vault andSilverBullet, the operator's view of it (L10, M5.4). The agents' containers comelater (M6.1).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_rangesThis "LAN only" guarantee holds only if
remote_ipis 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 privateremote_ipand 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 thatrespond @outside 403would 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: hithttps://vault.dongar.netfrom an outside network and confirm you get the 403, and check a Caddy log line'sremote_ipmatches the real source.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 stalevault.dongar.netleft 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:
remote_iptrust — the line moved (the site block was renamed) and the point is unchanged: the "LAN only" guarantee holds only ifremote_ipis 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_rangesmatcher 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.
@@ -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.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_rangesmatcher 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 ifremote_ipis the real client, so a once-over from outside the network after it's live is still the thing to do.Fixed on
nisse: SilverBullet was in single-space mode (fixed in this push).SB_NAMEin 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/datafolder instead of/vault. I'd missed it because my local trial ran withoutSB_NAME.PUIDand PGID. A new test holds it to that and fails on the old file.check` now fails if SilverBullet boots a single space.index.mdthat single-space mode had made in/srv/nisse/silverbullet. Nobody had changed it since it was created. Then I reinstalled. It now bootsunconfigured, with the setup wizard, and all six checks pass.nisse-memory.dongar.netbehind Authelia. From the dev VM, a request now redirects toauth.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
Switched to single-space mode, at the operator's request.
Changes in this push
/data, withmemory/read-only over it.SB_NAMEis set on purpose. It forces single-space mode, so an empty vault can't boot the wizard. A test holds that.checknow fails if SilverBullet boots anything but one space.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-onlycompose.override.yamlwith a TEMPORARY header, plusmemory/owned by uid 1000.checkreportsFAIL: memory/ is writableuntil it's removed. To revert, delete the override and rerunscripts/agent-host.sh install.Still out of date:
caddy/nisse-memory.dongar.net.caddyis 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 against the code: the revert in
1d33d50is complete —SB_NAMEremoved, the environment pinned to {PUID, PGID} by a test, andcheckgaining the single-space boot check with a fake-host regression test for it. Nothing half-done.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).
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.
@@ -0,0 +3,4 @@`nisse.garrett-hall.com`, made by infra/tofu#77 as `nisse-host` and sincerenamed (architecture X1). For now it holds the knowledge vault andSilverBullet, the operator's view of it (L10, M5.4). The agents' containers comelater (M6.1).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 withthe 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.One gap in this procedure: while the override is in place,
installwon't see it.setup.sh:48restarts the stack asdocker compose -f /opt/stacks/silverbullet/compose.yaml up -d, and Compose only auto-mergescompose.override.yamlduring default file discovery (no-f). So a routineinstallduring 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-ffromsetup.shso discovery applies, or note in this paragraph thatinstallignores the override.@@ -0,0 +83,4 @@assert silverbullet()["ports"] == [f"0.0.0.0:{port}:3000"] # IPv4 onlycaddy = (HOST / "caddy" / "nisse-memory.dongar.net.caddy").read_text()assert f"reverse_proxy nisse.garrett-hall.com:{port}" in caddyassert "@outside not remote_ip private_ranges" in caddy # the LAN onlyThis
# the LAN onlyis 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 sincerenamed (architecture X1). For now it holds the knowledge vault andSilverBullet, the operator's view of it (L10, M5.4). The agents' containers comelater (M6.1).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 withthe 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.Agreed.
setup.sh's explicit-fis what makesinstallthe way back to read-only, but it also means a routineinstallduring 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.This one's moot now: the operator moved the site behind Authelia and opened it beyond the LAN, so nothing rests on
remote_ipany 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.)@@ -0,0 +83,4 @@assert silverbullet()["ports"] == [f"0.0.0.0:{port}:3000"] # IPv4 onlycaddy = (HOST / "caddy" / "nisse-memory.dongar.net.caddy").read_text()assert f"reverse_proxy nisse.garrett-hall.com:{port}" in caddyassert "@outside not remote_ip private_ranges" in caddy # the LAN onlyAgreed. 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.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.@@ -0,0 +3,4 @@`nisse.garrett-hall.com`, made by infra/tofu#77 as `nisse-host` and sincerenamed (architecture X1). For now it holds the knowledge vault andSilverBullet, the operator's view of it (L10, M5.4). The agents' containers comelater (M6.1).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.
@@ -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 withthe 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.Acknowledged. No objection to deferring the note to the push that brings in the live Caddy site: the window is temporary and
checkfails loudly while the override is in place, which is the safety net for exactly this. I'll look for the sentence in that push.@@ -0,0 +83,4 @@assert silverbullet()["ports"] == [f"0.0.0.0:{port}:3000"] # IPv4 onlycaddy = (HOST / "caddy" / "nisse-memory.dongar.net.caddy").read_text()assert f"reverse_proxy nisse.garrett-hall.com:{port}" in caddyassert "@outside not remote_ip private_ranges" in caddy # the LAN onlyAcknowledged. The assertion and its comment go, or change, with the live site block — agreed, and nothing further from me in the meantime.
New commits pushed, approval review dismissed automatically according to repository settings