test(albok): sshfs pathname-withdrawal test for the .browse/ view passes (unit 4.3 exposure gate)

Replica of a browse view on nh3-albok, Linux sshfs 3.7.3 client in an ephemeral
container on nh3-dev, N=5 per condition. Fresh open of a never-opened child
through a cached listing: ENOENT 5/5 with sshfs defaults and 5/5 with hour-long
client caches. Null control (no rename) opens 5/5; held fd and failed-rename
positive controls read 5/5. Replica removed afterwards.

Also closes the post-clear checks in persistent-memory: fv-ml1 GPU 0/1 plateau,
nh3-nas usage, and the mislabelled sfsrv-ana pre-pbs4 item.
This commit is contained in:
vh
2026-10-09 20:23:30 -07:00
parent 9ba2ce476b
commit 172d893cdf
8 changed files with 1213 additions and 6 deletions
+2 -4
View File
@@ -120,15 +120,12 @@ _As of 2026-10-09 ~1955 PT. The newest subsection is first; older subsections ca
- **Nothing is mid-action.** No claims held (`scripts/ops-log claims`). eshpfi is pushed through this snapshot's commit. ⚠ The working tree AND index are shared with infra-hermes and subagents: commit with `git commit -- <paths>` (auto-memory `feedback_shared_git_index_commit_pathspecs`). `graphify-out/GRAPH_REPORT.md` stays modified and uncommitted on purpose (auto-regenerated).
- **Approved, NOT built (do not start before Prime says so after this snapshot):** SVOS app-traffic ingress for U13. Details, release (`afab123`, v0.0.9, schema 12) and gotchas are in the 2026-10-09 Recent-decisions entry; svos-dev's edit doc is `docs/operations/u13-svos-edit.md`; highseat-dev owns the schedule.
- **Owed by me (no input needed):**
- albok `.browse/` exposure test before ANY share: pick a surface (sshfs `follow_symlinks` is the lead), rename `.browse/` on nh3-albok, and a fresh open through the client's cached listing must FAIL; also record the failed-rename case. Report on albok-dev's thread; the operator's user needs the restricted wings' groups on the share host. → `stacks/albok-service/README.md`
- fv-ml1 GPU 0/1 plateau read (Beszel `stats.g.<n>.mu` × 1.024) ~10-10; every seat restarted at 0034 10-08.
- Destroy `zroot/ROOT/debian@pre-patch-20261008` on fv-ml1 ~10-15 if nothing regressed.
- sfsrv-ana `pre-pbs4` snapshot delete is unverified (infra-ops has no key there; ask infra-hermes).
- Re-read nh3-nas usage after the 10-08/09 GCs (79% at 10-07 2350).
- nh3-svos housekeeping: releases `6c6abbe` and `a737a06` are no longer referenced by the front door and can be deleted (`ac42ace` holds the same static as the current `b588510`).
- **Watching: Worldtree memory gate.** 10-09 FAILED on filing/user (9, 11, 8 of 12; median 0.75 < 0.80), the first fail that is not #416. Bootstrap from 27 prior runs puts P(median ≤ 9) ≈ 0.06 per batch, and the misses are the habitual fixtures, so it is not called a regression yet. But per-run means drifted 10.6 → 10.0 → 9.7 → 9.3 since the 10-08 fv-ml1 patch (writer + judge = flash-next on GPU 2). If the 10-10 batch also fails filing, infra-hermes frames it as a possible regression after that patch and sends it to me; then investigate flash-next. ⚠ worldtree-dev has not read mail in 6+ days, so the reports dead-end there (Prime's call, below).
- **Watching: jekyll on `sec-reasoning` (cyberprev, GPU 0)** since ~1435 10-06 (Prime moved it off gen-large). sec holds 337k KV tokens. jekyll's ~180k context plus a long sec prompt (up to 192k) can overflow it, as gen-large did (preemptions; decode down to 2–16 tok/s). jekyll's context grows ~1k/call toward the 262k ceiling. It runs on the shared all-agents-local key; a key of its own would make its traffic attributable.
- **Waiting on Prime:**
- albok `.browse/` share: the sshfs withdrawal test PASSED (Linux client). Before a share: which wings his nh3-albok user (`vh`) gets (`albok-read` = fleet + library, `albok-personal` = notes; `albok-feedback` stays default-deny), and, if the workstation is macOS, a re-run of `scripts/oneoff/albok-browse-withdrawal/` on it.
- Who sees Worldtree gate failures while worldtree-dev is not reading mail (Prime, Miranda, or keep waiting).
- Rotate the fv-ml1 BMC password? It was printed into a local session transcript on 10-08 (never left nh3-dev); infra-ops leans no.
- albok's 200-object load check, 2.01× against a 2× bar (albok-dev: the ruling is the operator's).
@@ -189,6 +186,7 @@ Every vLLM seat on a shared card (GPU 0, GPU 1) has byte-pinned KV (`--kv-cache-
## Recent decisions
- `[2026-10-09]` **Post-clear checks closed (2015–2025 PT):** albok `.browse/` sshfs pathname-withdrawal test PASSED (replica on nh3-albok, Linux sshfs 3.7.3 client in a container; fresh open ENOENT 5/5 default and 5/5 hour-long caches; null + positive controls ok; failed rename = path served; reported to albok-dev) → `scripts/oneoff/albok-browse-withdrawal/README.md`. fv-ml1 plateau (Beszel 120m, 42 h after the 0034 10-08 restart): GPU 0 flat at 5,606 MiB free (~3.9 GB at parakeet's cap), GPU 1 median 9,459 free (~3.3 GB at intern-decision's 32k peak), no creep; GPU 2 643 MiB free = flash-next util 0.96, single tenant, by design. nh3-nas `/volume1` 71% (79% on 10-07). The "sfsrv-ana pre-pbs4" item was a mislabel: the pre-pbs4 snapshots were pfi-pve VM 100 and nh3-pve VM 105, dropped by infra-hermes 10-05 and read gone 10-09.
- `[2026-10-09]` **albok `library` building (fiction, nonfiction) live for unit 4.5; Nemi + albok-dev re-minted with library grants (token grants are immutable), old tokens revoked 1555 by albok-dev.** → `stacks/albok-service/README.md`
- `[2026-10-09]` **Homepage: The High Seat's description no longer says "SVOS" (Prime: author it yourself); SVOS's now says "Internal network + mesh".**
- `[2026-10-09]` **APPROVED by Prime (10-09), BUILD DEFERRED until after his snapshot/clear: SVOS app-traffic ingress for U13** (apps on nh3-dev reach the nh3-svos kernel). Shape agreed with svos-dev: (1) kernel `--app-listen 127.0.0.1:8410`, which never trusts loopback (svos-dev builds it in U9a); (2) a Caddy origin `svos-kernel.phasefinal.com` on 10.100.50.43, no Basic auth, bearer passed, app paths only, allowlisted to nh3-dev; (3) kernel→bridge over plain LAN HTTP; kernel→seat host (terminal) via nh3-dev's wildcard Caddy `svos-seathost.nh3.phasefinal.com` (TLS, because the leg carries secrets). nh3-dev is NOT on the mesh; the contracts now say "fleet network". `--app-listen` is ON MAIN (`68c9933`, 10-09). It ships in kernel **0.0.9** with schema **11 → 12** (migration 0012 rebuilds dispatches + summons, so the 'schema 11' acceptance line changes and a pre-refresh backup is mandatory). At the cutover the bridge gets three owner-approved runtime grants (app-postoffice → `app:app-miranda` write, `app:app-postoffice` write, `ns:pages` write, all autonomous) and installs as `svos-postoffice[push]` with `althing-core` from `~/development/althing` in apps-env. **Release is cut: svoperatingsystem `afab123` (tag v0.0.9)**: kernel 0.0.9, svos-postoffice 0.0.2, svos-terminal 0.0.3, svos-connector 0.0.7, svos-agent 0.0.3, svos-miranda 0.0.2 (U9a + U5b). The SVOS side of the cutover is written up in `docs/operations/u13-svos-edit.md` (app listener on nh3-svos, terminal app moved to nh3-dev with `[seat_host]`, bridge on nh3-dev with the converse mapping). Gotchas: the terminal app serves WITHOUT its seat host if the socket or launch dir fails (one stderr line); `serve` refuses `--app-listen` on the primary port. Build only when the edit is scheduled (highseat-dev owns the schedule) and NOT before the snapshot; the runbook refresh is ours at deploy. Tracking: svos-dev thread `01M4GPXSBKQHCQ30FVPH4R3N7G`.
@@ -0,0 +1,65 @@
# albok browse view: sshfs pathname-withdrawal test (unit 4.3 exposure gate)
The albok unit 4.3 contract gates any workstation exposure of a wing's `.browse/` on infra-ops
demonstrating, on the chosen share surface, that renaming `.browse/` away on the server denies a
**fresh open of a never-opened child through a listing the client has cached**. The rename is what
albok-service does when it withholds a view (`.browse/` → `.browse.withheld/`).
**Result (2026-10-09 2019–2021 PT): PASS on sshfs `follow_symlinks`, Linux client.** Raw output:
`results-20261009-2019.json`; replica ids: `manifest.json`.
## Harness
- **Server:** a replica of a browse view on nh3-albok (`setup-replica.py`): 60 × 1 MiB objects at
`<wing>/<id[:2]>/<id>/<blob>`, `.browse/stash/<name>` → `../../<id[:2]>/<id>/<blob>`, the same shape
as the live view. Same root ext4 and the same OpenSSH `sftp-server` as `/srv/albok`. The live view
was NOT used: renaming it races albok-service's link/unlink and a failed mandatory unlink withholds
the wing. The replica was removed after the run (ops-log 2026-10-10T03:22:22Z).
- **Client:** ephemeral `debian:bookworm-slim` container on nh3-dev (`run.sh`): SSHFS 3.7.3,
libfuse 3.14.0, FUSE kernel interface 7.31, host kernel 6.1. Mount: `ro,follow_symlinks` plus SSH
options; the mount root is `.browse/` alone.
- **Per trial**, on a fresh mount: list `stash/` and stat every child (the client's caches now hold
the listing and every child's attributes); open+close child A; open child B and **hold** the fd;
rename on the server with `os.rename`; then immediately list again, stat C, open C (never opened
before), read B at offset 1 MiB − 8 KiB (past readahead, so it must reach the server), re-open A.
- **Conditions, N = 5 each**, distinct children every trial:
`null` (no rename), `default` (sshfs defaults), `aggressive` (`kernel_cache,entry_timeout=3600,
attr_timeout=3600,dir_cache=yes,dcache_timeout=3600`), `failed_rename` (target pre-created
non-empty, so `rename(2)` refuses with ENOTEMPTY).
## Results
| condition | rename | cached list after | stat C after | **open C (fresh)** | held B far read | re-open A | list at +25 s |
|---|---|---|---|---|---|---|---|
| null (null control) | none | 60 | ok | **opens, bytes ok** 5/5 | ok 5/5 | opens 5/5 | n/a |
| default | ok | 60 (stale) | ok (cached) | **ENOENT 5/5** | ok 5/5 | ENOENT 5/5 | ENOENT 5/5 |
| aggressive caches | ok | 60 (stale) | ok (cached) | **ENOENT 5/5** | ok 5/5 | ENOENT 5/5 | n/a |
| failed_rename | refused ENOTEMPTY | 60 | ok | **opens, bytes ok** 5/5 | ok 5/5 | opens 5/5 | n/a |
The fresh open was attempted 2–6 ms after the rename returned. Every trial started and ended with
the server in the clean state (`.browse/` present, no `.browse.withheld`).
- **The gate holds.** The client still listed the child and stat'ed it from cache, and the open
failed anyway: each sshfs open is an SFTP OPEN by path, resolved on the server, and hour-long
client caches do not change that.
- **Controls.** With no rename every open succeeds (the method does not manufacture failures). The
held fd keeps reading after the rename, and a refused rename leaves every path served, so the
instrument does register access that survives a rename (positive controls).
- **What a rename does NOT withdraw:** a file already open on the client keeps reading through its
SFTP handle. That is the contract's narrowed guarantee (new access by pathname only).
- **Failed rename:** the path remains and is served. The service cannot deny opens it could not
withdraw; its `BROWSE_WITHHELD` marker only stops publication.
## Limits
- **Proven for the Linux kernel FUSE client only.** A macOS workstation runs a different FUSE
backend (macFUSE kext, macFUSE FSKit, or fuse-t, which is NFS-backed and may cache handles the way
the contract warns NFS does). Re-run this harness on the real workstation before trusting that
surface: rebuild the replica, then run `run-trials.py` directly with `ALBOK_TEST_KEY`,
`ALBOK_TEST_KNOWN_HOSTS`, `ALBOK_TEST_MNT` (and `ALBOK_TEST_SRV`) set.
- **The mount root is a presentation choice, not a boundary.** The SSH user's authority is whatever
its groups can read: a member of the wing's read group can mount the wing root and see raw object
directories, sidecars with their sensitivity records, and git history. The contract already says
this ("the wing group's raw read is authority over files").
- The committed `run-trials.py` adds env overrides and a umount fallback after the run; the container
defaults it ran with are unchanged.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,880 @@
{
"started_note": "times are monotonic seconds; see run.sh for wall clock",
"sshfs_base": [
"ro",
"follow_symlinks",
"IdentityFile=/keys/id",
"UserKnownHostsFile=/keys/known_hosts",
"StrictHostKeyChecking=yes",
"BatchMode=yes"
],
"aggressive": [
"kernel_cache",
"entry_timeout=3600",
"attr_timeout=3600",
"dir_cache=yes",
"dcache_timeout=3600"
],
"trials": [
{
"cond": "null",
"trial": 0,
"A": "doc00.md",
"B": "doc01.md",
"C": "doc02.md",
"server_before": "True False",
"pre_list": 60,
"pre_A": {
"opened": true,
"read": 4096,
"content_ok": true
},
"pre_B_ok": true,
"post_list": {
"n": 60
},
"post_stat_C": {
"ok": true,
"size": 1048576
},
"post_open_C": {
"opened": true,
"read": 4096,
"content_ok": true,
"secs_after_rename": 0.002
},
"post_held_B": {
"read": 4096,
"content_ok": true
},
"post_reopen_A": {
"opened": true,
"read": 4096,
"content_ok": true
},
"server_after": "True False"
},
{
"cond": "null",
"trial": 1,
"A": "doc03.md",
"B": "doc04.md",
"C": "doc05.md",
"server_before": "True False",
"pre_list": 60,
"pre_A": {
"opened": true,
"read": 4096,
"content_ok": true
},
"pre_B_ok": true,
"post_list": {
"n": 60
},
"post_stat_C": {
"ok": true,
"size": 1048576
},
"post_open_C": {
"opened": true,
"read": 4096,
"content_ok": true,
"secs_after_rename": 0.002
},
"post_held_B": {
"read": 4096,
"content_ok": true
},
"post_reopen_A": {
"opened": true,
"read": 4096,
"content_ok": true
},
"server_after": "True False"
},
{
"cond": "null",
"trial": 2,
"A": "doc06.md",
"B": "doc07.md",
"C": "doc08.md",
"server_before": "True False",
"pre_list": 60,
"pre_A": {
"opened": true,
"read": 4096,
"content_ok": true
},
"pre_B_ok": true,
"post_list": {
"n": 60
},
"post_stat_C": {
"ok": true,
"size": 1048576
},
"post_open_C": {
"opened": true,
"read": 4096,
"content_ok": true,
"secs_after_rename": 0.002
},
"post_held_B": {
"read": 4096,
"content_ok": true
},
"post_reopen_A": {
"opened": true,
"read": 4096,
"content_ok": true
},
"server_after": "True False"
},
{
"cond": "null",
"trial": 3,
"A": "doc09.md",
"B": "doc10.md",
"C": "doc11.md",
"server_before": "True False",
"pre_list": 60,
"pre_A": {
"opened": true,
"read": 4096,
"content_ok": true
},
"pre_B_ok": true,
"post_list": {
"n": 60
},
"post_stat_C": {
"ok": true,
"size": 1048576
},
"post_open_C": {
"opened": true,
"read": 4096,
"content_ok": true,
"secs_after_rename": 0.002
},
"post_held_B": {
"read": 4096,
"content_ok": true
},
"post_reopen_A": {
"opened": true,
"read": 4096,
"content_ok": true
},
"server_after": "True False"
},
{
"cond": "null",
"trial": 4,
"A": "doc12.md",
"B": "doc13.md",
"C": "doc14.md",
"server_before": "True False",
"pre_list": 60,
"pre_A": {
"opened": true,
"read": 4096,
"content_ok": true
},
"pre_B_ok": true,
"post_list": {
"n": 60
},
"post_stat_C": {
"ok": true,
"size": 1048576
},
"post_open_C": {
"opened": true,
"read": 4096,
"content_ok": true,
"secs_after_rename": 0.002
},
"post_held_B": {
"read": 4096,
"content_ok": true
},
"post_reopen_A": {
"opened": true,
"read": 4096,
"content_ok": true
},
"server_after": "True False"
},
{
"cond": "default",
"trial": 0,
"A": "doc15.md",
"B": "doc16.md",
"C": "doc17.md",
"server_before": "True False",
"pre_list": 60,
"pre_A": {
"opened": true,
"read": 4096,
"content_ok": true
},
"pre_B_ok": true,
"rename": {
"rc": 0,
"out": "renamed",
"err": "",
"secs": 0.018
},
"post_list": {
"n": 60
},
"post_stat_C": {
"ok": true,
"size": 1048576
},
"post_open_C": {
"opened": false,
"errno": "ENOENT",
"secs_after_rename": 0.002
},
"post_held_B": {
"read": 4096,
"content_ok": true
},
"post_reopen_A": {
"opened": false,
"errno": "ENOENT"
},
"late_list": {
"errno": "ENOENT",
"secs_after_rename": 25.003
},
"restore": "renamed",
"server_after": "True False"
},
{
"cond": "default",
"trial": 1,
"A": "doc18.md",
"B": "doc19.md",
"C": "doc20.md",
"server_before": "True False",
"pre_list": 60,
"pre_A": {
"opened": true,
"read": 4096,
"content_ok": true
},
"pre_B_ok": true,
"rename": {
"rc": 0,
"out": "renamed",
"err": "",
"secs": 0.017
},
"post_list": {
"n": 60
},
"post_stat_C": {
"ok": true,
"size": 1048576
},
"post_open_C": {
"opened": false,
"errno": "ENOENT",
"secs_after_rename": 0.004
},
"post_held_B": {
"read": 4096,
"content_ok": true
},
"post_reopen_A": {
"opened": false,
"errno": "ENOENT"
},
"late_list": {
"errno": "ENOENT",
"secs_after_rename": 25.001
},
"restore": "renamed",
"server_after": "True False"
},
{
"cond": "default",
"trial": 2,
"A": "doc21.md",
"B": "doc22.md",
"C": "doc23.md",
"server_before": "True False",
"pre_list": 60,
"pre_A": {
"opened": true,
"read": 4096,
"content_ok": true
},
"pre_B_ok": true,
"rename": {
"rc": 0,
"out": "renamed",
"err": "",
"secs": 0.014
},
"post_list": {
"n": 60
},
"post_stat_C": {
"ok": true,
"size": 1048576
},
"post_open_C": {
"opened": false,
"errno": "ENOENT",
"secs_after_rename": 0.002
},
"post_held_B": {
"read": 4096,
"content_ok": true
},
"post_reopen_A": {
"opened": false,
"errno": "ENOENT"
},
"late_list": {
"errno": "ENOENT",
"secs_after_rename": 25.003
},
"restore": "renamed",
"server_after": "True False"
},
{
"cond": "default",
"trial": 3,
"A": "doc24.md",
"B": "doc25.md",
"C": "doc26.md",
"server_before": "True False",
"pre_list": 60,
"pre_A": {
"opened": true,
"read": 4096,
"content_ok": true
},
"pre_B_ok": true,
"rename": {
"rc": 0,
"out": "renamed",
"err": "",
"secs": 0.019
},
"post_list": {
"n": 60
},
"post_stat_C": {
"ok": true,
"size": 1048576
},
"post_open_C": {
"opened": false,
"errno": "ENOENT",
"secs_after_rename": 0.002
},
"post_held_B": {
"read": 4096,
"content_ok": true
},
"post_reopen_A": {
"opened": false,
"errno": "ENOENT"
},
"late_list": {
"errno": "ENOENT",
"secs_after_rename": 25.003
},
"restore": "renamed",
"server_after": "True False"
},
{
"cond": "default",
"trial": 4,
"A": "doc27.md",
"B": "doc28.md",
"C": "doc29.md",
"server_before": "True False",
"pre_list": 60,
"pre_A": {
"opened": true,
"read": 4096,
"content_ok": true
},
"pre_B_ok": true,
"rename": {
"rc": 0,
"out": "renamed",
"err": "",
"secs": 0.018
},
"post_list": {
"n": 60
},
"post_stat_C": {
"ok": true,
"size": 1048576
},
"post_open_C": {
"opened": false,
"errno": "ENOENT",
"secs_after_rename": 0.006
},
"post_held_B": {
"read": 4096,
"content_ok": true
},
"post_reopen_A": {
"opened": false,
"errno": "ENOENT"
},
"late_list": {
"errno": "ENOENT",
"secs_after_rename": 25.002
},
"restore": "renamed",
"server_after": "True False"
},
{
"cond": "aggressive",
"trial": 0,
"A": "doc30.md",
"B": "doc31.md",
"C": "doc32.md",
"server_before": "True False",
"pre_list": 60,
"pre_A": {
"opened": true,
"read": 4096,
"content_ok": true
},
"pre_B_ok": true,
"rename": {
"rc": 0,
"out": "renamed",
"err": "",
"secs": 0.019
},
"post_list": {
"n": 60
},
"post_stat_C": {
"ok": true,
"size": 1048576
},
"post_open_C": {
"opened": false,
"errno": "ENOENT",
"secs_after_rename": 0.003
},
"post_held_B": {
"read": 4096,
"content_ok": true
},
"post_reopen_A": {
"opened": false,
"errno": "ENOENT"
},
"restore": "renamed",
"server_after": "True False"
},
{
"cond": "aggressive",
"trial": 1,
"A": "doc33.md",
"B": "doc34.md",
"C": "doc35.md",
"server_before": "True False",
"pre_list": 60,
"pre_A": {
"opened": true,
"read": 4096,
"content_ok": true
},
"pre_B_ok": true,
"rename": {
"rc": 0,
"out": "renamed",
"err": "",
"secs": 0.016
},
"post_list": {
"n": 60
},
"post_stat_C": {
"ok": true,
"size": 1048576
},
"post_open_C": {
"opened": false,
"errno": "ENOENT",
"secs_after_rename": 0.002
},
"post_held_B": {
"read": 4096,
"content_ok": true
},
"post_reopen_A": {
"opened": false,
"errno": "ENOENT"
},
"restore": "renamed",
"server_after": "True False"
},
{
"cond": "aggressive",
"trial": 2,
"A": "doc36.md",
"B": "doc37.md",
"C": "doc38.md",
"server_before": "True False",
"pre_list": 60,
"pre_A": {
"opened": true,
"read": 4096,
"content_ok": true
},
"pre_B_ok": true,
"rename": {
"rc": 0,
"out": "renamed",
"err": "",
"secs": 0.017
},
"post_list": {
"n": 60
},
"post_stat_C": {
"ok": true,
"size": 1048576
},
"post_open_C": {
"opened": false,
"errno": "ENOENT",
"secs_after_rename": 0.002
},
"post_held_B": {
"read": 4096,
"content_ok": true
},
"post_reopen_A": {
"opened": false,
"errno": "ENOENT"
},
"restore": "renamed",
"server_after": "True False"
},
{
"cond": "aggressive",
"trial": 3,
"A": "doc39.md",
"B": "doc40.md",
"C": "doc41.md",
"server_before": "True False",
"pre_list": 60,
"pre_A": {
"opened": true,
"read": 4096,
"content_ok": true
},
"pre_B_ok": true,
"rename": {
"rc": 0,
"out": "renamed",
"err": "",
"secs": 0.015
},
"post_list": {
"n": 60
},
"post_stat_C": {
"ok": true,
"size": 1048576
},
"post_open_C": {
"opened": false,
"errno": "ENOENT",
"secs_after_rename": 0.002
},
"post_held_B": {
"read": 4096,
"content_ok": true
},
"post_reopen_A": {
"opened": false,
"errno": "ENOENT"
},
"restore": "renamed",
"server_after": "True False"
},
{
"cond": "aggressive",
"trial": 4,
"A": "doc42.md",
"B": "doc43.md",
"C": "doc44.md",
"server_before": "True False",
"pre_list": 60,
"pre_A": {
"opened": true,
"read": 4096,
"content_ok": true
},
"pre_B_ok": true,
"rename": {
"rc": 0,
"out": "renamed",
"err": "",
"secs": 0.013
},
"post_list": {
"n": 60
},
"post_stat_C": {
"ok": true,
"size": 1048576
},
"post_open_C": {
"opened": false,
"errno": "ENOENT",
"secs_after_rename": 0.003
},
"post_held_B": {
"read": 4096,
"content_ok": true
},
"post_reopen_A": {
"opened": false,
"errno": "ENOENT"
},
"restore": "renamed",
"server_after": "True False"
},
{
"cond": "failed_rename",
"trial": 0,
"A": "doc45.md",
"B": "doc46.md",
"C": "doc47.md",
"server_before": "True False",
"pre_list": 60,
"pre_A": {
"opened": true,
"read": 4096,
"content_ok": true
},
"pre_B_ok": true,
"rename": {
"rc": 3,
"out": "refused ENOTEMPTY",
"err": "",
"secs": 0.018
},
"post_list": {
"n": 60
},
"post_stat_C": {
"ok": true,
"size": 1048576
},
"post_open_C": {
"opened": true,
"read": 4096,
"content_ok": true,
"secs_after_rename": 0.003
},
"post_held_B": {
"read": 4096,
"content_ok": true
},
"post_reopen_A": {
"opened": true,
"read": 4096,
"content_ok": true
},
"server_after": "True False"
},
{
"cond": "failed_rename",
"trial": 1,
"A": "doc48.md",
"B": "doc49.md",
"C": "doc50.md",
"server_before": "True False",
"pre_list": 60,
"pre_A": {
"opened": true,
"read": 4096,
"content_ok": true
},
"pre_B_ok": true,
"rename": {
"rc": 3,
"out": "refused ENOTEMPTY",
"err": "",
"secs": 0.016
},
"post_list": {
"n": 60
},
"post_stat_C": {
"ok": true,
"size": 1048576
},
"post_open_C": {
"opened": true,
"read": 4096,
"content_ok": true,
"secs_after_rename": 0.003
},
"post_held_B": {
"read": 4096,
"content_ok": true
},
"post_reopen_A": {
"opened": true,
"read": 4096,
"content_ok": true
},
"server_after": "True False"
},
{
"cond": "failed_rename",
"trial": 2,
"A": "doc51.md",
"B": "doc52.md",
"C": "doc53.md",
"server_before": "True False",
"pre_list": 60,
"pre_A": {
"opened": true,
"read": 4096,
"content_ok": true
},
"pre_B_ok": true,
"rename": {
"rc": 3,
"out": "refused ENOTEMPTY",
"err": "",
"secs": 0.018
},
"post_list": {
"n": 60
},
"post_stat_C": {
"ok": true,
"size": 1048576
},
"post_open_C": {
"opened": true,
"read": 4096,
"content_ok": true,
"secs_after_rename": 0.003
},
"post_held_B": {
"read": 4096,
"content_ok": true
},
"post_reopen_A": {
"opened": true,
"read": 4096,
"content_ok": true
},
"server_after": "True False"
},
{
"cond": "failed_rename",
"trial": 3,
"A": "doc54.md",
"B": "doc55.md",
"C": "doc56.md",
"server_before": "True False",
"pre_list": 60,
"pre_A": {
"opened": true,
"read": 4096,
"content_ok": true
},
"pre_B_ok": true,
"rename": {
"rc": 3,
"out": "refused ENOTEMPTY",
"err": "",
"secs": 0.017
},
"post_list": {
"n": 60
},
"post_stat_C": {
"ok": true,
"size": 1048576
},
"post_open_C": {
"opened": true,
"read": 4096,
"content_ok": true,
"secs_after_rename": 0.002
},
"post_held_B": {
"read": 4096,
"content_ok": true
},
"post_reopen_A": {
"opened": true,
"read": 4096,
"content_ok": true
},
"server_after": "True False"
},
{
"cond": "failed_rename",
"trial": 4,
"A": "doc57.md",
"B": "doc58.md",
"C": "doc59.md",
"server_before": "True False",
"pre_list": 60,
"pre_A": {
"opened": true,
"read": 4096,
"content_ok": true
},
"pre_B_ok": true,
"rename": {
"rc": 3,
"out": "refused ENOTEMPTY",
"err": "",
"secs": 0.018
},
"post_list": {
"n": 60
},
"post_stat_C": {
"ok": true,
"size": 1048576
},
"post_open_C": {
"opened": true,
"read": 4096,
"content_ok": true,
"secs_after_rename": 0.003
},
"post_held_B": {
"read": 4096,
"content_ok": true
},
"post_reopen_A": {
"opened": true,
"read": 4096,
"content_ok": true
},
"server_after": "True False"
}
]
}
+183
View File
@@ -0,0 +1,183 @@
#!/usr/bin/env python3
"""Pathname-withdrawal test for an albok browse view shared over sshfs.
Runs on the CLIENT (an ephemeral container, see run.sh) against the replica built
by setup-replica.py. The gate (albok unit 4.3, exposure ruling): rename `.browse/`
away on the server, then a FRESH open of a child that was never opened, through a
listing the client has cached, must FAIL.
Per trial (fresh mount each time), three distinct children:
A opened and closed before the rename -> re-opened after it
B opened before the rename, fd HELD -> read at a far offset after it
C listed and stat'ed, never opened -> opened after it (the gate)
Conditions (N trials each):
null no rename C must OPEN (null control)
default rename ok, sshfs defaults C must FAIL (the gate)
aggressive rename ok, hour-long caches C must FAIL (gate, worst-case client caching)
failed_rename rename refused (ENOTEMPTY) C OPENS: the path remains and is served
B is the positive control: a retained handle must still read after a rename, which
proves the instrument can see access that survives a rename.
Usage: run-trials.py <manifest.json> <trials-per-condition> (JSON to stdout)
"""
import errno
import json
import os
import shlex
import subprocess
import sys
import time
# Overridable so the same harness runs on a workstation (e.g. macOS) without the container.
SRV = os.environ.get("ALBOK_TEST_SRV", "infra-ops@10.100.50.44")
KEY = os.environ.get("ALBOK_TEST_KEY", "/keys/id")
KH = os.environ.get("ALBOK_TEST_KNOWN_HOSTS", "/keys/known_hosts")
MNT = os.environ.get("ALBOK_TEST_MNT", "/mnt/browse")
SSH_OPTS = ["-i", KEY, "-o", "BatchMode=yes", "-o", f"UserKnownHostsFile={KH}",
"-o", "StrictHostKeyChecking=yes", "-o", "ControlMaster=auto",
"-o", "ControlPath=/tmp/cm-%r@%h", "-o", "ControlPersist=900"]
SSHFS_BASE = ["ro", "follow_symlinks", f"IdentityFile={KEY}", f"UserKnownHostsFile={KH}",
"StrictHostKeyChecking=yes", "BatchMode=yes"]
AGGRESSIVE = ["kernel_cache", "entry_timeout=3600", "attr_timeout=3600",
"dir_cache=yes", "dcache_timeout=3600"]
CONDITIONS = [("null", [], None), ("default", [], "ok"),
("aggressive", AGGRESSIVE, "ok"), ("failed_rename", [], "fail")]
LATE_LIST_DELAY = 25 # past sshfs's default 20 s cache timeout
manifest = json.load(open(sys.argv[1]))
N = int(sys.argv[2])
ROOT = manifest["root"]
SIZE = manifest["size"]
OBJS = manifest["objects"]
VIEW, WITHHELD = f"{ROOT}/.browse", f"{ROOT}/.browse.withheld"
def ename(e: OSError) -> str:
return errno.errorcode.get(e.errno or 0, str(e.errno))
def content(o) -> bytes:
line = f"replica object {o['i']} {o['id']}\n".encode()
return (line * (SIZE // len(line) + 1))[:SIZE]
def remote_py(code: str, *args: str):
cmd = "python3 -c " + shlex.quote(code) + "".join(" " + shlex.quote(a) for a in args)
t0 = time.monotonic()
r = subprocess.run(["ssh", *SSH_OPTS, SRV, cmd], capture_output=True, text=True, timeout=60)
return {"rc": r.returncode, "out": r.stdout.strip(), "err": r.stderr.strip()[-300:],
"secs": round(time.monotonic() - t0, 3)}
RENAME = ("import os,sys,errno\n"
"try:\n os.rename(sys.argv[1], sys.argv[2]); print('renamed')\n"
"except OSError as e:\n print('refused', errno.errorcode.get(e.errno)); sys.exit(3)")
STATE = "import os,sys; print(os.path.isdir(sys.argv[1]), os.path.lexists(sys.argv[2]))"
def open_read(path, expect, offset=0, n=4096):
try:
fd = os.open(path, os.O_RDONLY)
except OSError as e:
return {"opened": False, "errno": ename(e)}
try:
os.lseek(fd, offset, os.SEEK_SET)
b = os.read(fd, n)
return {"opened": True, "read": len(b), "content_ok": b == expect[offset:offset + n]}
except OSError as e:
return {"opened": True, "read_errno": ename(e)}
finally:
os.close(fd)
def listing(d):
try:
return {"n": len(os.listdir(d))}
except OSError as e:
return {"errno": ename(e)}
def stat(p):
try:
st = os.stat(p)
return {"ok": True, "size": st.st_size}
except OSError as e:
return {"ok": False, "errno": ename(e)}
def trial(cond, extra, rename, k, slot):
A, B, C = OBJS[3 * slot], OBJS[3 * slot + 1], OBJS[3 * slot + 2]
rec = {"cond": cond, "trial": k, "A": A["name"], "B": B["name"], "C": C["name"]}
rec["server_before"] = remote_py(STATE, VIEW, WITHHELD)["out"]
if rec["server_before"] != "True False":
rec["aborted"] = "server not in the clean state"
return rec
if rename == "fail": # a non-empty target makes rename(2) refuse
remote_py("import os,sys; os.makedirs(sys.argv[1])", f"{WITHHELD}/blocker")
os.makedirs(MNT, exist_ok=True)
m = subprocess.run(["sshfs", f"{SRV}:{VIEW}", MNT, "-o", ",".join(SSHFS_BASE + extra)],
capture_output=True, text=True, timeout=60)
if m.returncode != 0:
rec["aborted"] = f"mount failed: {m.stderr.strip()}"
return rec
renamed = False
fd_b = None
try:
st_dir = f"{MNT}/stash"
names = os.listdir(st_dir)
rec["pre_list"] = len(names)
for nm in names: # cache every child's attrs: the client "knows" C exists
os.stat(f"{st_dir}/{nm}")
rec["pre_A"] = open_read(f"{st_dir}/{A['name']}", content(A))
fd_b = os.open(f"{st_dir}/{B['name']}", os.O_RDONLY)
rec["pre_B_ok"] = os.read(fd_b, 4096) == content(B)[:4096]
if rename:
rec["rename"] = remote_py(RENAME, VIEW, WITHHELD)
renamed = rec["rename"]["out"] == "renamed"
t0 = time.monotonic()
rec["post_list"] = listing(st_dir)
rec["post_stat_C"] = stat(f"{st_dir}/{C['name']}")
rec["post_open_C"] = open_read(f"{st_dir}/{C['name']}", content(C))
rec["post_open_C"]["secs_after_rename"] = round(time.monotonic() - t0, 3)
far = SIZE - 8192 # well past readahead: this read must go to the server
try:
os.lseek(fd_b, far, os.SEEK_SET)
b = os.read(fd_b, 4096)
rec["post_held_B"] = {"read": len(b), "content_ok": b == content(B)[far:far + 4096]}
except OSError as e:
rec["post_held_B"] = {"read_errno": ename(e)}
rec["post_reopen_A"] = open_read(f"{st_dir}/{A['name']}", content(A))
if cond == "default":
time.sleep(max(0.0, LATE_LIST_DELAY - (time.monotonic() - t0)))
rec["late_list"] = listing(st_dir)
rec["late_list"]["secs_after_rename"] = round(time.monotonic() - t0, 3)
finally:
if fd_b is not None:
os.close(fd_b)
for um in (["fusermount3", "-u", MNT], ["fusermount", "-u", MNT], ["umount", MNT]):
try:
if subprocess.run(um, capture_output=True).returncode == 0:
break
except FileNotFoundError:
continue
if renamed:
rec["restore"] = remote_py(RENAME, WITHHELD, VIEW)["out"]
if rename == "fail":
remote_py("import os,sys; os.rmdir(sys.argv[1]); os.rmdir(sys.argv[2])",
f"{WITHHELD}/blocker", WITHHELD)
rec["server_after"] = remote_py(STATE, VIEW, WITHHELD)["out"]
return rec
results = []
slot = 0
for cond, extra, rename in CONDITIONS:
for k in range(N):
results.append(trial(cond, extra, rename, k, slot))
slot += 1
json.dump({"started_note": "times are monotonic seconds; see run.sh for wall clock",
"sshfs_base": SSHFS_BASE, "aggressive": AGGRESSIVE, "trials": results},
sys.stdout, indent=1)
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
# Pathname-withdrawal test for the albok browse view over sshfs (unit 4.3 exposure gate).
#
# 1. ssh infra-ops@10.100.50.44 python3 - < setup-replica.py > manifest.json
# 2. ./run.sh [trials-per-condition] # default 5; JSON lands in results-<stamp>.json
# 3. remove the replica on nh3-albok by its literal path when done
#
# The client is an ephemeral Debian 12 container on nh3-dev (sshfs 3.7.3 / libfuse 3), so
# nothing is installed on the host. It needs /dev/fuse and CAP_SYS_ADMIN to mount.
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
n=${1:-5}
stamp=$(date +%Y%m%d-%H%M)
out="$here/results-$stamp.json"
echo "start $(date '+%F %H%M %Z')" >&2
docker run --rm --device /dev/fuse --cap-add SYS_ADMIN --security-opt apparmor=unconfined \
-v "$HOME/.ssh/infra-ops_ed25519:/keys/id:ro" \
-v "$HOME/.ssh/known_hosts:/keys/known_hosts:ro" \
-v "$here:/work" \
debian:bookworm-slim bash -c "
apt-get -qq update >/dev/null && apt-get -qq install -y sshfs python3 openssh-client >/dev/null 2>&1
sshfs --version >&2
python3 /work/run-trials.py /work/manifest.json $n" > "$out"
echo "end $(date '+%F %H%M %Z') -> $out" >&2
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
"""Build a replica of an albok browse view on the share host (nh3-albok).
Run ON the server as infra-ops, no sudo:
ssh infra-ops@10.100.50.44 python3 - < setup-replica.py > manifest.json
Why a replica and not the live view: the test renames `.browse/` away. On a live
wing that rename races albok-service's own link()/unlink() calls, and a failed
mandatory unlink withholds the wing (BROWSE_WITHHELD marker + a failed write
transaction). The replica has the same shape as the real view
(`<wing>/.browse/<locator>` -> relative symlink `../../<id[:2]>/<id>/<blob>`), sits
on the same filesystem (root ext4), and is served by the same sftp-server, which
is what the withdrawal property depends on.
Content is deterministic per object, so the client can verify bytes without
fetching a reference copy.
"""
import hashlib
import json
import os
import uuid
ROOT = "/home/infra-ops/albok-browse-test/wing"
COUNT = 60
SIZE = 1 << 20 # 1 MiB: larger than FUSE readahead, so a late read must reach the server
def content(i: int, oid: str) -> bytes:
line = f"replica object {i} {oid}\n".encode()
return (line * (SIZE // len(line) + 1))[:SIZE]
os.makedirs(ROOT, exist_ok=False) # refuse to run over an existing replica
link_dir = os.path.join(ROOT, ".browse", "stash")
os.makedirs(link_dir)
objs = []
for i in range(COUNT):
oid = uuid.uuid4().hex
obj_dir = os.path.join(ROOT, oid[:2], oid)
os.makedirs(obj_dir)
name = f"doc{i:02d}.md"
blob = os.path.join(obj_dir, name)
data = content(i, oid)
with open(blob, "wb") as f:
f.write(data)
os.chmod(blob, 0o640)
os.symlink(os.path.relpath(blob, link_dir), os.path.join(link_dir, name))
objs.append({"i": i, "id": oid, "name": name, "size": SIZE,
"sha256": hashlib.sha256(data).hexdigest()})
print(json.dumps({"root": ROOT, "size": SIZE, "objects": objs}))
+7 -2
View File
@@ -22,8 +22,13 @@ The FastAPI process that owns Albok's buildings-and-wings store (`vh/albok`,
`/health` gains `browse_withheld`; 0 is normal, and a non-zero value is a WARN. No config change.
⚠ **`.browse/` is NOT shared with anyone yet.** The contract gates exposure on a demonstration on the chosen share:
rename `.browse/` away on the server, then try a fresh open of an unopened child from the client through a cached
listing. That open must fail. sshfs `follow_symlinks` works by path; NFS/SMB may keep directory handles. Report a
passing surface to albok-dev, who tells the operator. Rollback: `compose.yaml.pre-0.1.14` (0.1.13) is free, because no
listing. That open must fail. sshfs `follow_symlinks` works by path; NFS/SMB may keep directory handles.
**[2026-10-09] PASSED on sshfs `ro,follow_symlinks` with a Linux client** (SSHFS 3.7.3 / libfuse 3.14): fresh open
ENOENT 5/5 with sshfs defaults and 5/5 with hour-long client caches, while the cached listing and stat still showed
the child; null and positive controls behaved (`scripts/oneoff/albok-browse-withdrawal/`). Reported to albok-dev.
Still before any share: the workstation's own client must re-run that harness if it is macOS (different FUSE
backend), and no user is in `albok-read`/`albok-personal` yet, so the operator's user needs those groups (the
operator's call; `albok-feedback` stays default-deny). Rollback: `compose.yaml.pre-0.1.14` (0.1.13) is free, because no
sidecar shape changed.
Prior: 0.1.13 @ `sha256:bea95617…` (2026-10-07 1135 – 1804) (healthy 1136:17,
between walks). infra-ops deployed it at albok-dev's ask (althing 8413), from a clean archive of vh/albok `70b55ad7`