Skip to content

fix(ptc): expose the session result cache inside a bwrap private /tmp - #338

Merged
T-Auto merged 4 commits into
mainfrom
work/337-bwrap-result-cache-ccch1mneyyy
Oct 21, 2026
Merged

T-Auto merged 4 commits into
mainfrom
work/337-bwrap-result-cache-ccch1mneyyy

Conversation

@ccch1mneyyy

@ccch1mneyyy ccch1mneyyy commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #337

What was wrong

On Linux hosts where @deepseek-ai/dsh-sandbox-local selects bwrap (any distribution that does not restrict unprivileged user namespaces — Arch, Fedora, Debian…), every ref:// result reference was unusable. The first call returned a reference, the next ops_result page returned REFERENCE_EXPIRED: Reference cache is missing, expired, or destroyed.

  • posixCacheLocation() keeps the session cache at <tmpdir()>/dsh-ops-results-<key> (0700, owner-checked).
  • bwrap's workspace-write profile mounts --tmpfs /tmp for the child, so the confined PTC worker behind ops_result starts in an empty private /tmp and never sees that directory. (The Native file worker is not confined, which is why the reference could be created at all.)
  • The session purge goes through the same confinement, finds no cache, reports completed, and leaves the blobs in the real /tmp.

CI stays green because the Ubuntu runners restrict user namespaces, the bwrap probe fails, and the host falls back to Landlock, which shares the real /tmp. On a bwrap host main fails 9 cases in docx-host, pdf-host, table-host, native-result, ptc-sdk and result-storage-host; replacing bwrap on PATH with a stub that exits 1 makes the same tree pass.

The change

  • src/kernel/result-storage.ts: exposePosixCache(argv, storage) adds --bind <cache> <cache> immediately before the runner's -- separator, only when the runner is bwrap, its own options (not the wrapped program's) contain --tmpfs, and the storage is a POSIX location. The purge path applies it to its confined argv.
  • src/ptc/worker.ts: the confined worker argv goes through the same function once storage is known.

Only the one session directory is exposed, which is narrower than the Landlock profile's writable /tmp. Landlock, Seatbelt, Windows (--temp grant) and read-only bwrap (no tmpfs) keep their argv byte-for-byte. Deliberately not done: moving the cache into the workspace (#257 asks for nothing to be written there) or running the PTC worker unconfined.

Validation

New checks in test/result-storage-host.test.mjs:

  • only a bwrap private /tmp gains a bind for the one session cache, before the wrapped program — argv transform, including the unchanged cases (Landlock, read-only bwrap, --tmpfs after the separator, Windows storage, relative root, no storage).
  • a bwrap-style private /tmp still sees the session cache for put, page and session purge — a runner shim named bwrap records its argv and execs the worker; asserts put, page and the session purge all carry the bind. This runs on CI without a usable bwrap. With src/ reverted it fails (and so does the existing real host references … check on a bwrap host):
$ git checkout origin/main -- src && node scripts/build.mjs && DSH_OPS_REQUIRE_RUNTIME=1 node test/run.mjs result-storage-host
  ok   cache flags accept only the trusted runner section and private-temp capability family
  FAIL real host references bind live session, current authority and producer visibility; disposal purges only cache blobs
  FAIL a bwrap-style private /tmp still sees the session cache for put, page and session purge
  ok   read-only first use cannot create result storage or invoke a cache-preparation provider

With the change, real bwrap (Arch, bwrap 0.13.0, Rust 1.97.1 worker built from this tree):

$ DSH_OPS_REQUIRE_RUNTIME=1 node test/run.mjs result-storage-host
  ok   cache flags accept only the trusted runner section and private-temp capability family
  ok   real host references bind live session, current authority and producer visibility; disposal purges only cache blobs
  ok   only a bwrap private /tmp gains a bind for the one session cache, before the wrapped program
  ok   a bwrap-style private /tmp still sees the session cache for put, page and session purge
  ok   read-only first use cannot create result storage or invoke a cache-preparation provider
dsh-ops result-storage-host: all 5 checks passed

$ for s in docx-host native-result pdf-host ptc-sdk result-storage-host table-host; do DSH_OPS_REQUIRE_RUNTIME=1 node test/run.mjs $s | tail -1; done
dsh-ops test: 1 suite(s) passed in 1s
dsh-ops test: 1 suite(s) passed in 3s
dsh-ops test: 1 suite(s) passed in 1s
dsh-ops test: 2 suite(s) passed in 2s
dsh-ops test: 1 suite(s) passed in 0s
dsh-ops test: 1 suite(s) passed in 1s

$ DSH_OPS_REQUIRE_RUNTIME=1 node test/run.mjs          # real bwrap
dsh-ops test: 2 of 126 suite(s) failed in 58s
--- tui-host.test.mjs skipped while DSH_OPS_REQUIRE_RUNTIME=1     # needs DSH_OPS_TUI_ROOT; same on main
--- tui-native.test.mjs skipped while DSH_OPS_REQUIRE_RUNTIME=1   # needs DSH_OPS_TUI_ROOT; same on main
(main on the same host: 9 of 126 suite(s) failed)

$ PATH=/tmp/nobwrap:$PATH node test/run.mjs            # bwrap probe fails → Landlock, like CI
dsh-ops test: 124 suite(s) passed, 2 skipped in 58s

$ node scripts/check-file-size.mjs
check-file-size: 383 code file(s) checked, all at or under 1000 lines (longest: bench/setup/profile.mjs (924))
  • CHANGELOG ## [Unreleased] entry (EN + ZH)
  • No new dependencies, no package.json change
  • No model-facing description or schema change

CI (pull_request run): see checks below; URL added once it completes.

Environment

Arch Linux x64, kernel 6.18 LTS, bwrap 0.13.0, Node 24.21.0, Rust 1.97.1, upstream main @ a9ff940, built from source (npm ci --ignore-scripts, npm run build:ptc).

PR-triggered CI: https://github.com/T-Auto/dsh-ops/actions/runs/39470903973 — pull_request, aggregate CI required: SUCCESS (Windows x64, Linux x64, macOS arm64 and required supporting jobs passed).

Combined local validation

The submitted heads of #338, #340, #341, #342, #343, #344 and #345 were applied together in an isolated local verification worktree, including their combined changelog entries. With ordinary PATH selecting real bwrap 0.13.0 and real dsh-TUI 0.16.1 host/worker/renderer suites enabled, node scripts/verify-gate.mjs --skip-install passed all 11 release-gate steps: 129 suites passed, 0 skipped; 46 Rust unit tests passed; manifest 73/59. This Linux combined-tree evidence is separate from each PR's own cross-platform CI; no upstream merge or host installation was performed.

On Linux hosts where the sandbox selects bwrap, workspace-write children
get a private tmpfs over /tmp. The result cache lives below the host's
tmpdir(), so the confined PTC worker behind ops_result never saw it:
every ref:// page failed with REFERENCE_EXPIRED and the session purge
completed against an empty tmpfs, leaving the cache behind.

Bind exactly that one private cache directory back into the bwrap argv
for page/put and for the session purge. Other runners, read-only bwrap
and Windows keep their argv unchanged.

Refs #337
bwrap accepts the private temp mount as either "--tmpfs /tmp" or
"--tmpfs=/tmp". Recognize both spellings before adding the session cache bind,
and keep every other runner argv byte-identical.
Assert the single-token bwrap form gains the same cache bind, and state what the
Linux runner-shim check actually verifies: the cache bind lands before the
wrapped program, not that a real private tmpfs exposes the cache.
@T-Auto
T-Auto merged commit 3a54316 into main Oct 21, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants