Global gpg.program points at a purged native-session path, so gpg-ssm cannot sign anything #1137

Closed
opened 2026-08-19 01:56:39 +00:00 by coilyco-ops · 1 comment
Owner

Filed by Saiya (exec seat), 2026-08-19, at Kai's direction. Read-only inspection, nothing changed.

Found while pulling coilyco-bridge/agentic-os-kai and applying the lore-stack-git-pull-verification policy. The policy's signature layer turns out to be inert, and the reason is a stale path rather than a missing tool.

Finding 1: the configured signer does not exist

Read from git config on this host:

commit.gpgsign   global = false
gpg.program      global = /var/folders/.../T/aos/native/<session>/home/.local/bin/gpg-ssm
user.signingkey  global = (set)

The gpg.program value is a native-session temporary home from 2026-08-06, and that temp root has since been purged. Confirmed absent and not executable.

The real gpg-ssm is present and correctly installed:

/Users/kai/.local/bin/gpg-ssm -> agentic-os/scripts/gpg-ssm

That is exactly the wire-up the tooling-gpg-ssm skill prescribes, git config --global gpg.program "$HOME/.local/bin/gpg-ssm". So the tool is fine. The pointer is wrong.

Consequence: signing is currently disabled, and if anyone flips commit.gpgsign true today it fails, because the program it would invoke is a dead path. The fix looks like it would work and would not.

Why this matters beyond one setting

lore-stack-git-pull-verification says only pull commits that are GPG-signed and authored by the expected author, and calls the signature check "the cheapest layer that closes the gap" because it depends only on the local keyring rather than on the remote being trustworthy.

Measured just now, last 50 commits in each:

  • coilyco-bridge/agentic-os-kai - 50 of 50 unsigned
  • coilyco-flight-deck/agentic-os - 50 of 50 unsigned

So the signature half of that policy is verifying nothing anywhere, and the author check is carrying the whole policy alone. That is worth knowing rather than assuming the layer is present. It is highest-stakes on repos that auto-deploy on push to main, where a poisoned merge becomes a deploy with no further gate.

The mechanism worth fixing, not just the value

A native session wrote its own session-scoped home path into global config, where it outlived the session that created it. That is the same hazard class the session-shadow rule already names: a session-scoped path is durable enough to be written down and not durable enough to be relied on.

Fixing the one value restores signing. It does not stop the next session doing the same thing. Candidate directions, none of them decided here:

  • Make the global wire-up always resolve through $HOME/.local/bin/gpg-ssm and treat any /aos/native/ path in gpg.program as a defect to correct rather than a value to preserve.
  • Have gpg-ssm or the shell base detect that gpg.program points outside $HOME and say so, since a dead signer currently fails silently by being switched off.
  • Add it to whatever already checks host hygiene, so drift surfaces without someone reading git config by hand.

Finding 2: ssm-get is a shell function, so tooling cannot call it

ssm-get is defined at shell/common.sh:484 as a function, not a binary on PATH. It works as designed in an interactive zsh or bash session and is unavailable to any non-interactive shell, script, or agent tool call.

This surfaced when writing a one-off command for Kai to run: ssm-get was the natural helper to reach for and it resolved to nothing, so the command fell back to a raw aws ssm get-parameter --with-decryption invocation.

This may be entirely intentional and I am not claiming it is a defect. Recording it because the consequence is real: any command an agent hands a human, or any script in this repo, cannot use ssm-get and has to restate the raw AWS call, which is exactly the duplication the single-owning-source convention exists to prevent. If a callable form is wanted, a thin scripts/ssm-get calling the same logic would give both surfaces one implementation.

What would settle each

  1. git config --global gpg.program resolves to an executable, and a test commit signs, verified by %G? returning G rather than N.
  2. A decision on whether commit.gpgsign should be true by default on this fleet, which is Kai's call and not assumed here.
  3. A stated answer on whether ssm-get is deliberately interactive-only.

Boundaries

  • Authoring belongs here. gpg-ssm and shell/common.sh both live in this repo.
  • Fleet convergence belongs to infrastructure/ansible, per the authoring-versus-rollout contract. The ansible shell role already owns the ~/.local/bin/gpg-ssm symlink, so correcting drift across hosts is a rollout concern rather than a change here.
  • Not urgent tonight. This is hours from the August 19 livestream and touches nothing on that path. It has been broken since at least 2026-08-06 and can wait for a calm day.
  • tooling-gpg-ssm - the wrapper, its wire-up, and the debugging notes.
  • lore-stack-git-pull-verification - the policy this weakens.
**Filed by Saiya (exec seat), 2026-08-19, at Kai's direction. Read-only inspection, nothing changed.** Found while pulling `coilyco-bridge/agentic-os-kai` and applying the `lore-stack-git-pull-verification` policy. The policy's signature layer turns out to be inert, and the reason is a stale path rather than a missing tool. ## Finding 1: the configured signer does not exist Read from git config on this host: ``` commit.gpgsign global = false gpg.program global = /var/folders/.../T/aos/native/<session>/home/.local/bin/gpg-ssm user.signingkey global = (set) ``` The `gpg.program` value is a **native-session temporary home from 2026-08-06**, and that temp root has since been purged. Confirmed absent and not executable. The real `gpg-ssm` is present and correctly installed: ``` /Users/kai/.local/bin/gpg-ssm -> agentic-os/scripts/gpg-ssm ``` That is exactly the wire-up the `tooling-gpg-ssm` skill prescribes, `git config --global gpg.program "$HOME/.local/bin/gpg-ssm"`. So the tool is fine. The pointer is wrong. **Consequence:** signing is currently disabled, and if anyone flips `commit.gpgsign true` today it fails, because the program it would invoke is a dead path. The fix looks like it would work and would not. ## Why this matters beyond one setting `lore-stack-git-pull-verification` says only pull commits that are GPG-signed and authored by the expected author, and calls the signature check "the cheapest layer that closes the gap" because it depends only on the local keyring rather than on the remote being trustworthy. Measured just now, last 50 commits in each: * `coilyco-bridge/agentic-os-kai` - **50 of 50 unsigned** * `coilyco-flight-deck/agentic-os` - **50 of 50 unsigned** So the signature half of that policy is verifying nothing anywhere, and the author check is carrying the whole policy alone. That is worth knowing rather than assuming the layer is present. It is highest-stakes on repos that auto-deploy on push to `main`, where a poisoned merge becomes a deploy with no further gate. ## The mechanism worth fixing, not just the value A native session wrote **its own session-scoped home path into global config**, where it outlived the session that created it. That is the same hazard class the session-shadow rule already names: a session-scoped path is durable enough to be written down and not durable enough to be relied on. Fixing the one value restores signing. It does not stop the next session doing the same thing. Candidate directions, none of them decided here: * Make the global wire-up always resolve through `$HOME/.local/bin/gpg-ssm` and treat any `/aos/native/` path in `gpg.program` as a defect to correct rather than a value to preserve. * Have `gpg-ssm` or the shell base detect that `gpg.program` points outside `$HOME` and say so, since a dead signer currently fails silently by being switched off. * Add it to whatever already checks host hygiene, so drift surfaces without someone reading git config by hand. ## Finding 2: ssm-get is a shell function, so tooling cannot call it `ssm-get` is defined at `shell/common.sh:484` as a function, not a binary on `PATH`. It works as designed in an interactive zsh or bash session and is unavailable to any non-interactive shell, script, or agent tool call. This surfaced when writing a one-off command for Kai to run: `ssm-get` was the natural helper to reach for and it resolved to nothing, so the command fell back to a raw `aws ssm get-parameter --with-decryption` invocation. **This may be entirely intentional** and I am not claiming it is a defect. Recording it because the consequence is real: any command an agent hands a human, or any script in this repo, cannot use `ssm-get` and has to restate the raw AWS call, which is exactly the duplication the single-owning-source convention exists to prevent. If a callable form is wanted, a thin `scripts/ssm-get` calling the same logic would give both surfaces one implementation. ## What would settle each 1. `git config --global gpg.program` resolves to an executable, and a test commit signs, verified by `%G?` returning `G` rather than `N`. 2. A decision on whether `commit.gpgsign` should be `true` by default on this fleet, which is Kai's call and not assumed here. 3. A stated answer on whether `ssm-get` is deliberately interactive-only. ## Boundaries * **Authoring belongs here.** `gpg-ssm` and `shell/common.sh` both live in this repo. * **Fleet convergence belongs to infrastructure/ansible**, per the authoring-versus-rollout contract. The ansible shell role already owns the `~/.local/bin/gpg-ssm` symlink, so correcting drift across hosts is a rollout concern rather than a change here. * **Not urgent tonight.** This is hours from the August 19 livestream and touches nothing on that path. It has been broken since at least 2026-08-06 and can wait for a calm day. ## Related * `tooling-gpg-ssm` - the wrapper, its wire-up, and the debugging notes. * `lore-stack-git-pull-verification` - the policy this weakens.
Author
Owner

Settling condition 1 is met, and the mechanism behind it is closed. Landed in #1306.

The value, on kais-macbook-pro. gpg.program was still the purged .../aos/native/20260806t051414z-53486-47e39f32/home/.local/bin/gpg-ssm, confirmed non-executable. It now reads /Users/kai/.local/bin/gpg-ssm, and a probe commit returns %G? %GK = G D5742C052C5501A0. Signing works.

The mechanism. The cause is one layer more specific than the issue could see from outside: a native session home symlinks .gitconfig to the durable one while $HOME itself is session-scoped, so git config --global gpg.program "$HOME/.local/bin/gpg-ssm" writes an ephemeral path into durable config every time it is run from a session. The documented wire-up was the trap.

The same trap sat one layer down and was not in the report: apply-shell-links.py derives its link sources from its own checkout, so running it from a shadow worktree would have pointed ~/.local/bin/gpg-ssm at a path that dies with the session. Same defect, one indirection earlier.

scripts/apply-shell-links.py now owns gpg.program alongside the symlink it names, resolves the durable host home rather than $HOME, refuses to run from a native-session checkout, and reports both as drift under just apply-shell-links -- --check. That is candidate directions one and three from the report, taken together. Direction two, having gpg-ssm self-check, is not viable: a dead gpg.program means the wrapper never runs, so the detector cannot live inside it.

docs/install.md, the tooling-gpg-ssm skill, its debugging notes, and the justfile comment all move to just apply-shell-links as the wire-up, so there is no hand-run git config left to get wrong.

Verified: uv run pytest tests/ 730 passed, uv run pre-commit run --all-files clean, and --check from the canonical checkout flagged the purged path before the fix and is quiet after.

Not done here, and now tracked rather than buried. Settling conditions 2 and 3 are decisions rather than work, and closing this issue on condition 1 would have taken them with it:

  • #1307 - whether commit.gpgsign should default to true on this fleet, priced with what it costs the bot seats and expired-SSO hosts.
  • #1308 - whether ssm-get is deliberately interactive-only, or wants a callable scripts/ssm-get.

Fleet convergence of the corrected value stays with infrastructure/ansible per the authoring-versus-rollout contract. Only this host was fixed directly.

Settling condition 1 is met, and the mechanism behind it is closed. Landed in #1306. **The value, on kais-macbook-pro.** `gpg.program` was still the purged `.../aos/native/20260806t051414z-53486-47e39f32/home/.local/bin/gpg-ssm`, confirmed non-executable. It now reads `/Users/kai/.local/bin/gpg-ssm`, and a probe commit returns `%G? %GK` = `G D5742C052C5501A0`. Signing works. **The mechanism.** The cause is one layer more specific than the issue could see from outside: a native session home symlinks `.gitconfig` to the durable one while `$HOME` itself is session-scoped, so `git config --global gpg.program "$HOME/.local/bin/gpg-ssm"` writes an ephemeral path into durable config every time it is run from a session. The documented wire-up was the trap. The same trap sat one layer down and was not in the report: `apply-shell-links.py` derives its link sources from its own checkout, so running it from a shadow worktree would have pointed `~/.local/bin/gpg-ssm` at a path that dies with the session. Same defect, one indirection earlier. `scripts/apply-shell-links.py` now owns `gpg.program` alongside the symlink it names, resolves the durable host home rather than `$HOME`, refuses to run from a native-session checkout, and reports both as drift under `just apply-shell-links -- --check`. That is candidate directions one and three from the report, taken together. Direction two, having `gpg-ssm` self-check, is not viable: a dead `gpg.program` means the wrapper never runs, so the detector cannot live inside it. `docs/install.md`, the `tooling-gpg-ssm` skill, its debugging notes, and the justfile comment all move to `just apply-shell-links` as the wire-up, so there is no hand-run `git config` left to get wrong. Verified: `uv run pytest tests/` 730 passed, `uv run pre-commit run --all-files` clean, and `--check` from the canonical checkout flagged the purged path before the fix and is quiet after. **Not done here, and now tracked rather than buried.** Settling conditions 2 and 3 are decisions rather than work, and closing this issue on condition 1 would have taken them with it: * #1307 - whether `commit.gpgsign` should default to `true` on this fleet, priced with what it costs the bot seats and expired-SSO hosts. * #1308 - whether `ssm-get` is deliberately interactive-only, or wants a callable `scripts/ssm-get`. Fleet convergence of the corrected value stays with infrastructure/ansible per the authoring-versus-rollout contract. Only this host was fixed directly.
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/agentic-os#1137
No description provided.