Files
Michael Ramos 7682628db7 feat(install): Codex opt-out and credential-free attestation verification (#1197)
* feat(install): Codex opt-out and credential-free attestation verification

Implements both asks from #1178 (reported and designed by @astradevkin):

- Per-agent installer opt-outs: --skip-codex / --skip-gemini / --skip-kiro
  flags, PLANNOTATOR_SKIP_{CODEX,GEMINI,KIRO}_INSTALL env vars, and
  config.json skipInstall.{codex,gemini,kiro} keys, with flag > env >
  config precedence mirroring verifyAttestation. Detected-but-skipped is
  reported as its own honest state, never conflated with not-detected,
  and a skipped agent's home is neither written nor cleaned up.

- Credential-free provenance verification: when --verify-attestation is
  active, the Sigstore bundle is fetched from GitHub's public
  attestations endpoint (single unauthenticated attempt, no retry) and
  verified via gh attestation verify --bundle with the same
  --repo/--source-ref/--signer-workflow constraints; gh's authenticated
  fetch remains the fallback. TUF trust-root failures are reported as
  connectivity, distinct from real provenance failures; every path
  stays fail-closed.

Zero behavior change for users who do not opt in: the default install
path is unchanged (verified by sandbox-HOME parity runs against main).

* fix(install): address #1197 review round (H1 retry, M2-M7, lows)

- H1: a failed gh --bundle invocation now retries once through the exact
  authenticated path before any classification, so an older gh (unknown
  flag) or a corrupt bundle never reports as a provenance failure. Pinned
  by a functional stub-gh test; a real failure still fails again on the
  retry and aborts.
- M2: the sh config layer extracts the skipInstall object (awk, character
  indexed) before matching per-agent keys and honors explicit false as a
  veto; cmd now parses the real JSON via PowerShell like ps1. Functional
  tests cover the foreign-key collision and explicit-true cases.
- M3: sh names the real cause when the bundle path cannot run (no JSON
  extractor vs fetch vs extraction failure) and gains python3 and jq
  fallback extractors; docs state the dependency.
- M4: ps1/cmd gate the existing-integration note on plannotator content in
  hooks.json and word the skip state as what those platforms actually do
  (manual instructions suppressed).
- M5: sh bundle lives inside a private mktemp -d, one rm -rf on every exit,
  and a mktemp failure degrades to the fallback instead of aborting.
- M6: README, verifying-your-install, environment-variables, and
  installation docs updated for the credential-free path and skip flags.
- M7: ps1/cmd extraction replaced with a byte-exact string scanner (no
  ConvertFrom/ConvertTo round trip, immune to DateTime coercion), with
  PowerShell-driven unit tests over the captured real attestations
  response plus synthetic DateTime and brace-in-string controls.
- Lows: fail-closed abort pinned by a functional test; TUF beats auth in
  cmd classification (matches sh/ps1); skipped-state output mentions the
  shared ~/.agents/skills; Gemini summary is skip-aware and gains an
  honest not-detected state; --skip-opencode do-not-write switch added
  (flag, env var, config key) across all three scripts.

* fix(install): review round three (EncodedCommand fetcher, mutation-proof fail-closed test, lows)

- R1: install.cmd's attestation fetcher no longer touches disk. The
  %RANDOM%-named %TEMP% .ps1 (predictable-path code execution, the M5
  class escalated) is replaced by powershell -NoProfile -EncodedCommand
  with a base64(UTF-16LE) payload defined next to its full REM PS:
  plaintext; a test decodes the blob and asserts byte equality with the
  documented lines plus the security-relevant shape (env-var inputs,
  ordinal scan, no JSON round trip, distinct exit codes). Inputs still
  travel via env vars. Verified end to end under pwsh: the decoded blob
  fetched the real attestations response, wrote 2 bundles, gh verified
  the real v0.25.1 binary credential-free (exit 0) and rejected a wrong
  binary (exit 1).
- R2: the fail-closed test now asserts the output ENDS with 'Refusing to
  install.' - mutation-verified: with the verify-failure exit 1 deleted
  the mutant still exits 1 via an incidental mv failure, but the trailing
  mv error breaks the endsWith and the test fails; restored, it passes.
- Lows: CI guard test fails loudly when process.env.CI is set and no
  pwsh/powershell is on PATH (scanner coverage cannot silently vanish);
  scanner IndexOf calls are ordinal in ps1 and the encoded cmd variant;
  the awk skipInstall extraction requires optional-whitespace-then-colon-
  then-brace after the key (string values can no longer anchor it, with
  non-token occurrences skipped, unit-checked against escaped-embedded
  payloads); install.cmd comments warn that the fallback-reason literals
  inside parenthesized blocks must stay parenthesis-free.
2026-08-04 13:17:27 -07:00
..