Skip to content

feat(deploy): L5 journey suite, fleet substrate, CI gates (owner-gated merge) - #346

Draft
Yambr wants to merge 119 commits into
next/v1from
docs/demo-walkthrough
Draft

feat(deploy): L5 journey suite, fleet substrate, CI gates (owner-gated merge)#346
Yambr wants to merge 119 commits into
next/v1from
docs/demo-walkthrough

Conversation

@Yambr

@Yambr Yambr commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Forked from next/v1 at a6b48bd. No changes under docs/architecture/ or contracts/ — canon untouched (the three-dot PR diff under those paths is empty; a git merge-tree against next/v1 HEAD keeps ADR-0028/0029 and both openapi contracts intact). Merge simulates clean against current next/v1 HEAD.

What lands

The L5 user-journey e2e suite + the fleet deploy substrate + tier-1 CI gates. The three MCP tool-surface output-losing defects the suite found are already merged to their component mains and live-verified — this PR carries only the deploy/test tooling that proves them end-to-end:

  • deploy/tests/journeys/ — PoC-vs-fleet journey groups A-I; groups H (gateway auth-edge) + I (MCP tool-surface) close the sk-ocu keyed-create + exit/truncation/timeout/EACCES/persistence coverage below the exec contract.
  • deploy/fleet/ — the assembled 8-component stand: guest image, embed-portal IdP stand-in, gateway PKI, the vendored boot-set minter + its key-set schema, .env.example, README run-book.
  • .github/workflows/journeys-collect.yml — tier-1 gate: import/collection health + a mint-smoke step (red-probed on schema delete/corrupt/drift).

Proof (firsthand, live Lima stand)

From a truly clean in-Lima clone that renders its OWN boot-set via the vendored minter (no symlinked secrets): H (5) + I (8) = 13 passed, 0 skipped. Full A-I: 23 passed, 92 loud-skip (A-G loud-skip when the backend is absent — never mocked green).

Verify it yourself (zero trust in the author)

In Lima ocu-linux, follow deploy/fleet/README.md -> "Running the journey suite": clean clone, mint the boot-set, .venv/bin/pytest deploy/tests/journeys, compare against the pinned expected counts (13/0 with a python3 guest; 10 passed + 3 skipped on the stripped default).

Merged component fixes (already on their mains)

Tracked non-blocking follow-ups

#344 canon exec-reply schema (owner-gated next/v1), #345 demo default python3 guest, tier-2 live-CI gate, JOURNEYS_REQUIRE_FLEET fail-loud knob, deploy/fleet/contracts lint-path gap.

Draft: merge is owner-gated.

🤖 Generated with Claude Code

widemoat-ai and others added 25 commits June 28, 2026 16:00
A runnable walkthrough of each architectural security and data-flow guarantee,
one step at a time, with the command and the expected observable. Aspects 2, 3,
and 4 carry a firsthand run: a green baseline and a neutered counter-case that
reds (the IaC gate's own --self-test, a red-probe for the cross-tenant and F9
guards), against the merged/shipped code. Front-matter and curl examples use
env-var placeholders so no secret-shaped literal enters the history.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
45 end-user-journey scenarios (7 groups: auth/bootstrap, docx create+download,
upload/edit/download, authz boundary, auto-disconnect/lifecycle, agentic load,
adversarial). Each scenario is paired: it runs against the PoC (Open WebUI +
computer-use-server) and the fleet (gateway mTLS -> control -> gVisor guest ->
FUSE -> egress edge -> filestore -> MinIO), and asserts the per-backend end
state plus a keystone that stays reproducibly reddable.

A pytest spine drives real Backend verbs; scenarios.yaml is the single source
of truth and renders CONTRAST.md. The fleet leg runs live only under Lima +
runsc and loud-skips elsewhere (never a mocked green); the exec_sh chokepoint
busybox-prefixes every in-guest command, guarded by a meta-test that greps the
suite for bare sh/python3 argv. Two watchable bash demos narrate the two
journeys.

Run firsthand against the live fleet, the suite exposed three real defects,
recorded in FINDINGS.md as strict issue-linked xfails (keystones not weakened):
the storage-write plane does not round-trip (mount Put omits the required
declared_size_bytes; the stand-in read plane is 501), the concurrency counter
leaks under the kill-switch and wedges the deployment at the tier cap, and the
mount-facing network is internal:false so a guest reaches the public internet.

Two deploy fixes land alongside: control now depends_on harness-init (it writes
the CA the control plane latches at boot, so a clean down -v + up self-heals)
and allow-lists the busybox-carrying demo guest image.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZogNpHHSvgo41RsoBmsxH
The fleet compose healthcheck called /ocu-mcp-gatewayd but the gateway
image entrypoint is /usr/local/bin/ocu-mcp-gatewayd, so the check failed
its stat and the container never went healthy though the process served
MCP on :8080. Point the healthcheck at the entrypoint path (canonical and
permanent, mirrors control).

G3 and E8 issued a guest exec immediately after create, before the guest
boot-child brings up the FUSE mount and exec plane, so the op was denied
(exit -1) and the test read a boot race as a failure. Wait for the exec
plane with await_fleet_exec_ready before the audited/keystone exec, the
same gate the B group already uses; both now record their remaining gaps
as honest xfails rather than a boot-race red.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZogNpHHSvgo41RsoBmsxH
Add the OpenWebUI client leg to the fleet compose, repointed at OUR MCP
gateway instead of the old monolithic Computer Use Server. The old tool
already speaks MCP tools/call; here it targets mcp-gateway:8080 with an
sk-ocu- boot-set key and MCP-Protocol-Version. A tool-call flows
OpenWebUI -> gateway (auth + validate) -> control (create) -> gVisor
guest, and executes behind the gateway; the control plane owns the
session lifecycle (the tool never destroys).

- open-webui + its OWN openwebui-db (never control-db) on ocu-frontend,
  host port 3001 (3000 is the fleet webui BFF).
- mcp-gateway now runs the G4+G6+G7 combined image with the G7 durable
  audit sink (-audit-sink) so a valid keyed create reaches 201 instead
  of the fail-closed 500, plus the G6 readiness healthcheck.
- mcp-gateway-audit-init chowns the one writable audit-journal mount to
  the gateway uid, mirroring webui-audit-init.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZogNpHHSvgo41RsoBmsxH
The exec-forward runs a bash_tool command as an argv in the guest. The demo
guest shipped only /bin/busybox with no /bin/sh, so a tool command spawned via a
shell path failed ENOENT and the exec fail-closed with a 409. An image that
supports bash_tool must guarantee a POSIX /bin/sh so the gateway need not know
the image internals; symlink /bin/sh onto the static busybox to provide it.

Pairs with the gateway sending /bin/sh -c (not bash -lc: -l is non-POSIX and a
login shell is not needed for a tool-call). Follow-up: the prod assembled image
must also ship a /bin/sh or it hits the same ENOENT.
The File Pane (component-08) is an embeddable SPA that never self-issues its
bootstrap credential: it trusts exactly one parent origin
(NEXT_PUBLIC_OCU_PARENT_ORIGIN, strict equality) and waits for the parent to
postMessage a peer-minted embed token. The fleet had no such parent, and the
build-arg was unset, so the pane could never bootstrap in a browser — it hung
on "Loading files…" forever.

embed-portal is that parent: a tiny Go service that iframes the pane, mints a
short-lived HS256 embed token server-side (aud ocu-webui, exp 60s under the
120s ceiling, sub/filesystem_id/intent claims the BFF requires), and
postMessages it into the iframe at the pane's literal origin. It holds the
embed-verify secret in a separate origin/process, preserving the invariant
that the webui origin never mints its own bootstrap credential — the same role
a customer portal/IdP fills in production.

Compose wiring: portal published on :3003; webui gets
NEXT_PUBLIC_OCU_PARENT_ORIGIN as a build-arg (NEXT_PUBLIC_* inlines at build,
not runtime) and the portal origin in its frame-ancestors allowlist. Open the
demo at http://localhost:3003 (localhost, not 127.0.0.1 — strict origin
equality plus the SameSite=None;Secure session cookie requires a localhost
http origin).

Proven live in a real browser: bootstrap -> list -> upload round-trips
end to end; the download 403 on an uploaded file is the NFR-SEC-73 control
(downloadable is a read-time egress tag, default false), not a defect.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZogNpHHSvgo41RsoBmsxH
…gateway requires

The tool's manual `/mcp` initialize preflight sent protocolVersion
"2024-11-05" in the JSON-RPC body and omitted the MCP-Protocol-Version
HTTP header. The next/v1 MCP gateway negotiates the version through that
header and rejects a request that carries the wrong version, or none,
with -32602 "unsupported or missing protocol version" (HTTP 400) before
auth. The preflight treats anything other than 401/403 as a broken
server, so every tool call surfaced [CONFIG ERROR] and never reached the
real MCP SDK call (which sets the header itself).

Pin the version as `_MCP_PROTOCOL_VERSION = "2025-06-18"` and send it in
both the header and the initialize body. Verified live against the fleet
stand: the browser bash tool now runs into the guest and returns output
(no [CONFIG ERROR]).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CLSUfSnuBtUftkxB7RvA72
…etworks

The PoC guest image installs ~24 npm globals with binary postinstalls
(phantomjs via markdown-pdf, sharp, playwright). Under an emulated (qemu
amd64) or otherwise slow network, npm's default 300s idle timeout aborts
mid-fetch (EIDLETIMEOUT), failing the build. Seed the assistant user's
npmrc once after useradd so every later global install retries with a
600s ceiling. No effect when the network is fast.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CLSUfSnuBtUftkxB7RvA72
Running `glab config set check_update false --global` at build time crashes
under qemu-user emulation: glab is a pure-Go binary, and Go's lock-free
stack hits its 48-bit pointer-packing assertion (lfstack.push) when
qemu-user returns higher addresses. Building the x86_64 image with
--platform linux/amd64 on an arm64 host runs that step emulated.

Replace the execution with a direct write of the config.yml/aliases.yml
artifacts glab would have produced (check_update:false is the only
non-default line the step set). Shipping the artifact instead of running
the producer is byte-identical on the emulated and native paths — no
image drift, and no guard that could mask a real native failure.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CLSUfSnuBtUftkxB7RvA72
The image was x86_64-hardcoded (node linux-x64, ttyd.x86_64, glab_amd64,
JAVA_HOME .../-amd64, a vendored x86_64 extract-text ELF), so it built
only under --platform linux/amd64 — and on an arm64 host that means qemu
emulation, where Go binaries crash at build time and every download is
slower. Parameterize each arch-specific site on the buildx-provided
TARGETARCH so one Dockerfile builds both amd64 (x86_64-faithful) and arm64
(native on aarch64 hosts, incl. gVisor):

- node: linux-${x64|arm64}
- ttyd: ttyd.${x86_64|aarch64}
- glab: glab_..._linux_${amd64|arm64}
- JAVA_HOME: a stable symlink to java-21-openjdk-${TARGETARCH}
- extract-text: the vendored binary is x86_64-only; install it on amd64,
  omit it on arm64 with an explicit build note (never ship a broken ELF
  that pretends to be present). Ship an arm64 build to close that gap.

bun and codex resolve their own arch at install; playwright installs
per-arch — no change. Verified firsthand under linux/arm64 buildx: node
v22.11.0, glab and ttyd extract as ARM aarch64 ELFs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CLSUfSnuBtUftkxB7RvA72
…grants

Seed a workspace-model record per surviving catalog model with
base_model_id=null so meta.toolIds surfaces into the resolved /api/models
(Open WebUI's get_all_models applies a null-base record as a direct
override; a self-referential base_model_id lands on the skip path and the
tool never reaches the model). The catalog is trimmed to Qwen + DeepSeek
flash, native function-calling is set, and the fresh-chat default is
deepseek-flash — so any chat a user opens has the Computer Use tool live
without a manual toggle.

Also seed public read access_grants on each model record: a seeded record
with empty grants is dropped by get_filtered_models for non-admin users,
who would otherwise see zero models.

Verified firsthand in the real browser on the default deepseek-flash chat:
bash_tool runs and returns real guest output (echo 7*7 -> 49, pwd -> /),
and create_file surfaces the gateway's clean "unimplemented tool" error in
the UI (no hang) then falls back to bash.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CLSUfSnuBtUftkxB7RvA72
…nly)

markdown-pdf pulls phantomjs-prebuilt, which ships x86_64 binaries only
(abandoned, predates arm64 Linux); its postinstall hard-fails on a native
arm64 build. Filter it out of the global install when TARGETARCH=arm64, so
the arm64 image builds clean. The markdown->PDF skill helper is then
unavailable on arm64 — a documented host-ISA gap, symmetric to the
x86_64-only extract-text helper; amd64 keeps it. Verified the filter:
amd64 installs 21 globals (markdown-pdf included), arm64 installs 20.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CLSUfSnuBtUftkxB7RvA72
Cover the MCP tool surface (bash_tool, str_replace, create_file, view)
end-to-end against the live fleet, derived from the PoC behavior in
computer-use-server/mcp_tools.py. mcp_tool_surface.feature is the Gherkin
source of truth; @L4 scenarios are proven in the gateway forward e2e, @l5
here as live journeys through the real gateway on 127.0.0.1:8080.

Group I proves what only a live guest can settle:
  I1  a real non-zero exit transports to isError, empty-output non-zero
      carries the synthesized "[Exit code: N]" marker (verified live)
  I2b a moderate output returns whole; I2 (oversize) is a bounded result
  I3  a command past the exec-timeout is KILLED (enforcement holds)
  I4  create_file EACCES is a guest-identity contrast + writable keystone
  I5  one chat maps to one persistent workspace; a different chat is
      isolated (per-session, not a shared global fs)
  I6  the four tools compose over one workspace (create->view->str_replace
      ->bash = ALPHA EDITED), gated on a python3-bearing guest

I2 and I3b are xfail(strict) pending DEFECT #127: oversized output and a
timed-out command surface as a 502 forward-refusal that loses the whole
result, where the PoC returns a bounded/timeout-noted tool result. The
strict marker XPASSes -> reds the suite the moment the fix lands, forcing
the marker's removal in the verifying PR. Root cause: gateway
maxReplyBytes=64KiB read-cap vs control 8MiB stream-cap — a cross-component
sizing-invariant the two repos never cross-checked.

Firsthand on the live Lima stand: 6 passed, 2 xfailed. I5 persistence is
red-probed non-vacuous (break the session -> the journal reds).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CLSUfSnuBtUftkxB7RvA72
…t to #129

#127 step 1 (gateway raise maxReplyBytes) kills the 502 for outputs up to
the read-cap but does not deliver the 64KiB caller ceiling (that is step 2,
control-side, #128). Per the multi-step-xfail rule, split so each xfail
tracks exactly one un-landed step and none sits red-hidden across PRs:

  I2  -> plain green: the step-1 "nothing lost" contract (a 120k output that
         used to 502 now returns HTTP 200 with the output intact, len>=120000).
         Strict whole-return is a designed paired-flip with I2c: when step 2
         lands, I2 reds as I2c strict-xpasses, forcing the step-2 PR to merge
         them into one bounded-contract test.
  I2c -> new strict-xfail (#128): the 64KiB caller ceiling + truncation marker.
  i2b -> unchanged 30k whole-return keystone; stale base64 NOTE dropped.
  I3b -> re-pointed from "#127 same class" to #129: the timeout 502 is a
         SHAPING defect, not the size class. Firsthand on the live stand:
         control returns HTTP 409 on a timed-out exec -> gateway 502; a
         "echo MARKER; sleep 600" loses the partial output (the PoC preserves
         it + a timeout notice). sleep 600 emits zero stdout, so #127's
         maxReplyBytes raise could not have touched this path.

Firsthand on the live Lima stand (gateway rebuilt from PR #43): 7 passed,
2 xfailed. The 502 for oversized output is dead; the timeout 502 (#129) and
the 64KiB ceiling (#128) remain, each tracked by exactly one strict-xfail.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CLSUfSnuBtUftkxB7RvA72
#129 fixed)

Control PR #63 shapes a host exec-timeout into a valid exit-124 reply with
the pre-kill partial output + a "[Command timed out after Ns]" notice in the
stream the gateway relays on isError — so the killed command is a usable
Tier-2 tool result, not a 502 that loses everything.

I3b was strict-xfail(#129); the fix made it XPASS (strict -> reds the suite),
so per the multi-step-xfail rule it flips to a plain green assert: a timed-out
command returns HTTP 200 + isError, carrying its PARTIAL output AND the notice.
Firsthand-verified on the live Lima stand (control rebuilt from PR #63):
"echo MARKER; sleep 600" -> HTTP 200, isError:true,
content "MARKER\n\n[Command timed out after 30s]\n" (was 502, MARKER lost).

Group I on the live stand: 8 passed, 1 xfailed (I2c still tracks #128). I3
still proves the KILL; I3b now proves the RESULT SHAPE.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CLSUfSnuBtUftkxB7RvA72
…nded)

Control PR #64 bounds each F5 exec-reply stream at 64KiB at the source
(stdioCap 8MiB->64<<10) + a truncation marker, so an oversized output is a
bounded tool result — not a 502 (#127) and not relayed whole (#128).

The designed paired flip fired: I2's whole-return-of-120k assert reds AND
I2c's 64KiB-ceiling strict-xfail xpasses at the same moment #128 lands, so
per the multi-step-xfail rule they merge into ONE plain-green bounded-contract
test: HTTP 200 + isError:false + DATA truncated to <=64KiB + a truncation
marker + a keystone that a small output comes back whole and un-truncated.

Firsthand-verified on the live Lima stand (control rebuilt from PR #64,
FAT-guest override re-applied): "yes X | head -c 120000" -> 65535 bytes of
data + "[output truncated at 65536 bytes]" (was 502, then whole at 8MiB).
Group I: 8 passed, 0 xfailed — the tool-surface arc's three defects
([Exit code:N], large-output 502, timeout 502) are all closed end-to-end.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CLSUfSnuBtUftkxB7RvA72
…un journeys

The fleet stand was runnable only on one laptop: the compose bind-mounts
./fixtures/guest-config.json, the guest-image Dockerfile, the demo scripts,
gen-gateway-pki.sh, README, .env.example, and the group-H journeys were all
UNTRACKED, and the compose diff carried a security default (downloadable
prefixes -> /outputs, NFR-SEC-73) that lived nowhere in git. A clone could
not build or run anything.

This commits the deployable substrate:
- docker-compose.fleet.yml: the -downloadable-prefixes /outputs exfil default
  + OCU_DOWNLOADABLE_PREFIXES / OCU_GUEST_IMAGE env seams.
- .env.example: documents OCU_GUEST_IMAGE (the file tools need a python3-bearing
  FAT guest; the stripped default runs bash-only — see #122) + the prefixes.
- fixtures/guest-config.json: the mount config the guest reads (placeholders
  rendered at bring-up; no secrets).
- guest-image/Dockerfile, gen-gateway-pki.sh, exec-demo.sh, storage-chain-demo.sh,
  g7-visualizer/, README.md: the bring-up + demo surface.
- test_h_gateway.py: the group-H MCP gateway auth-edge journeys.

Scrub: process_api kept only where load-bearing (the real guest-agent binary
COPY/ENTRYPOINT + the real image tag in executable build/create commands);
every prose/comment mention genericized to "the guest agent" (identifier-vs-
provenance rule). The H1 forged-key fixture uses the allowlisted sk-ocu-wrong
placeholder. Secrets (gateway-pki/, secrets/, .env) stay gitignored.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CLSUfSnuBtUftkxB7RvA72
…le comment

The fleet journey suite (deploy/tests/journeys/) ran only when a human
remembered to, on a live Lima stand — no CI ever imported it, so import rot
or fixture drift would land silently. journeys-collect.yml adds a tier-1 gate
on every PR touching deploy/: compileall + `pytest --collect-only` (115 tests
collected firsthand, 0.18s), no live run. The tier-2 live-stand gate (needs a
real fleet a GH runner lacks) is a separate follow-up.

Also correct a stale compose comment: the mount root_path fix it called "task
tracked" was in fact REJECTED (ocu-control #113) in favor of storage-engine
scope resolution from the token intent claim (ADR-0029, shipped #116/#117/#118).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CLSUfSnuBtUftkxB7RvA72
…cap invariant

Every mcp_tool_surface.feature scenario now maps to a named covering test — the
13 @L4 to ocu-mcp-gateway forward-e2e (TestL4*), the 10 @l5 to group-I journeys
(test_i/test_h). 23 scenarios, 0 unmapped (the "18" I'd claimed miscounted the
Scenario Outline). Pins the large-output cap invariant in one committed place:
gateway.maxReplyBytes >= 2*ceil(control.replyCeiling*4/3)+envelope, control 64KiB
< gateway 256KiB; the F5 exec-reply schema follow-up is tracked as #344.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CLSUfSnuBtUftkxB7RvA72
… a clean clone

The final-proof run from a fresh in-Lima clone @bc8faa3 caught it: the journey
suite needs pytest + pyyaml, undocumented, so a clean clone stops at
`ModuleNotFoundError: yaml` before any test runs. requirements.txt pins both;
README adds the venv + install + pytest steps and notes the file-tool legs need
the OCU_GUEST_IMAGE=poc-fat-arm64 override (#345).

Verified: from the clean clone with these deps installed, the full A-I suite is
23 passed, 92 skipped, 0 failed against the live stand (gateway f7b6e5c, control
37d6492) — the arc's coverage runs from pushed state, not just this laptop.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CLSUfSnuBtUftkxB7RvA72
…eview

A fresh-clone review (Fable) found the arc's own tests were not reproducible
from pushed state. Four fixes:

1. Vendor scripts/mint_boot_set.py into deploy/fleet/scripts/ — the H/I journeys
   need a minted boot-set + bearer, but the minter lived only in ocu-mcp-gateway,
   so a clean clone of THIS repo could never render it and every gateway journey
   skipped (green-by-skip on missing substrate). It is standalone stdlib; the
   vendor note points at the gateway as the formula owner.
2. journeys-collect.yml installed only pytest while conftest imports yaml — the
   tier-1 gate would ImportError on its first run. Install -r requirements.txt.
3. Dead ticket refs in pushed files: FINDINGS cited "ocu-mcp-gateway #131" (the
   real anchor is PR #44/f7b6e5c); the compose comment cited a bare "ocu-control
   #113" + an ADR path absent from this branch — both de-numbered to
   repo-neutral descriptions a reader here can actually resolve.
4. README documents the mint step before the pytest run.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CLSUfSnuBtUftkxB7RvA72
…int runbook

mint_boot_set.py validates its rendered boot-set against
contracts/mcp/mcp-key-set.schema.json resolved next to the script, but that
schema was never committed here — it lived only in the gateway/control repos.
A clean clone therefore crashed the minter with FileNotFoundError before it
could render a boot-set, so the H (gateway auth-edge) and I (tool-surface)
journeys skipped for lack of a boot-set: green-by-skip, not a pass.

Vendor the schema alongside the minter (its doc-comment already promised it
sits there) and fix two runbook gaps in the README that would leave a clean
clone unable to run H/I:
  - --deployment is required and MUST equal the gateway's -deployment
    (fleet-local); a foreign-deployment record 401s (ADR-0027).
  - the minter prints the bearer to stdout; the tests read it from bearer.txt,
    so the runbook now captures the printed line there and recreates the
    gateway to reload the boot-set.

Verified from a truly clean in-Lima clone that renders its own boot-set: the
minter exits 0, the live gateway binary accepts the clean-clone-minted key
(200, not 401; a forged key still 401s), and H (5) + I (8) run green against
the live stand instead of skipping.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CLSUfSnuBtUftkxB7RvA72
… order)

The prior runbook redirected the bearer into
deploy/fleet/secrets/gateway/bearer.txt, but the shell opens that redirect
target before the minter creates --out-dir, so a clean clone failed with
"No such file or directory" and a BrokenPipeError. mkdir -p the out-dir first,
write the minter's stdout to a temp file, then take the last line as the
bearer — so a mint failure leaves a traceback in the temp file, never a
half-written bearer.txt.

Verified verbatim from a fresh in-Lima clone of the pushed tree.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CLSUfSnuBtUftkxB7RvA72
…at-guest build

Two closeout gaps a final review surfaced:

1. Nothing exercised the minter, so the vendored contracts/mcp schema could be
   deleted or drift and CI would stay green while a clean clone crashed with the
   same FileNotFoundError the vendoring just fixed. Add a mint-smoke step to
   journeys-collect.yml: it runs mint_boot_set.py against a throwaway deployment
   + out-dir (stdlib-only, no live stand, no pip) and reds if a boot-set cannot
   render. Red-probed: hiding the schema makes the step fail; restoring it passes.

2. The group-I file-tool legs need a python3-bearing guest, but no build recipe
   for one lived in the tree — the "13 passed" outcome leaned on a Lima-local
   image tag. Document the build (layer deploy/guest-image/Dockerfile over the
   repo-root PoC userland base) and state the expected counts so a third party
   can tell honest-green (13 passed / 0 skipped with a python3 guest) from
   green-by-skip (10 passed / 3 skipped on the stripped default).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CLSUfSnuBtUftkxB7RvA72
…d .env.example

Follow-on cleanup after the fat-guest recipe landed: .env.example cited
ocu-guest:poc-fat-arm64 (task #122), but the README build recipe produces
ocu-guest:poc-fat (task #345) — a reader following the .env.example comment
would set a tag the recipe never builds. Point the comment at the README
recipe and the poc-fat tag. Add --platform linux/amd64 to the base-build step
per repo policy, with the Lima-native drop note.

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

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 62f68617-a9d8-4f34-81e3-8584001f0a32

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch docs/demo-walkthrough

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

widemoat-ai and others added 4 commits July 10, 2026 14:02
The first tool call in a fresh Open WebUI chat wrote to /home/assistant and
died on the read-only rootfs: the model gets zero path guidance. The old
filter's /system-prompt fetch points at the MCP gateway, which fronts
tools/call only, so the injection degrades to nothing. Bind a fleet-true
system prompt (filesystem map, session semantics, verify-your-work) onto
every seeded model via params.system in init.sh.

Raise the default -session-idle-ttl 1m -> 15m (the NFR-SEC-40 ceiling): a 1m
window reaped the guest while the user was still reading the reply, wiping
/tmp and the scratch home mid-conversation. Short values stay one env var
away for reaper demos.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CLSUfSnuBtUftkxB7RvA72
A re-minted boot-set updates what the gateway accepts, but the chat leg keeps
presenting the OLD bearer from its tool Valve (seeded from MCP_API_KEY on
first boot). Every chat tool-call then dies 401 wrapped in the MCP SDK's
opaque cancel-scope transport error while the journey suite - which reads
bearer.txt directly - stays green. Add the .env update + Valve re-seed steps
to the runbook.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CLSUfSnuBtUftkxB7RvA72
…122)

The compose default was the distroless assembled tag: no shell, no python3,
so the first chat input in a fresh deploy hit a dead guest (file tools
project onto python3). Default to ocu-guest:poc-fat (build recipe already in
the README journey-suite section) and keep the stripped tags one env var
away for the minimal rung and the storage-chain demo scripts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CLSUfSnuBtUftkxB7RvA72
… semantics

The per-model seeding loop walks base-catalog ids only, so a legacy derived
record (unique id + base_model_id set, e.g. an ocu-* alias of a kept base)
kept stale params forever. When such a record is the fresh-chat default,
every new chat runs with NO system prompt while directly-seeded models carry
one. Step 2b now walks workspace records and re-binds tool + native FC +
prompt onto every record whose base points into the kept set.

The /mnt/user-data prompt bullet now states the exchange semantics the guest
actually observes: user uploads are readable in place, saved deliverables
write through to the user's Files panel and may leave the guest's listing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014g5EttcR8mqYDn8EByShUL
widemoat-ai and others added 30 commits August 2, 2026 16:29
…hand

The valves endpoint REPLACES the stored object. Listing only the keys a change
cares about drops the rest: adding the two resolve-scope valves silently
removed PREVIEW_MODE and both label valves, and with no PREVIEW_MODE the outlet
appends no preview link at all. The panel stops offering Preview and nothing
says why — the failure surfaces two layers from its cause.

Measured on the stand: the stored key set was exactly the seven this script
wrote, and PREVIEW_MODE read back empty.

Co-Authored-By: Claude <noreply@anthropic.com>
M12 walks the scenario the owner reported broken, in one browser session
with nothing staged: a save-request in a fresh chat, the model emitting the
download marker ITSELF, the filter minting a link carrying that chat's own
storage scope, the panel opening inside the chat, listing this chat's file
and no other's, Preview clicked and rendering the real content, Download
returning bytes, the chat-emitted link returning the SAME bytes, and both
the no-session and wrong-scope requests refused.

Openness of the panel is measured from the frame's bounding box against the
viewport, not from is_visible(): a slide-in panel parked at translateX(+width)
is "visible" to the DOM while sitting entirely off-screen, so is_visible()
reports a closed panel as open.

Two hops carry a recorded red-probe. Rebuilding the panel without
NEXT_PUBLIC_PREVIEW_RENDER_ENABLED reds the Preview assertion verbatim and
restoring the flag greens it, which also proves the compose build-arg is
load-bearing. Emptying the RESOLVE_SCOPE_URL valve reds the per-chat-scope
assertion with the base scope named in the message. The docstring records
only those two, and records that the filter's PREVIEW_MODE valve does not
red this test in either broken form: it governs the preview button appended
to a chat message, not the panel row's control.

Co-Authored-By: Claude <noreply@anthropic.com>
The foreign-scope leg ran on the same sessionless context as the no-session
leg. A caller with no pane session is refused whatever scope it asks for, so
that request re-measured the no-session refusal: the assertion would have
stayed green through a total scope leak, and the test's own passing output
showed it, since the line above it gets 401 for the CORRECT link from that
same context.

The refusal now runs on the session that just downloaded successfully, and a
control asserts that same session is served its OWN scope. If the control
does not pass, the refusal is unattributable and the control fails saying so,
rather than the refusal passing for the wrong reason.

Red-probed: replacing the foreign scope with the caller's own reds the
assertion verbatim ("a live session for fs-fleet-00cc2794 was served the
foreign scope ..."), so it discriminates the scope rather than the session.

Co-Authored-By: Claude <noreply@anthropic.com>
The panel hop asserted only that the chat's own file appears. That check
survives the failure it exists to catch: when the panel falls back to the
shared tree the chat's file is still among the rows, so a total loss of
per-chat isolation reads as green. The docstring already claimed "and no
other's" while nothing measured it.

The panel now must offer exactly one Download control, the number of files
this chat produced. The bound is measured rather than assumed: the same panel
opened on the base scope offers 925 Download controls and on a fresh chat
scope 0, so requiring 1 discriminates against the shared tree.

Also recorded, from probing it: breaking the portal's scope resolution empties
the panel instead of widening it, so that lever reds the presence assertion
rather than the count, and no configuration widens the panel without reddening
an earlier hop first.

Co-Authored-By: Claude <noreply@anthropic.com>
Dropping the chat's origin from OCU_FRAME_ANCESTORS and leaving the portal's
in place reds M12 with "the pane frame never attached inside the panel", so
the panel hop binds the allowlist rather than merely running alongside it.

The two-origin default is the reason: frame-ancestors is evaluated against the
whole ancestor chain, and the chat frames the portal which frames the pane.

Co-Authored-By: Claude <noreply@anthropic.com>
Pointing the chat link at an object that does not exist in the same scope
never produces a download, so the byte comparison reds. The link resolves the
object it names rather than serving whatever the scope happens to hold.

The comparison was already anchored outside the two delivery surfaces: the
body carries a per-run random marker, so panel bytes containing it can only be
the file this run produced, and the link's bytes are then held to that.

Co-Authored-By: Claude <noreply@anthropic.com>
_pane_session minted a portal token with no chat, so it always bound the base
scope, while _guest_exec writes into the chat's own subtree. Under per-chat
isolation those are different trees, so a list issued through this helper could
not see what the guest had just written.

Measured, not inferred: the same chat id gives scope fs-fleet (no chat) versus
fs-fleet-7e502c54c4e845d0 (with one), and a guest that writes probe.txt into
/mnt/user-data/outputs is then listed as ['probe.txt'] by a pane session bound
to that chat — the write propagates and the chat-scoped pane sees it.

The four M call sites pass their own chat id. This does not by itself green
M1-M11; their remaining failure has a separate, still unmeasured cause.

Co-Authored-By: Claude <noreply@anthropic.com>
…scope

The browser leg navigated to the portal with no chat, so the pane it framed
bound the base scope while the guest had written into the chat's own subtree.
The file was in the store and in the north-face list, and the browser still
could not see it.

M6 fails before this change and passes after it, with M12 unaffected in the
same run. The list-side half (01c9fbc) moved M6's failure from "never appeared
in GET /v1/files" to "waiting for locator to be visible"; this closes the
browser-side half of the same mismatch.

Co-Authored-By: Claude <noreply@anthropic.com>
a9bb0f7 rewrote every portal navigation to name a chat, including ones inside
helpers and tests that never define chat_id, and one where the navigation
precedes the binding. That turned three tests into NameError/UnboundLocalError
and took M12 down with them — a blind edit across seven sites, checked on two.

Reverted at the five sites with no chat in scope. M2b, M6 and M12 pass together
after this; M2 is back to the AssertionError it failed with before any of this
work, not to a crash this change introduced.

Co-Authored-By: Claude <noreply@anthropic.com>
01c9fbc passed chat_id=chat_id at four list call sites; two of them sit in
functions that never bind it, so those tests raised NameError instead of
running. a2aa3af repaired the portal-navigation half of the same blind edit and
missed this one, which the next full run surfaced.

The argument stays where the name is a parameter or is assigned before use, and
is dropped where it is not. M6 and M12 pass together after this; M3 fails as it
did before any of this work.

Co-Authored-By: Claude <noreply@anthropic.com>
Three times today a broad edit threaded an argument into call sites whose
functions never bind it. Each time the tests raised NameError instead of
running, and each time the only thing that surfaced it was a full browser run
of twenty-five minutes — twice, because the first repair was verified on two
tests out of fourteen.

pyflakes answers the same question in 0.2 seconds. The guard runs it over every
journey module and fails on any undefined name, and skips loudly rather than
passing when pyflakes is absent.

Red-probed: a planted reference to an unbound name fails the guard, and
removing it greens it again.

Co-Authored-By: Claude <noreply@anthropic.com>
M12's panel hop rules out the shared tree — 925 rows against 1 — but not a
second chat: two chats whose scopes collided would each show one row and pass
it. M13 names the property directly. A and B each write a file; A's pane must
list A's and not B's, and B's the reverse.

Red-probed: binding A's pane to B's chat id reds with "chat A's pane does not
list its own file: ['m13b-....txt']" — one row, and it is B's, so the pane
binds the chat it is given rather than ignoring the argument.

Runs in seven seconds; it drives the pane API, not a browser.

Co-Authored-By: Claude <noreply@anthropic.com>
m11 guards that the background poll fires, and recognised a poll tick by the
?order=desc query. ocu-webui 5a93ffe reaches newest-first a different way: it
reads EVERY page client-side and sorts, deliberately sending no order
parameter, and ships tests of its own that fail a first-page-only pane. The
marker therefore vanished from a pane that is correct, and m11 red on it 0 of
22, three runs out of three — while all 22 polls fired. A red that means the
implementation changed, not that the product broke.

It now asserts what its name says: the poll issued more than one list request
in the window without a navigation. Probed by raising the bound, which reds
and prints the live count (22), so the assertion reads the measurement rather
than a constant.

Co-Authored-By: Claude <noreply@anthropic.com>
_preview_text_in_pane navigated to the portal with no chat, so the pane it
framed bound the base scope while the file lived in the chat's subtree. m5
died on the first preview, never reaching what it exists to check.

This does not make m5 green, and the commit does not claim it does. It moves
the failure from "the file never appears in the pane" to the assertion the
test is named for: after a str_replace, the pane preview still shows the old
text. The first preview now passes; the second is where it stops.

Measured on m5's own creation path: create_file through MCP lands the object,
the chat-scoped list returns it, and the pane frame renders it at 6 B
text/plain — so create, list and render are not the defect.

Co-Authored-By: Claude <noreply@anthropic.com>
_wait_content_contains bootstrapped a pane session with no chat, so it polled
the base scope for an object that lives in a chat subtree, and waited out its
whole 45s budget every time. That is the last of the four places the chat had
to be threaded through: the list, the portal navigation, the preview helper,
and now the content read.

m5 goes from failing at 53s to passing at 13.65s — it stops burning the
timeout and finds what it was looking for. The product half was already
sound: sampled directly, the pane preview shows the replaced text at 10s,
25s, 45s and 60s after a str_replace.

Co-Authored-By: Claude <noreply@anthropic.com>
m3 built its chat id inline in the _guest_exec argument, so the guest wrote
into a subtree whose name existed nowhere afterwards. The pane could not be
opened on that scope by construction, and the list and preview both looked at
the base scope instead.

The chat is now a variable and travels to both the list wait and the image
preview. m3 goes from failing at 62s to passing at 8.27s.

The image preview helper regains the chat parameter it was given and then had
reverted when no caller could supply one; m3 now can.

Co-Authored-By: Claude <noreply@anthropic.com>
Creating a guest session churns the container network. A browser that
navigates inside that window loses its lazy chunks mid-load: the server still
serves the full 17 KB of HTML, the container never restarts and its log is
clean, but SvelteKit does not mount and the body comes back empty. The five
live-model tests all create a guest session and then drive the browser, so all
five died on Page.fill waiting for an input that was not rendered.

Measured, both directions and order-controlled: body empty at 0s after a guest
exec, normal at 20s and 40s; three clean loads before a preamble and three
empty after it, then the same with the order reversed. The browser reports
ERR_NETWORK_CHANGED and "Failed to fetch dynamically imported module".

The login now reloads until the app mounts. m7 moves from a 30s fill timeout
to reaching its actual subject — the model runs, drives a bash_tool call, and
the assertion that fails is the artifact one. M12 is unaffected.

Co-Authored-By: Claude <noreply@anthropic.com>
The read-back polled through a fresh _guest_exec chat id, and the comment a few
lines up still explains why that was safe: cross-chat was off, so any id saw the
same outputs tree. Per-chat isolation retired that. A fresh id now opens an empty
tree, so the artifact was invisible no matter how well the turn went — and the
assertion blamed the model for a scope the test chose.

It now reads in the chat OpenWebUI settled in the URL. Measured on that path
before the change: the model issued a correct bash_tool call, the PNG existed in
the chat's own scope (m7-318fccf1.png under fs-fleet-d2b42caa), and a direct
guest exec in the same chat wrote and listed a file fine.

m7 passes for the first time. M12 red in the same run went green after an
OpenWebUI restart with no code change, so that was stand wear, not this edit.

Co-Authored-By: Claude <noreply@anthropic.com>
…ed them

Same stale assumption m7 carried: the read-back polled through a fresh
_guest_exec chat id, which used to see the same outputs tree and no longer does.
The artifact was invisible however well the turn went, and the assertion blamed
the model for a scope the test chose.

All three now read in the chat OpenWebUI settled in the URL, the shape already
proven on m7. m8, m9, m10 and M12 pass together.

Co-Authored-By: Claude <noreply@anthropic.com>
The browser opened the portal with no ?chat= and uploaded into the base
scope, while the guest carried X-Chat-Id and mounted the chat's own
subtree. The bytes were never where the guest looked: 23 m2 uploads sit
in fs-fleet/uploads and none in any per-chat scope.

The pane barrier waited for a Download row, which a brand-new chat can
never show -- it only ever passed by reading the shared base tree. It now
waits for the empty-state line and asserts zero rows, so the barrier
states what this chat's scope actually holds.

Both legs red-probed: opening the portal with no chat fails the barrier;
pointing the guest at another chat fails the byte assertion.

Co-Authored-By: Claude <noreply@anthropic.com>
M13 proves the isolation at the API and never opens a browser; M12 opens
the panel and stays in one chat. Switching chats is a client-side
navigation: nothing reloads and the iframe the panel already holds keeps
pointing at the chat the user left. The panel re-checks the path on an
interval and remounts. Nothing exercised that.

Two real chats, one file each. The panel is opened in the second, then
the first is clicked in the sidebar, and the panel must list the first
chat's file and not the second's. A page-scoped marker set before the
click must survive it, so a full reload cannot pass this by rebuilding
the panel from scratch.

Red-probed against the live image: dropping the remount call from the
panel's interval fails the post-switch assertions and nothing else.

Three probe artifacts fixed while writing it, each recorded where it bit:
page.url lags the document across replaceState (ask location.href), the
portal frame is the one carrying ?chat= and the pane is a frame deeper,
and a reader that returns the first non-empty text accepts the
empty-state as an answer.

Co-Authored-By: Claude <noreply@anthropic.com>
The pane is bound to a chat by the ?chat= the panel puts on the portal
URL. With no chat there is nothing to put there, and a portal opened
without one binds the base scope: the whole tree, every chat's files,
rendered under a control that says "Files produced in this chat". The
label asserts the opposite of what is on screen.

The panel already declines to mount and shows a hint. M12 and M14 both
arrive with a chat open, so neither reaches that branch.

The test asserts the page is not a chat before anything else, so a stray
chat id cannot make it vacuous. Red-probed against the live image:
removing the no-chat guard embeds one pane iframe and fails on the count.

Co-Authored-By: Claude <noreply@anthropic.com>
Four files conflicted. Each was resolved by reading both parents against
the merge base rather than the markers alone, and each resolution was then
checked by an independent pass whose job was to find behaviour that had
been dropped. None was found.

- .github/workflows/build.yml: next/v1's digest-then-tag sign/promote
  architecture is kept whole; this branch's per-image SLSA provenance is
  folded into its sign matrix at the same action SHA, and the
  attestations: write permission next/v1 lacked comes along. The
  system-prompt/skills contract test joins the test job.
- openwebui/functions/computer_link_filter.py: both branches landed the
  same scope-segment fix independently, so this branch is a strict
  superset. Kept whole. The new RESOLVE_SCOPE_URL valve defaults empty,
  which reduces the scope expression to exactly next/v1's.
- openwebui/functions/test_computer_link_filter_download.py: next/v1's
  174 lines are a byte-exact prefix of this branch's 269. All ten of its
  tests survive at the same line numbers.
- openwebui/init.sh: next/v1's clearer comment on the download base wins;
  the valve payload keeps all five of its keys at identical values plus
  this branch's five.

Nothing executable changed for the three runtime files this branch's live
measurements were taken against: two are byte-identical, init.sh differs
only in comment prose.

Co-Authored-By: Claude <noreply@anthropic.com>
Every other M test gates on the browser or on a configured model and skips
where neither exists. M13 talks to the gateway directly and had no gate, so
on a runner with no fleet it raised "curl transport failure rc=7" and the
suite reported a failure where the truth was that nothing had been deployed
to test. That is what the collect job hit.

The I-suite's equivalent is an autouse fixture, which pytest refuses to let
another module call, so its three conditions are mirrored. The asymmetry is
kept: an absent gateway skips, a gateway answering 401 fails -- that means
the bearer and the running boot-set are from different trees, and skipping
there once let a stale bearer pass as a clean capability skip.

Both sides measured: with the stand up M13 passes; with the boot-set path
pointed at nothing it skips with that reason instead of raising.

Every M test that reaches the stand is now gated -- checked by walking each
test body for a stand call and for a gate, not by fixing the one that failed.

Co-Authored-By: Claude <noreply@anthropic.com>
Both callers reached for shlex.quote, which is the tell that a shell was in
the loop at all: with shell=True the quoting was the only thing standing
between an image name or a compose path and a second command. Passing a list
hands the words to execve, so a value carrying a quote, a semicolon or a
newline stays one argument by construction rather than by escaping.

docker_cmd is split rather than concatenated because it is a command line,
not a word: deployments set it to "sudo docker".

Measured both ways with the same hostile input "probe; touch <marker>":
through the list form /bin/echo printed it verbatim and no marker appeared;
through the old shell form the shell cut it in two and touch ran.

Closes the only semgrep subprocess-shell-true finding on this branch. The
remaining subprocess findings are the harness driving docker and curl with
argv it builds itself, which is a separate call.

Co-Authored-By: Claude <noreply@anthropic.com>
The SCA gate runs two passes: CRITICAL including unfixed, then HIGH
fixable-only. The second is what reds this branch -- a fixable HIGH has a
bump answer, so the gate enforces bump-not-ignore. It printed no findings
because format: sarif sends them to a file, which is why the log reads as
an unexplained exit 1.

  pillow    12.2.0 -> 12.3.0   10 CVE
  mcp       1.27.0 -> 1.28.1    3 CVE
  pypdf     5.9.0  -> 6.14.2    2 CVE
  httplib2  0.20.4 -> 0.32.0    1 CVE

pypdf 5 -> 6 is a major, so its consumers were checked rather than assumed:
the four skill scripts and the server import PdfReader, PdfWriter,
generic.DictionaryObject, constants.FieldDictionaryAttributes and
annotations.FreeText -- no removed camelCase shim. All five resolve against
an installed 6.14.2.

Measured with the gate's own flags: fixable HIGH 16 -> 0, CRITICAL 0.

The same pillow bump merged separately on main. It does not reach here:
main and this lineage share no ancestor, and this branch still pinned
12.2.0 after that merge.

Co-Authored-By: Claude <noreply@anthropic.com>
Control answers every unclassified refusal with a bare 409 and an EMPTY
body on purpose: the reason lives in the audit stream, never in the
response. A guest image the daemon does not have fails at materialize and
surfaces here identical to an exhausted quota.

The bare "denied:409" sent this investigation through the quota source,
the counter schema, the control database and the audit volume before the
audit named it -- and the counters could never have answered, because a
refused charge is refunded to zero by design.

Measured on the live stand, one variable changed:

  FLEET_GUEST_IMAGE=ocu-guest:assembled-demo  ->  32 failed / 57 passed
  FLEET_GUEST_IMAGE=ocu-guest:poc-fat         ->   2 failed / 81 passed

The stand's control runs -guest-image ocu-guest:poc-fat and merely
allow-lists assembled-demo, which was never built. Thirty of the
thirty-two failures were that one absent image.

Co-Authored-By: Claude <noreply@anthropic.com>
Both opened the pane with no chat -- which binds the BASE storage tree --
and then named a fresh chat for the guest, whose mount is that chat's own
subtree. The bytes land correctly and the guest looks somewhere else, so
j2 reads "No such file or directory" and j5b fails on its own precondition
instead of on the tamper property it exists to guard.

Same defect m2 carried this morning: under per-chat isolation both halves
must name the same chat or the test silently measures two trees.

Red-probed one side each: pointing the guest at another chat reds j2;
binding the pane to another chat reds j5b.

Co-Authored-By: Claude <noreply@anthropic.com>
Folding the image name into SessionRef.status broke every test that
compares that field for equality -- test_e_lifecycle asserts
status == "denied:409" exactly, and the appended "(image=...)" made it
never match. The whole e-module went red on a change meant to make one
message clearer.

status is a contract value, not a message. The diagnosis moves to a new
`detail` field nothing compares, so a refused create still names the image
without redefining the code beside it.

Measured after the change with no other run contending: test_e_lifecycle
7 passed, 0 failed. The 6 failures seen while a full suite ran in parallel
were session contention, not this fix.

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants