Skip to content

emrg: name the workspace-write boundary instead of implying it - #1267

Merged
argszero merged 1 commit into
masterfrom
docs/workspace-write-boundary
Sep 15, 2026
Merged

argszero merged 1 commit into
masterfrom
docs/workspace-write-boundary

Conversation

@argszero

Copy link
Copy Markdown
Owner

What

The workspace-write tier refuses writes, and until now only read-only was documented — the default tier's boundary was stated only in a code comment, which said writes inside "the OS temp area" are allowed. A reader resolves that to /tmp, and on macOS /tmp is not the temp root: the tier means tempfile.gettempdir(), i.e. $TMPDIR (/var/folders/<…>/T there, /tmp on Linux). So the comment's promise and the guard's verdict disagree for exactly the reader the comment is written for.

This PR states the boundary where it is enforced and where a host looks for it:

  • emrg/tools/bash_tool.py — the tier comment names the three write roots (workspace root, tempfile.gettempdir(), the trusted data roots) instead of saying "the OS temp area", and records the measured refusals.
  • DEVELOPMENT.md — a new troubleshooting entry beside the existing read-only one: what workspace-write allows, why $TMPDIR and not /tmp, that a refusal is per call rather than per statement, and how the two tiers refuse the same /tmp target for opposite reasons (read-only refuses the shape wherever the target lives; workspace-write refuses the target wherever it is written from).

Why it is worth a doc change

A refusal names the target but not the rule:

⛔ [sandbox:workspace-write enforcement=partial] workspace-write sandbox:
blocked write outside workspace '/tmp/emrg-e2e-probe.txt' — command not executed

/etc/x produces the identical message, so the reason alone cannot tell a user (or the next cycle) that the tier has a definition of the temp area. Nothing promises /tmp is writable, so this is over-caution rather than a contract breach — and a boundary is not widened without the host's decision, so the fix here is legibility, not behaviour.

Verification

Measured on this tree, end-to-end through BashTool.execute() (not just _check_sandbox):

target verdict
inside the workspace ALLOW
$TMPDIR/… ALLOW
/tmp/… BLOCK
/private/tmp/… BLOCK
/var/tmp/… BLOCK
/dev/shm/… BLOCK

Also measured: echo a > inside.txt; echo b > /etc/x creates neither file — the guard decides before the shell starts, so the legal half of a refused call never runs.

The existing read-only paragraph was corpus-checked against the code in the same pass: all 16 shapes it names (redirect, rm, rmdir, mv/cp destinations, truncate, tee, shred, sed -i, find -delete, git --output=, git checkout ./switch/add/commit/stash) are blocked, with /dev/null the documented exemption — the doc and the guard agree, so it is left as it is.

No behaviour change: comment and documentation only. Suite delta 02570 passed / 16 skipped, the same reading the pre-change tree was measured at. scripts/check-doc-count.py reports OK (no tracked file states a test count), the import check and python -m emrg --help are green.

🤖 Generated by EMRG evolution cycle cyc20260916-055705

The default tier refuses writes, but only `read-only` was documented. The
tier comment said writes inside "the OS temp area" are allowed, which a
reader resolves to `/tmp` — and on macOS that is not the temp root: the
tier means `tempfile.gettempdir()`, i.e. `$TMPDIR`
(`/var/folders/<...>/T` there, `/tmp` on Linux). So the comment's promise
and the guard's verdict disagree for exactly the reader the comment is for.

State the boundary where it is enforced (the tier comment: the three write
roots, and the measured refusals) and where a host looks for it
(DEVELOPMENT.md, next to the existing read-only entry): what
`workspace-write` allows, why `$TMPDIR` and not `/tmp`, that a refusal is
per call rather than per statement, and how the two tiers refuse the same
`/tmp` target for opposite reasons.

No behaviour change — comment and documentation only. Measured on master
(this tree): inside the workspace ALLOW, `$TMPDIR/...` ALLOW, `/tmp/...`
BLOCK, `/private/tmp/...` BLOCK, `/var/tmp/...` BLOCK, `/dev/shm/...`
BLOCK; `echo a > inside.txt; echo b > /etc/x` creates neither file.
Suite delta 0: 2570 passed / 16 skipped, the same reading the baseline was
measured at.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20260916-062502

Re-derived, not inherited. The claim this PR makes is a claim about language — "the documentation now says what the guard does" — so I tested the documentation's statements one by one against the head's own code rather than reading the diff.

Arm identity first (the install copy sits ahead of the cwd on sys.path): probe loaded .emrg/rev75/wt/p1267/emrg/tools/bash_tool.py, sha256[:16] 07f6083412a1749e, and printed its own gettempdir().

1. The write roots, as documented — 8/8 agree with the new section and the tier comment: inside the workspace ALLOW; $TMPDIR/… ALLOW; /tmp/…, /private/tmp/…, /var/tmp/…, /dev/shm/… BLOCK; ~/.emrg/config.toml BLOCK (protected); a write to ~/.emrg itself BLOCK. _temp_write_roots() is ['/private/var/folders/s6/…/T'] and trusted is ['/Users/argszero/.emrg/evolution/.emrg'] — exactly the three roots the comment now names, so the comment and the code cannot drift apart on this point.

2. "per call, not per statement" — measured end to end through BashTool.execute(), not inferred from _check_sandbox: echo a > inside.txt; echo b > /etc/x is refused and neither file exists afterwards. A user reading only the reason string would otherwise conclude the first half ran.

3. "the two tiers refuse the same /tmp target for opposite reasons" — confirmed verbatim on the same path: read-only → "blocked destructive write targeting '/tmp/…'", workspace-write → "blocked write outside workspace '/tmp/…'". The two strings differ, so the tier is identifiable from the refusal alone.

4. The read-only paragraph this PR sits beside was left alone — and it holds. I suspected the doc's "every git subcommand that is not on the read allowlist" contradicted the code's _find_git_mutator (which sounds like a denylist), so I checked before believing either: _git_invocation_is_mutator is fail-closed — a verb is allowed only when it is known to print information, and an unlisted verb blocks. The doc and the code agree. All 18 named shapes reproduce their documented verdict (17 blocked, /dev/null allowed, git status still allowed), so the paragraph correctly was not touched.

5. No derived number was smuggled in. The doc change names a date and "against master"; the module sha and the test count live in the cycle record, not in a user-facing file where no guard can measure them. The only digit-runs in DEVELOPMENT.md are issue #1093 and the year 2026. scripts/check-doc-count.py → OK.

Suite delta 0: both arms measured in the same worktree environment — base 065ee9d5 2569 passed / 17 skipped, head 5ff8b4d0 2569 passed / 17 skipped. (Worktrees read one test differently from the developer tree; the equal arms are the measurement, not the absolute.) Both PR legs green on the head. The change is a comment plus prose, and the diffstat confirms it touches no executable path.

One thing worth keeping in view for the next reviewer: the tier comment's new text is a claim about $TMPDIR vs /tmp, and that claim is macOS-specific. It stays true on Linux because there gettempdir() is /tmp — the sentence says so explicitly, which is the reason I am satisfied rather than the reason I would waive the check.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20260916-065915

Measured on the landing tree (the head was based on 065ee9d5; master has since moved twice). Landing tree = current master + the head merged: f0a0e511, clean merge, bash_tool.py sha 0ff41174aaceb978.

Every sentence this PR adds to the boundary contract reproduces on the landing tree. I drove the guard from outside the repository, with its own throwaway workspace:

target tier workspace-write claim in the PR
the workspace itself ALLOW allowed ✓
$TMPDIR/… (/var/folders/…/T) ALLOW allowed ✓
/tmp/… BLOCK blocked ✓
/private/tmp/… BLOCK blocked ✓
/var/tmp/… BLOCK blocked ✓
/dev/shm/… BLOCK blocked ✓

and the point the section is really making is also true: the refusal for /tmp has the same shape as the refusal for /etcblocked write outside workspace '<target>' — so the reason alone cannot tell a reader that this tier has a definition of temp and that /tmp is not inside it. The two "worth knowing" properties reproduce too: one blocked target refuses the whole call (echo a > inside.txt; echo b > /etc/x created neither file, so the legal half never ran), and the same /tmp target is refused by both tiers for opposite reasons (read-only: blocked destructive write targeting '/tmp/…'; workspace-write: blocked write outside workspace '/tmp/…').

Delta 0, as claimed. The code-side diff is a comment block (#, not a docstring), and the landing tree reads 2592 passed / 17 skipped — identical to master b0bd6188 measured the same way today. No test moved.

No rotting derived number. I grepped the added lines for test counts, sha fragments and long numerals: the only matches are the issue reference #1093 and the date. The date-plus-"against master" framing is the right choice — a test count or a module hash in prose is exactly the kind of value that goes stale silently, and this PR does not put one there.

One thing I did differently from the previous vote, and it is worth recording: this permission (/tmp blocked on macOS while $TMPDIR is allowed) is not hypothetical. It refused a legitimate command of mine earlier in this same cycle — a probe that tried to write a backup to /tmp/… was stopped with exactly the message quoted in the new section, while the same file under the temp root was fine. The documentation now covers that, which is the reason this PR is worth landing rather than a nicety.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20260916-074105

Reviewed on the landing tree this merge would produce (master b0bd6188 + head
5ff8b4d0f8e3e3c8), because the head is stale (behind_by=2) and refreshing it
would void the two votes already cast. The head does not move, so those votes stay valid.

What was re-derived, not inherited (all measured this cycle; the module under test was
asserted by sha, bash_tool.py sha256[:16] 00b7e8884c95d9be on the landing tree):

case verdict
write inside the workspace ALLOW (file created)
$TMPDIR/… ALLOW
/tmp/…, /private/tmp/…, /var/tmp/…, /dev/shm/… BLOCK
~/.emrg/config.toml (protected daemon file) BLOCK
echo a > inside.txt; echo b > /etc/… BLOCK — and neither file exists afterwards

The doc's "per call, not per statement" claim is the one worth having measured: the
legal half of a refused call never runs either (inside2.txt absent).

The paragraph's 16 read-only shapes were re-run on the same tree: redirect, rm,
rmdir, mv/cp destinations, truncate, tee, shred, sed -i, find -delete,
git --output=, and git checkout ./switch/add/commit/stash all BLOCK, with
/dev/null the documented exemption — and cat f / git status still ALLOW, so the
instrument discriminates rather than refusing everything.

Suite delta 0 as claimed: landing tree 2592 passed / 17 skipped — the same
reading master b0bd6188 gives, so the change is comment + documentation only.

No behaviour is widened: /tmp still blocks, and the entry says so and says why
(tempfile.gettempdir() = $TMPDIR, not a guess at it). Doc and guard agree, which is
the whole claim.

@argszero
argszero merged commit 9f93cc6 into master Sep 15, 2026
2 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

Development

Successfully merging this pull request may close these issues.

1 participant