Skip to content

fix(agent-isolation): keep the touch overlay's state out of /tmp - #1306

Merged
potiuk merged 1 commit into
mainfrom
fix/gpg-touch-runtime-dir
Sep 21, 2026
Merged

potiuk merged 1 commit into
mainfrom
fix/gpg-touch-runtime-dir

Conversation

@potiuk

@potiuk potiuk commented Sep 21, 2026

Copy link
Copy Markdown
Member

Summary

  • The touch overlay kept its runtime state in /tmp. RUNTIME_DIR fell back to /tmp/magpie-gpg-touch wherever XDG_RUNTIME_DIR is unset, which on macOS is always. /tmp is world-writable, so another local user can pre-create the directory and sit on the pid files and the lock the window is leased through.
  • That also broke signed commits under our own sandbox. /tmp is outside the reference allowWrite, so the wrapper cannot create its pid file, no watcher starts, nothing puts a window on screen, the key is never touched, and gpg-agent gives up with agent refused operation. The error names the watcher, not the key, which is what makes it read like broken signing rather than a denial.
  • The fallback is now ${XDG_CACHE_HOME:-$HOME/.cache}/magpie-gpg-touch. XDG_RUNTIME_DIR is still preferred where the platform provides it.

Observed while committing #1304: every pre-commit hook passed and the commit then died at the signature.

error: .../gpg-touch-wrap-ssh-keygen: line 340: /tmp/magpie-gpg-touch/watcher.pid: Operation not permitted
Couldn't sign message (signer): agent refused operation?
fatal: failed to write commit object

Type of change

  • Skill change (.claude/skills/<name>/) — eval fixtures updated below
  • Tool / bridge contract (tools/<system>/*.md)
  • Python package (tools/*/ with pyproject.toml)
  • Groovy reference impl
  • Cross-cutting (RFC, AGENTS.md, sandbox, privacy-LLM)
  • Documentation (docs/, README.md, CONTRIBUTING.md)
  • Project template (projects/_template/)
  • CI / dev loop (prek, workflows, validators)
  • Other: shell tool (tools/agent-isolation/*.sh) + its pytest suite

Test plan

  • prek run --all-files passes
  • For Python packages touched: uv run pytest / ruff check / mypy passes
  • For Groovy bridges touched: command-line invocation tested end-to-end
  • For skill changes: eval suite passes for the affected skill
  • For skill behaviour changes: a new or updated eval fixture is included in this PR
  • Other: four new tests — three pinning the resolution order (XDG_RUNTIME_DIR wins, the fallback is under $HOME and never bare /tmp, XDG_CACHE_HOME is honoured) and one pinning the hint-hook routing.

RFC-AI-0004 compliance

  • HITL — any new mutation is gated on explicit user confirmation
  • Sandbox — no new unrestricted host access; network reach declared in the adapter
  • Vendor neutrality — placeholders used in all skill / tool prose
  • Conversational + correctable — agentic-override path documented if behaviour is adopter-tunable
  • Write-access discipline — no autonomous outbound messages; drafts only, sent on confirmation
  • Privacy LLM — private content does not reach a non-approved LLM; redactor invoked where needed

Nothing widens. The change moves state out of a world-writable directory and into one already in the reference allowWrite, so a signed commit stops needing a sandbox bypass rather than being granted one.

Linked issues

Refs #1304 — found while committing it. Same family as the TMPDIR and unix:// defects fixed there: a path assumed available that the environment does not actually provide.

Notes for reviewers

Why not $TMPDIR. It is per-user and writable, so it looks like the obvious answer, and it is wrong here. It differs between the signing contexts that have to find one another — the agent's hooks see the harness's scratch directory, a terminal git sees the login one. Two contexts computing two runtime directories cannot share an owners registry or a window lease, and that sharing is the whole design of the ownership model added in #1301. The cache directory is stable for a user whatever spawned the process, and everything kept there is regenerable: pid files swept by the next arm, a lease reclaimed when its holder dies.

XDG semantics. Runtime state in a cache directory is a compromise. On Linux, where XDG_RUNTIME_DIR exists, nothing changes and the correct directory is still used. The cache fallback only applies to platforms that provide no runtime directory at all, and the contents genuinely are regenerable.

The hint hook was mis-routing this failure. Its first branch matches agent refused operation, so it claimed this error and pointed the reader at "SSH agent / Yubikey appears unreachable" — the wrong entry, since the key is fine and the watcher is what never started. The new branch is deliberately ordered ahead of it, and a test asserts both that the right anchor appears and that the ssh-agent one does not.

Test seam. _runtime_dir follows the existing _signing_in_flight / _agent_sockets / _lease-probe seams. I tried asserting via arm first; it needs a GUI and a JSON payload, and on a host without Tk it returns early having created nothing, so it cannot observe the path.

A probe is deliberately missing. The catalog says a new entry should also grow a doctor probe. I did not add one: a seventh probe cascades into the skill's ## The 6 probes heading, its frontmatter description, the eval output spec, every fixture and the case counts — and the failure is now fixed at source, so the probe would only catch a stale install. Happy to add it if you'd rather the catalog and the doctor stay strictly in lock-step.

Your installed copy is behind. This fixes the framework; an operator's ~/.claude/scripts/ copy keeps the old fallback until they update it, so signed commits there keep needing a bypass. Nothing in this PR touches an installed copy.

🤖 Generated with Claude Code

`RUNTIME_DIR` fell back to `/tmp/magpie-gpg-touch` wherever
`XDG_RUNTIME_DIR` is unset, which on macOS is always. That is wrong
twice over.

`/tmp` is world-writable, so another local user can pre-create the
directory and sit on the pid files and the lock the window is leased
through. And it is outside the write set of the sandbox this framework
ships, so a signed commit from a sandboxed agent dies at

    gpg-touch-wrap-ssh-keygen: line 340:
        /tmp/magpie-gpg-touch/watcher.pid: Operation not permitted
    Couldn't sign message (signer): agent refused operation?

before any watcher starts: no window, no touch, and gpg-agent gives up.
The error names the watcher rather than the key, which is what makes it
read like broken signing rather than a denial.

The fallback is now `${XDG_CACHE_HOME:-$HOME/.cache}/magpie-gpg-touch`.
`$TMPDIR` would have been the obvious per-user answer and is the wrong
one: it differs between the signing contexts that have to find each
other — the agent's hooks see the harness's scratch directory, a
terminal `git` sees the login one — and two contexts computing two
runtime directories cannot share an owners registry or a window lease,
which is the whole design. The cache directory is stable for a user
whatever spawned the process, and everything kept there is regenerable.

The hint hook needed a branch of its own, ahead of the ssh-agent one:
this failure also ends in `agent refused operation`, so that branch
claimed it and sent the reader to the entry about an unreachable key
when the key was never the problem.

Adds a `_runtime_dir` test seam in the idiom of the existing ones, so
the three new tests assert the script's own value rather than a copy of
the expression under test.

Generated-by: Claude Opus 5
@potiuk
potiuk merged commit bf09647 into main Sep 21, 2026
9 checks passed
@potiuk
potiuk deleted the fix/gpg-touch-runtime-dir branch September 21, 2026 01:01
potiuk added a commit to potiuk/magpie that referenced this pull request Sep 21, 2026
The marker sat at 0e7012b, where apache#1306 left it, while apache#1305 and
apache#1307-apache#1310 landed on top. Reviewing that delta:

- apache#1305 (stats events cache `updatedAt`) and apache#1307 (dev version bump)
  touch no behaviour any spec describes; no spec references
  `fetch_issues` or the events cache.
- apache#1308, apache#1309 and apache#1310 are the vetted-ops sandbox work, and the
  `excludedCommands` section this branch already rewrites is where
  they belong. That rewrite covers all three.

So the delta is documented and the marker can move. This is the
by-hand route AGENTS.md allows for a small known gap, not a full
`loop.sh update` pass — it reconciles these five commits rather than
re-auditing every skill, tool and modes.md row.

Generated-by: Claude Opus 5
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