feat(secrets): tps secrets rotate-github-pat + list-github-pats (ops-njgl)#274
feat(secrets): tps secrets rotate-github-pat + list-github-pats (ops-njgl)#274
Conversation
…njgl) Adds two new actions under `tps secrets` for GitHub PAT lifecycle: tps secrets rotate-github-pat <agent> tps secrets list-github-pats ## Why Today's rotation experience: 6 PATs to rotate manually (GitHub UI clicks + 6 file writes + 6 chmods + 6 verifies). 30+ min per cycle, friction tempts operators to extend PAT expiry past 30 days. The right answer is to automate rotation, not extend the cadence (per ops-njgl). ## Design properties - **Token never on argv.** Reads stdin only — pipe (`cat new-pat | tps secrets rotate-github-pat anvil`) or interactive silent-read prompt. - **Pre-validate shape** before any network or filesystem touch (length, GitHub prefix, no whitespace/quotes/backticks). - **HTTP 200 probe BEFORE overwriting.** Failed probe leaves existing PAT untouched. - **Atomic write** for file-based PATs (tmp + rename + chmod 600). - **Keyring path for flint** — pipes to `gh-as flint auth login --with-token` instead of writing a file (gh-as resolves flint via macOS Keychain). - **Post-rotation verify** via probe or gh-as. - **Sibling list-github-pats** probes every secret file + keyring agent in one pass — replaces the inline curl loop ops have been running by hand. ## Files - packages/cli/src/commands/pat-rotate.ts — implementation, exports normalizeToken + validateTokenShape + probeToken for testability - packages/cli/bin/tps.ts — wires the two new actions into the secrets case alongside set/list/remove - packages/cli/test/pat-rotate.test.ts — 14 new unit tests covering normalizeToken (4 cases) + validateTokenShape (10 cases including all 4 GitHub prefixes, length boundaries, paste-mishap detection) ## Out of scope (future work) - Auto-generation of replacement tokens (still operator's GitHub UI step; this Bead just plumbs the resulting token) - GitHub App / OAuth tokens (separate larger architectural change) - Other secret types (Discord token, Harper admin pass, Anthropic API key, etc) — pattern reusable but file separately ## Note on naming Spec calls for `tps secret rotate-github-pat` (singular). Implementation extends the existing `tps secrets` (plural) command since adding a top-level singular `tps secret` alongside `tps secrets` is bad UX. Functionally identical.
tps-kern
left a comment
There was a problem hiding this comment.
Architecture Review: Approved
All three design decisions are correct.
1. Naming: tps secrets (plural) — Correct
Extending the existing 'tps secrets' command group avoids the UX confusion of a parallel singular 'tps secret' alongside 'tps secrets'. The spec's intent is preserved functionally — only the command prefix differs. No pushback.
2. Validation ordering: shape → probe → write — Correct
Three-layer defense before any mutation:
- Shape check: O(1), pure function, catches paste mishaps, wrong clipboard contents, truncated tokens
- HTTP 200 probe: confirms the token actually works (5s timeout, /user endpoint)
- Atomic write: only fires if both pass
Failed probe leaves existing PAT untouched. This is the right sequence — cheapest checks first, mutation last.
3. Keyring vs file dispatch — Correct
Branching on KEYRING_AGENTS set is clean — currently {flint}, extensible. Both paths:
- File: tmp + rename + chmod 600 (true atomic write). Agent name sanitized with /^[a-zA-Z0-9_-]+$/ before path construction — path traversal defense.
- Keyring: stdin-piped to gh-as auth login --with-token. Token never reaches argv or shell history.
Post-rotation verify handles both paths correctly: direct /user probe for file PATs, gh-as api user for keyring (can't read tokens back from keychain).
Token handling: Strong
- Stdin only — pipe or interactive silent-read prompt
- normalizeToken strips surrounding whitespace + trailing newline (paste/pipe artifact)
- validateTokenShape covers all 4 GitHub prefixes (github_pat_, ghp_, ghs_, gho_), length [40,256], rejects whitespace/quotes/backticks
- Never reaches argv, shell history, or process table
list-github-pats: Clean
Probes all file PATs + keyring agents in one pass. Text table with source/status/login columns, exits 1 on failures with corrective command hint. JSON output for scripting. Replaces the hand-run curl loop cleanly.
Minor: unused import
readline is imported at line 24 but only createInterface is used from it. The named import on line 23 already covers readline types. Line 24 Version: ImageMagick 7.1.2-19 Q16-HDRI aarch64 23897 https://imagemagick.org
Copyright: (C) 1999 ImageMagick Studio LLC
License: https://imagemagick.org/license/
Features: Cipher DPC HDRI Modules
Delegates (built-in): bzlib freetype heic jng jpeg lcms ltdl lzma png tiff webp xml zlib zstd
Compiler: clang (21.0.0)
Usage: import [options ...] [ file ]
Image Settings:
-adjoin join images into a single multi-image file
-border include window border in the output image
-channel type apply option to select image channels
-colorspace type alternate image colorspace
-comment string annotate image with comment
-compress type type of pixel compression when writing the image
-define format:option
define one or more image format options
-density geometry horizontal and vertical density of the image
-depth value image depth
-descend obtain image by descending window hierarchy
-display server X server to contact
-dispose method layer disposal method
-dither method apply error diffusion to image
-delay value display the next image after pausing
-encipher filename convert plain pixels to cipher pixels
-endian type endianness (MSB or LSB) of the image
-encoding type text encoding type
-filter type use this filter when resizing an image
-format "string" output formatted image characteristics
-frame include window manager frame
-gravity direction which direction to gravitate towards
-identify identify the format and characteristics of the image
-interlace type None, Line, Plane, or Partition
-interpolate method pixel color interpolation method
-label string assign a label to an image
-limit type value Area, Disk, Map, or Memory resource limit
-monitor monitor progress
-page geometry size and location of an image canvas
-pause seconds seconds delay between snapshots
-pointsize value font point size
-quality value JPEG/MIFF/PNG compression level
-quiet suppress all warning messages
-regard-warnings pay attention to warning messages
-repage geometry size and location of an image canvas
-respect-parentheses settings remain in effect until parenthesis boundary
-sampling-factor geometry
horizontal and vertical sampling factor
-scene value image scene number
-screen select image from root window
-seed value seed a new sequence of pseudo-random numbers
-set property value set an image property
-silent operate silently, i.e. don't ring any bells
-snaps value number of screen snapshots
-support factor resize support: > 1.0 is blurry, < 1.0 is sharp
-synchronize synchronize image to storage device
-taint declare the image as modified
-transparent-color color
transparent color
-treedepth value color tree depth
-verbose print detailed information about the image
-virtual-pixel method
Constant, Edge, Mirror, or Tile
-window id select window with this id or name
root selects whole screen
Image Operators:
-annotate geometry text
annotate the image with text
-colors value preferred number of colors in the image
-crop geometry preferred size and location of the cropped image
-encipher filename convert plain pixels to cipher pixels
-extent geometry set the image size
-geometry geometry preferred size or location of the image
-help print program options
-monochrome transform image to black and white
-negate replace every pixel with its complementary color
-quantize colorspace reduce colors in this colorspace
-resize geometry resize the image
-rotate degrees apply Paeth rotation to the image
-strip strip image of all profiles and comments
-thumbnail geometry create a thumbnail of the image
-transparent color make this color transparent within the image
-trim trim image edges
-type type image type
Miscellaneous Options:
-debug events display copious debugging information
-help print program options
-list type print a list of supported option arguments
-log format format of debugging information
-version print version information
By default, 'file' is written in the MIFF image format. To
specify a particular image format, precede the filename with an image
format name and a colon (i.e. ps:image) or specify the image type as
the filename suffix (i.e. image.ps). Specify 'file' as '-' for
standard input or output. is unused — lint cleanup. Non-blocking.
CI: Build, Lint, Dependency Audit, Binary Smoke, Semgrep, Unit Tests all green. Docker + CodeQL queued. Sherlock hasn't reviewed yet — they should verify stdin handling, no argv leaks, atomic write semantics.
Merge when CI + security review complete. 📐
tps-kern
left a comment
There was a problem hiding this comment.
Architecture review: approved. All three design decisions correct — plural naming consistent with existing command group, shape→probe→write ordering is proper defense-in-depth, keyring vs file dispatch clean. Token never reaches argv, stdin only. Minor unused import (readline on line 24) for lint cleanup. Sherlock should verify stdin handling and atomic write semantics. Merge when CI + security review complete. 📐
Summary
Fixes ops-njgl (P1, 1.0). Adds two new actions under
tps secretsfor GitHub PAT lifecycle:Why
Today's rotation experience: 6 PATs to rotate manually (GitHub UI clicks + 6 file writes + 6 chmods + 6 verifies). 30+ min per cycle. Friction tempts operators to extend PAT expiry past 30 days, which is the wrong move given today's secret-leak rate.
Design properties
cat new-pat | tps secrets rotate-github-pat anvil) or interactive silent-read prompt.flint— pipes togh-as flint auth login --with-tokeninstead of writing a file (gh-as resolves flint via macOS Keychain).list-github-patsprobes every secret file + keyring agent in one pass — replaces the inline curl loop ops have been running by hand.Test plan
normalizeToken(4 cases) +validateTokenShape(10 cases). Cover all 4 GitHub prefixes (github_pat_,ghp_,ghs_,gho_), length boundaries (40 / 256 chars), and paste-mishap detection (whitespace, quotes, backticks).Naming note
Spec called for
tps secret(singular). Implementation extends existingtps secrets(plural) since adding a parallel singular top-level command alongside the existing plural would be bad UX. Functionally identical to spec intent.Out of scope (future work, file separately)
🤖 Generated with Claude Code