Skip to content

feat(secrets): atomic grob secrets rotate (Phase E)#316

Closed
Destynova2 wants to merge 5 commits intomainfrom
feat/credentials-rotate
Closed

feat(secrets): atomic grob secrets rotate (Phase E)#316
Destynova2 wants to merge 5 commits intomainfrom
feat/credentials-rotate

Conversation

@Destynova2
Copy link
Copy Markdown
Contributor

Summary

Adds grob secrets rotate <name> for atomic credential replacement on top of the SecretBackend trait merged in #275/#276.

  • Reads the new value from stdin (one line, like add).
  • Atomic flow: writes new ciphertext to a sibling <name>.rotating.enc, decrypts back to verify integrity, then renames over the live <name>.enc. Any failure before the rename leaves the previous value untouched and best-effort removes the temp file.
  • --keep-old copies the previous ciphertext to <name>.previous-<unix_ts>.enc for rollback.
  • --reason "..." is recorded in the audit log entry.
  • New AuditEvent::CredentialRotated variant; rotation emits an entry to ~/.grob/audit/ when reachable. Audit failure is non-fatal because the rename has already durably committed the swap.
  • list_secrets now hides <name>.rotating artifacts so a crashed rotation does not surface a phantom name; <name>.previous-<ts> archives stay visible for rollback.

Notes

Test plan

  • cargo check --tests
  • cargo fmt --all -- --check
  • cargo clippy --lib --tests --bins -- -D warnings
  • cargo nextest run --lib (all tests pass)
  • Five new storage tests cover: successful swap, --keep-old archive, empty stdin rejected (old preserved), unknown source name, <name>.rotating hidden from list_secrets.

🤖 Generated with Claude Code

Clément LIARD and others added 5 commits April 27, 2026 23:34
PR #298 deleted obsolete preset .toml files (cheap/fast/local/medium/
optimal/perf) but did not update src/preset/mod.rs which had hard-coded
include_str! macros for those filenames. Result: main has been broken
since then (CI build error: "couldn't read presets/medium.toml").

Root cause was an over-coupled architecture: adding/removing a preset
required touching 4 places in code (file + index.toml + include_str!
const + match arms in two functions). This commit makes the architecture
robust by making it impossible to forget any of those steps.

Architecture change:
- Replace 7 hand-maintained `const BUILTIN_*: &str = include_str!(...)`
  lines with a single `static BUILTIN_PRESETS: Dir = include_dir!(...)`.
- Replace the hand-maintained Vec<PresetInfo> in list_presets() with a
  loop over BUILTIN_PRESETS.files().
- Replace the match arm in preset_content() with BUILTIN_PRESETS.get_file().
- Each preset now declares its description in a `[meta]` section at the
  top of its TOML; list_presets() parses it once at call time.

Adding a new preset is now a single action: drop a .toml file in
presets/ with a `[meta] description = "..."` section. Build picks it
up on next compile. Removing a preset = delete the .toml file. No
Rust code change needed in either case. The trap that caused this
incident is structurally impossible.

Files touched:
- Cargo.toml: + include_dir = "0.7"
- src/preset/mod.rs: rewritten list_presets / preset_content + new
  parse_description helper + updated builtin-validation test
- presets/{perf,ultra-cheap,eu-eco,eu-pro,eu-max,gdpr,eu-ai-act}.toml:
  + [meta] description = "..."
- tests/enterprise/preset_snapshot_test.rs: replaced removed presets
  (medium/local/cheap/fast) with the new lineup (ultra-cheap, eu-eco,
  eu-pro, eu-max). preset_snapshot_test! macros + cross-preset tests
  updated to match.
- tests/enterprise/snapshots/: regenerated for the new preset lineup,
  obsolete snapshots removed.

Test plan:
- [x] `cargo check --lib` clean
- [x] `cargo build --release` succeeds (was failing on main)
- [x] `cargo nextest run preset` — 30/30 pass
- [x] `cargo clippy --lib --tests -- -D warnings` clean
- [x] `cargo fmt --check` clean
- [x] `./target/release/grob preset list` shows all 7 builtins with
  descriptions parsed from [meta]
- [x] `./target/release/grob preset apply ultra-cheap --dry-run` works
  (verifies [meta] doesn't break the apply path)
- [x] User-installed preset workflow: drop a .toml in ~/.grob/presets/
  with [meta] description, `grob preset list` displays it correctly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Users frequently send small variations of provider model IDs (date
suffixes like `claude-3-5-sonnet-20241022`, `-latest` aliases, dotted
versions like `gemini-2.5-flash`, mixed casing, or the older Anthropic
`claude-{N}-{M}-{family}` ordering). Without canonicalization those all
miss the configured `[[models]]` entry and fall through to the
auto-mapper's catch-all default — bypassing the user's intended
fallback chain.

Add `routing::classify::model_name::canonicalize_model_name` and call
it as step 0 of `Router::route`, before any background / auto-map /
prompt-rule / lookup logic. The function is idempotent and returns
`Cow::Borrowed` for already-canonical inputs (steady state in
production), so configs that use the canonical IDs (`claude-sonnet-4-5`,
`gpt-4o`, `gemini-3-pro`, …) pay zero allocations.

Coverage: 25-row matrix across Anthropic 3.x/4.x, OpenAI (gpt-4o /
gpt-5 / gpt-5.2), DeepSeek, Gemini, and Grok families plus a proptest
that exhaustively checks idempotence over arbitrary alphanumeric
strings. All 130 existing routing tests still pass.
feat(routing): canonicalize model names before [[models]] lookup
Probes each stored secret by issuing a single low-cost call (typically
`GET /v1/models`) against the providers that reference it via
`secret:<name>`. Per-secret status maps to ok (2xx), invalid (401/403),
or warn (network/5xx/unknown provider). Exits 1 if any secret is
explicitly invalid; network noise only warns.

The probe shares its endpoint catalogue with the existing wizard-side
credential check (extended with a richer `CheckOutcome` enum so callers
can tell auth failures apart from infrastructure failures). Secret values
are never written to logs or output, the timeout is fixed at 10s per
provider, and a `--json` flag emits a stable schema for scripting.

Phase E credentials vault expansion: complements the SecretBackend trait
landed in #275/#276 by giving operators a one-shot health command for
their stored credentials.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(secrets): add `grob secrets test` subcommand
@Destynova2 Destynova2 enabled auto-merge April 28, 2026 08:04
@Destynova2 Destynova2 closed this Apr 28, 2026
auto-merge was automatically disabled April 28, 2026 08:06

Pull request was closed

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