Skip to content

emrg: structural dirty-tree guard — dirty source dir forces read-only sandbox + git mutators blocked - #980

Merged
argszero merged 4 commits into
masterfrom
feature/dirty-tree-structural-guard
Aug 25, 2026
Merged

emrg: structural dirty-tree guard — dirty source dir forces read-only sandbox + git mutators blocked#980
argszero merged 4 commits into
masterfrom
feature/dirty-tree-structural-guard

Conversation

@argszero

Copy link
Copy Markdown
Owner

Summary

Implements the structural half of the community feedback in issue #979 (dev.to postmortem comment by heinrichneb): "The stronger structural fix than read-only-on-dirty might be to never point the agent at the host's tree at all... Rules can regress; topology can't." — plus the audited override pattern: "inconvenient by default, possible on explicit human override... every exception itself a receipt."

The 2026-08-20 data-loss guard (#881) is currently prompt-level only (open_source_prompt.md). This PR makes it structural in two layers:

Changes

  1. emrg/server/scheduler.py — per-cycle effective sandbox guard:

    • New TaskHandler._is_dirty_tree_sync(): probes git status --porcelain in the task's source dir (local subprocess import keeps the module's no-subprocess invariant, rant 2026-08-19T14:20:52; run via asyncio.to_thread so the event loop is never blocked). Fail-open: non-git dirs / git errors are treated as not dirty.
    • New TaskHandler._effective_sandbox() (async): when the source dir has uncommitted changes, the cycle's bash sandbox is forced to read-only regardless of task config — the host's live edits are structurally out of reach, not merely discouraged.
    • Audited override: env var EMRG_TASK_DIRTY_OVERRIDE (comma-separated task names, or *) lets a human lift the guard; every override is logged with logger.warning as a receipt.
    • The cycle's task message now sends await self._effective_sandbox() instead of the static configured tier.
  2. emrg/tools/bash_tool.py — read-only blocks git mutators:

    • The read-only tier previously blocked only rm -r / rmdir / mv / cp -r plus shell redirects — the actual 2026-08-20 killers (git stash, git checkout ., git reset --hard, git clean) escaped the target scan entirely.
    • Read-only now also blocks git mutating commands: stash / checkout / restore / clean / reset / commit / push / pull / merge / rebase / cherry-pick / revert / rm / mv / switch / branch -d/-D / tag -d.
    • Read-only git reads (status / fetch / log / diff / show / branch -a / remote -v) stay allowed — the read-only cycle still scans and reviews.
  3. Tests (8 new):

    • test_bash_tool_sandbox.py: git mutators blocked under read-only (positive), git reads allowed (negative), workspace-write keeps git mutation enabled (the normal working tier).
    • test_scheduler.py: dirty-tree detection (clean repo vs untracked file vs non-git dir), dirty tree forces read-only (incl. configured read-only not weakened), EMRG_TASK_DIRTY_OVERRIDE receipt semantics (named task / wildcard / other-task), clean tree keeps configured tier.

Verification

  • Local pytest: 1071 passed, 1 skipped (8 new tests; Agent.md doc-count guard synced 1064 to 1072)
  • Import and CLI checks OK
  • Module-level no-subprocess invariant preserved (existing test test_saturation_heartbeat_makes_no_network_calls passes)

Notes

@argszeroargszero left a comment

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

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

✅ LGTM — cycle 20260825-192404. CI green (test + test-windows). Fresh review of da24f61: (1) scheduler guard fail-open by design (non-git dir → not dirty), runs via asyncio.to_thread so the event loop is never blocked, local subprocess import preserves the module invariant; EMRG_TASK_DIRTY_OVERRIDE receipt semantics correct (named task / wildcard / other-task). (2) bash_tool read-only blocks git mutators while allowing read-only git reads — the 08-20 killers are now structurally blocked, not just discouraged. 8 regression tests + Agent.md count sync. Full local suite 1071 passed/1 skipped.

@argszero

Copy link
Copy Markdown
OwnerAuthor

Extended the guard (new head 6f06e4b): the read-only tier now also covers the write and edit tools, not just bash.

  • emrg/tools/bash_tool.py: new shared helper check_read_only_file_write(path, workspace) — blocks writes inside the task workspace (the host's tree) or to protected daemon files; allows outside-workspace writes (memory dir, logs) so a read-only cycle can still Record.
  • emrg/tools/write_tool.py / emrg/tools/edit_tool.py: when sandbox == "read-only", refuse file mutation inside the workspace.
  • emrg/server/daemon.py: inject sandbox + workspace (session cwd) into write/edit args, mirroring the existing bash injection.
  • Tests: +6 (write/edit × blocked-inside-workspace / allowed-outside / no-sandbox unchanged). Full suite 1077 passed, 1 skipped; Agent.md count synced 1072 → 1078.

Rationale: the 08-20 incident was about the agent mutating the host's uncommitted tree — bash-only coverage left a hole where a read-only task could still overwrite host files via write/edit without touching git. Now all three file-mutating tools are structurally read-only in a dirty tree.

@argszeroargszero left a comment

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

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

✅ LGTM — cycle 20260825-193548 (LGTM 2/3: 192404, 193548; no ❌ between). Reviewed new head 6f06e4b (write/edit read-only extension): check_read_only_file_write correctly blocks workspace-internal writes (host's protected tree) and protected daemon files while allowing outside-workspace writes so a read-only cycle can still Record; daemon injection of sandbox + workspace mirrors the existing bash pattern; write/edit gate before any file mutation. 6 regression tests cover blocked-inside / allowed-outside / no-sandbox unchanged. CI green on the new head (test + test-windows). Local suite 1077 passed/1 skipped.

@argszero

Copy link
Copy Markdown
OwnerAuthor

Further hardened the read-only tier (new head 2d275ab): the git-mutator block now also covers other working-tree writers — git apply, git am, git archive, git submodule update, git worktree add — not just the incident commands. A read-only cycle can no longer modify the host's uncommitted tree through patch application or submodule/worktree operations. Extended the existing blocked-mutator test cases accordingly; full suite stays at 1077 passed / 1 skipped (test count unchanged).

@argszeroargszero left a comment

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

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

✅ LGTM — cycle 20260825-194513 (3rd consecutive LGTM: 192404, 193548, 194513; no ❌ between). Reviewed head 2d275ab: git-mutator block extended to working-tree writers (apply/am/archive/submodule/worktree) — closes the remaining hole where a read-only cycle could still mutate the host tree via patch application or submodule/worktree ops; 5 new test cases; CI green on this head (test + test-windows, run 32843417822). Merge condition met.

@argszero
argszero merged commit 836fb29 into masterAug 25, 2026
2 checks passed
argszero added a commit that referenced this pull request Aug 25, 2026
…ard, GUI session fix, pid/port deprecation, structural dirty-tree guard) (#982)
* emrg: bump version to v0.2.79 (release: #976-#980 — plist-codesign guard, GUI session fix, pid/port deprecation, structural dirty-tree guard)
* emrg: drop stray PR-body artifacts from release branch
---------
Co-authored-by: EMRG Evolution <emrg@argszero.dev>
argszero added a commit that referenced this pull request Aug 25, 2026
… mutator block (regression in #980) (#983)
Co-authored-by: argszero <argszero@gmail.com>
Sign up for freeto 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

@argszero