feat(secrets): atomic grob secrets rotate (Phase E)#314
Closed
Destynova2 wants to merge 5 commits intomainfrom
Closed
feat(secrets): atomic grob secrets rotate (Phase E)#314Destynova2 wants to merge 5 commits intomainfrom
grob secrets rotate (Phase E)#314Destynova2 wants to merge 5 commits intomainfrom
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
grob secrets rotate <name>for atomic credential replacement on top of the SecretBackend trait merged in #275/#276.add).<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-oldcopies the previous ciphertext to<name>.previous-<unix_ts>.encfor rollback.--reason "..."is recorded in the audit log entry.AuditEvent::CredentialRotatedvariant; rotation emits an entry to~/.grob/audit/when reachable. Audit failure is non-fatal — the rename has already durably committed the swap.list_secretsnow hides<name>.rotatingartifacts so a crashed rotation does not surface a phantom name;<name>.previous-<ts>archives stay visible for rollback.Notes
feat/credentials-rotate-implbecause sibling agents are actively resettingfeat/credentials-rotatein this multi-agent run.grob secrets testsubcommand #311'ssecrets testbecause main has the broken-preset issue. Will rebase once those land.test_one_secret(name)reuse from feat(secrets): addgrob secrets testsubcommand #311 is a follow-up: post-rotationrotate_secretalready does an end-to-end decrypt round-trip to validate the new ciphertext, but real provider probing requires plumbing&AppConfiginto the rotation command.Test plan
cargo check --testscargo fmt --all -- --checkcargo clippy --lib --tests --bins -- -D warningscargo nextest run --lib(1052+ tests pass)--keep-oldarchive, empty stdin rejected (old preserved), unknown source name,<name>.rotatinghidden fromlist_secrets.🤖 Generated with Claude Code