Skip to content

fix(agent-isolation): raise the touch overlay for key users other than git - #1314

Merged
potiuk merged 1 commit into
apache:mainfrom
potiuk:feat/overlay-non-git-key-consumers
Sep 21, 2026
Merged

potiuk merged 1 commit into
apache:mainfrom
potiuk:feat/overlay-non-git-key-consumers

Conversation

@potiuk

@potiuk potiuk commented Sep 21, 2026

Copy link
Copy Markdown
Member

Summary

  • The overlay armed only for git <subcommand>. Anything that reaches the security key without git in front of it — ssh, scp, sftp, rsync -e ssh, svn+ssh, a direct gpg --detach-sign, ssh-keygen -Y sign — blocked for its touch with nothing on screen. A new KEY_COMMANDS matcher covers them under the same whole-command shape rule as the git list.
  • A shim directory extends wrap past git. Git can be told which program to call (gpg.ssh.program, core.sshCommand); a bare ssh you type cannot, so only PATH can put the wrapper in front of it. The script now dispatches on its own basename for a key command's name, not only for gpg-touch-wrap-<program>. Opt-in, and the PATH line is printed for the operator rather than written into their rc.
  • Fixes a latent recursion bug this surfaced. command -v -a is not valid bash (-a belongs to type), so wrap's "find the real program behind me" loop never saw a candidate and every call fell through to exec'ing the bare name. Harmless while the script was never on PATH; with a shim it re-execs itself indefinitely — hung terminal, no window, climbing load average. Now type -aP, and exit 127 instead of falling back to the name.

Arming stays deliberately over-broad: the watcher shows no window until something has actually blocked on the key for longer than SHOW_DELAY, so a false-positive arm costs one short-lived background process and nothing visible. That is also why no "is a YubiKey configured?" probe was added — a machine with no touch-required key never reaches the window, and a probe would add a dependency, per-call latency and a cache that goes stale on replug, in exchange for a false-negative risk (a silent block with no window), which is the expensive direction.

No double overlay. The self-skip in the program lookup is load-bearing twice: a wrapped git does not chain into a shim (one connection, one wrapper), and the lookup must fail rather than fall back to the bare name. The pre-existing guarantees still hold and are still tested — CLAUDECODE=1 makes the wrapper stand aside inside an agent session, and across contexts the window is leased by atomic directory create with reclaim from a dead holder.

Type of change

  • Skill change — isolated-setup-install / -verify / -update (no eval-fixture change needed; see Test plan)
  • Tool / bridge contract (tools/<system>/*.md)
  • Python package (tools/*/ with pyproject.toml) — tools/agent-isolation tests
  • 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)

Test plan

  • prek run --all-files passes (32 hooks, exit 0)
  • uv run pytest tools/agent-isolation/tests/ — 233 passed, 7 skipped
  • TDD throughout: the 16 non-git matcher cases, the 4 shim-dispatch cases, the recursion guard and the no-chaining case were each written first and watched fail. The recursion guard was verified by stashing the fix and re-running.
  • New negatives pin the shape rule: cat ~/.ssh/config, ls -la ~/.ssh, sshuttle … and ssh-agent -s must not arm.
  • Every new shim test carries timeout=30, so a regression shows up as a failure rather than a hung suite — this bug originally presented as two pytest runs stuck for 3.5 hours.
  • Skill eval suites for the three isolated-setup-* skills: not re-run. The --cli harness cannot authenticate a nested claude -p from inside the sandbox. The verify change is a sub-check (10e) under existing check 10, so the graded schema (n: 1-10) is unchanged; the install and update edits are additive prose. Happy to run them outside the sandbox if you want them before merge.

RFC-AI-0004 compliance

  • HITL — the shim directory is offered, not installed by default, and the PATH line is printed for the operator; the install skill still never edits a shell rc.
  • Sandbox — no new grant. Shims resolve to gpg-touch-overlay.sh, already covered by the K.4 allowRead entry, and the sandbox checks the resolved path.

Linked issues

None.

Notes for reviewers (optional)

Two things worth a second opinion:

  1. Breadth of KEY_COMMANDS. It matches a bare word in command position or after any whitespace, following the existing git matcher's shape rule, so man ssh arms too. That is consistent with the file's stated bias ("a false positive costs one short-lived watcher, a false negative costs a silent block with no window"), but it is a judgement call — tightening to command position only would drop sudo ssh, env … ssh and time ssh.
  2. Pre-existing inconsistency I did not touch: tools/skill-evals/evals/setup-isolated-setup-verify/step-1-classify/fixtures/output-spec.md still says n=1 through n=10 at most, but the skill has had a check 11 since feat(agent-isolation): hint when gh ran inside the sandbox and catalogue the TLS -26276 shape #1289. Out of scope here; flagging it rather than folding an unrelated fix in.

🤖 Generated with Claude Code

…n git

The hook armed only on `git <subcommand>`, so anything reaching the
security key without git in front of it — `ssh`, `scp`, `sftp`,
`rsync -e ssh`, `svn+ssh`, a direct `gpg --detach-sign` — blocked for
its touch with nothing on screen. Add those under the same
whole-command shape rule. Arming stays deliberately broad: the watcher
shows no window until something has actually blocked on the key for
longer than the grace, so a false positive costs one short-lived
background process and nothing visible.

`wrap` reaches the same commands through an optional shim directory on
PATH. Git can be told which program to call; a bare `ssh` cannot, so
only PATH can put the wrapper in front of it. The script now dispatches
on its own basename for a key command's name, not only for
`gpg-touch-wrap-<program>`.

That surfaced a latent bug in the lookup for the real program.
`command -v -a` is not valid bash — `-a` belongs to `type` — so the
loop never saw a candidate and every call fell through to exec'ing the
bare name, relying on PATH to resolve it. Harmless while the script was
never on PATH; with a shim it re-execs itself indefinitely, leaving a
hung terminal, no window, and a load average that climbs until someone
goes looking. Use `type -aP`, and exit 127 rather than falling back to
the name. That same self-skip is what keeps a wrapped git from chaining
into a shim, so one connection still gets one wrapper and one window.

Generated-by: Claude Opus 5
@potiuk potiuk added the family:setup setup-* skills label Sep 21, 2026
@potiuk
potiuk merged commit 9f23f2d into apache:main Sep 21, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

family:setup setup-* skills

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant