Read-only surface: container doctrine promises /scratch but the mount does not exist #1142

Closed
opened 2026-07-13 03:03:52 +00:00 by coilyco-ops · 2 comments
Member

Found during a director-surface capability audit on director-claude-sc69 (ward v0.633.0, WARD_READONLY=1, target coilyco-flight-deck/agentic-os).

What is wrong

The container doctrine composed into every agent context says:

For read-only surface sessions, the workspace clone is chmod'd read-only and /scratch is the writable escape hatch for throwaway scripts, temp files, and one-off command output.

and later:

Read the repo, reason about it, answer questions, scratch in /scratch if it helps you think

On this surface /scratch does not exist:

$ ls -ld /scratch
ls: cannot access '/scratch': No such file or directory
$ touch /workspace/agentic-os/.probe
touch: ... Permission denied      # workspace really is read-only, as documented
$ touch /tmp/.probe && echo ok
ok                                # /tmp IS writable

So the read-only half of the contract holds and the escape-hatch half does not.

Why it matters

This is a doctrine-vs-implementation mismatch, not a hard block: /tmp is writable, so an agent that tries /scratch gets an ENOENT, then has to rediscover a writable path on its own. Every read-only surface session pays that tax, and an agent that trusts the doctrine may instead conclude it cannot write anywhere and silently skip work it should have done.

Fix, either direction

  • Make the doctrine true - have read-only surface bring-up mkdir a writable /scratch (tmpfs or plain dir), which is the better outcome since a named scratch surface is greppable and reapable.
  • Or make the doctrine honest - change the two doctrine lines to point at /tmp.

Preference is the first: the doctrine text is the more valuable artifact and /scratch is a clearer contract than "somewhere in /tmp".

Verification

On a fresh read-only director surface, ls -ld /scratch shows a writable directory owned by the container user, and a touch /scratch/probe succeeds.

Filed by the director surface capability audit.

Found during a director-surface capability audit on `director-claude-sc69` (ward v0.633.0, `WARD_READONLY=1`, target `coilyco-flight-deck/agentic-os`). ## What is wrong The container doctrine composed into every agent context says: > For read-only surface sessions, the workspace clone is chmod'd read-only and `/scratch` is the writable escape hatch for throwaway scripts, temp files, and one-off command output. and later: > Read the repo, reason about it, answer questions, scratch in `/scratch` if it helps you think On this surface `/scratch` does not exist: ``` $ ls -ld /scratch ls: cannot access '/scratch': No such file or directory $ touch /workspace/agentic-os/.probe touch: ... Permission denied # workspace really is read-only, as documented $ touch /tmp/.probe && echo ok ok # /tmp IS writable ``` So the read-only half of the contract holds and the escape-hatch half does not. ## Why it matters This is a doctrine-vs-implementation mismatch, not a hard block: `/tmp` is writable, so an agent that tries `/scratch` gets an ENOENT, then has to rediscover a writable path on its own. Every read-only surface session pays that tax, and an agent that trusts the doctrine may instead conclude it cannot write anywhere and silently skip work it should have done. ## Fix, either direction * Make the doctrine true - have read-only surface bring-up `mkdir` a writable `/scratch` (tmpfs or plain dir), which is the better outcome since a named scratch surface is greppable and reapable. * Or make the doctrine honest - change the two doctrine lines to point at `/tmp`. Preference is the first: the doctrine text is the more valuable artifact and `/scratch` is a clearer contract than "somewhere in /tmp". ## Verification On a fresh read-only director surface, `ls -ld /scratch` shows a writable directory owned by the container user, and a `touch /scratch/probe` succeeds. Filed by the director surface capability audit.
Author
Member

WARD-RESERVATION: held 🔒

reservation details

Holder: container engineer-claude-ward-1142 on host KAI-DESKTOP-TOWER.

Reserved by ward agent --harness claude (reserved 2026-07-13T03:04:09Z). Concurrent ward agent runs are blocked until it finishes or the reservation goes stale (3h TTL). --override-reservation overrides.

Do not comment on or edit this issue to steer the run while it is reserved. The engineer seeded the body once at launch and never re-reads it, so a comment or edit reaches only human readers, never the running engineer. A correction goes to a new issue, dispatched fresh. That is the only channel that reaches a run in flight. Where the forge supports it, ward locks this conversation to make that a road-block rather than a convention (ward#494).

run seed context — what this run is carrying (ward#609)
  • Resolved: coilyco-flight-deck/ward#1142 · branch issue-1142 · harness claude · workflow pull-request-and-merge
  • Run: engineer-claude-ward-1142 · ward v0.633.0 · dispatched 2026-07-13T03:04:09Z
  • Comment thread: 0 included in the pre-flight read, 0 stripped (ward's own automated comments).

Static container doctrine and seed boilerplate are identical every run and omitted here (they ride ward v0.633.0).

— Claude (she/her), via ward agent

<!-- ward-agent-reservation --> WARD-RESERVATION: held 🔒 <details><summary>reservation details</summary> Holder: container `engineer-claude-ward-1142` on host `KAI-DESKTOP-TOWER`. Reserved by `ward agent --harness claude` (reserved 2026-07-13T03:04:09Z). Concurrent `ward agent` runs are blocked until it finishes or the reservation goes stale (3h TTL). `--override-reservation` overrides. **Do not comment on or edit this issue to steer the run while it is reserved.** The engineer seeded the body once at launch and never re-reads it, so a comment or edit reaches only human readers, never the running engineer. A correction goes to a **new issue, dispatched fresh**. That is the only channel that reaches a run in flight. Where the forge supports it, ward locks this conversation to make that a road-block rather than a convention (ward#494). <details><summary>run seed context — what this run is carrying (ward#609)</summary> - **Resolved:** `coilyco-flight-deck/ward#1142` · branch `issue-1142` · harness `claude` · workflow `pull-request-and-merge` - **Run:** `engineer-claude-ward-1142` · ward `v0.633.0` · dispatched `2026-07-13T03:04:09Z` - **Comment thread:** 0 included in the pre-flight read, 0 stripped (ward's own automated comments). Static container doctrine and seed boilerplate are identical every run and omitted here (they ride ward v0.633.0). </details> </details> <!-- ward-agent-signature --> — Claude (she/her), via `ward agent`
Author
Member

WARD-OUTCOME: submitted

details

workflow: pull-request-and-merge; review summary: skipped - the in-container review gate was intentionally skipped per the temporary ward default pending brokered QA.

What landed. PR #1146 (branch issue-1142, head 50f3d92b) makes the doctrine true, the issue's preferred direction: bootstrap now links /scratch to the actual scratch root right after prepareScratchSpace, so read-only surfaces (whose scratch deliberately lives at <gitcache>/surface-scratch for Go-verification disk headroom) get the promised path while the storage stays on the volume with headroom. Writable runs already use /scratch directly, so the alias is a no-op there. ensureScratchAlias is idempotent, repoints a stale link, and leaves a pre-existing real directory alone. Covered by TestEnsureScratchAlias including the issue's touch-through-the-alias probe. CI on the PR head is green (test run 7446, success).

Surprise worth knowing. main was red on the tree-walking code-comments hook - 40 violations left by 2026-07-12 reaper commits that bypass pre-commit - so no hook-clean commit could land at all. The branch carries a burndown commit (a5c0bdee) reflowing those comment blocks and fixing two terse doc refs first. The gate hole itself is filed as ward#1147.

Confidence. High on the fix and tests. The one unverified-in-anger piece is bring-up ordering on a live read-only surface (the symlink is created as root before the drop to the agent user), medium-high there since it rides the same path as the existing prepareScratchSpace root-phase call. Verification per the issue: on a fresh read-only director surface, ls -ld /scratch shows the link and touch /scratch/probe succeeds.

Felt like. A clean small fix wrapped in an archaeology detour - most of the elapsed effort went to the pre-existing gate debt, not the feature.

Branch is merge-ready, the director holds the merge under pull-request-and-merge.

WARD-OUTCOME: submitted <details><summary>details</summary> workflow: pull-request-and-merge; review summary: skipped - the in-container review gate was intentionally skipped per the temporary ward default pending brokered QA. **What landed.** PR #1146 (branch `issue-1142`, head 50f3d92b) makes the doctrine true, the issue's preferred direction: bootstrap now links `/scratch` to the actual scratch root right after `prepareScratchSpace`, so read-only surfaces (whose scratch deliberately lives at `<gitcache>/surface-scratch` for Go-verification disk headroom) get the promised path while the storage stays on the volume with headroom. Writable runs already use `/scratch` directly, so the alias is a no-op there. `ensureScratchAlias` is idempotent, repoints a stale link, and leaves a pre-existing real directory alone. Covered by `TestEnsureScratchAlias` including the issue's touch-through-the-alias probe. CI on the PR head is green (test run 7446, success). **Surprise worth knowing.** main was red on the tree-walking code-comments hook - 40 violations left by 2026-07-12 reaper commits that bypass pre-commit - so no hook-clean commit could land at all. The branch carries a burndown commit (a5c0bdee) reflowing those comment blocks and fixing two terse doc refs first. The gate hole itself is filed as ward#1147. **Confidence.** High on the fix and tests. The one unverified-in-anger piece is bring-up ordering on a live read-only surface (the symlink is created as root before the drop to the agent user), medium-high there since it rides the same path as the existing `prepareScratchSpace` root-phase call. Verification per the issue: on a fresh read-only director surface, `ls -ld /scratch` shows the link and `touch /scratch/probe` succeeds. **Felt like.** A clean small fix wrapped in an archaeology detour - most of the elapsed effort went to the pre-existing gate debt, not the feature. Branch is merge-ready, the director holds the merge under pull-request-and-merge. </details>
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
coilyco-flight-deck/ward#1142
No description provided.