Skip to content

feat: keys rotate, status Codex/Gemini checks, remote developer health (#20 #21 #22)#26

Merged
codeprakhar25 merged 2 commits into
mainfrom
feat/issues-20-21-22
May 13, 2026
Merged

feat: keys rotate, status Codex/Gemini checks, remote developer health (#20 #21 #22)#26
codeprakhar25 merged 2 commits into
mainfrom
feat/issues-20-21-22

Conversation

@codeprakhar25
Copy link
Copy Markdown
Owner

@codeprakhar25 codeprakhar25 commented May 13, 2026

What does this PR do?

Closes #20,
Closes #21,
Closes #22.

Issue #20agentdiff keys rotate

  • keys::archive_current_keypair: moves the active keypair into a timestamped folder under ~/.agentdiff/keys/archive/ with an archive.toml metadata file (key_id, archived_at, optional expires_at)
  • keys::try_load_archived_verifying_key: lets agentdiff verify fall back to the local archive when a key ID is absent from the git registry — ensures traces signed by rotated keys remain verifiable offline
  • keys rotate --resign-last N: re-signs the last N entries of the current branch's local trace buffer with the newly generated key

Issue #21agentdiff status Codex/Gemini enablement flags

  • Codex: after confirming the hook marker is present in ~/.codex/config.toml, additionally checks features.codex_hooks = true via TOML parse — warns with re-run hint if missing
  • Gemini: after confirming capture-antigravity is registered in settings.json, additionally checks tools.enableHooks = true via JSON parse — warns if the flag is absent (without it, Gemini ignores hooks even when entries are present)

Issue #22 — Per-developer health in agentdiff status --remote

  • Adds REMOTE TRACE BRANCHES table: one row per refs/agentdiff/traces/* ref with trace count, age, and stale (>7d) warning
  • Adds DEVELOPERS table: groups traces by AgentdiffMetadata.author, shows trace count and last-active age; stale developers highlighted in yellow
  • --since DURATION flag (e.g. 7, 7d, 48h) filters both tables to the active window

Type of change

  • New feature
  • Bug fix (Greptile review fixes applied)

Greptile bugs addressed

  • archive_current_keypair (src/keys.rs): removed redundant if pub_path.exists() re-check after the private key has already been renamed — the public key existence is guaranteed by the anyhow::ensure! above, so the re-check created a silent partial-archive path that would leave old-key traces unverifiable after rotation
  • status --remote Gemini arm (src/commands/status.rs): the (true, true) else branch was printing an ok()-prefixed line before the actual warn() — consolidated into a single warn() line so the output is not misleading

Testing

  • cargo check passes (no errors)
  • agentdiff status runs correctly (verified via post-commit hook output)

Checklist

  • Follows existing code style
  • No new dependencies (toml and serde_json were already in Cargo.toml)

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 13, 2026

AgentDiff Report

Summary

Agent Lines %
cursor 487 99%
claude-code 3 1%

Review Context

  • Intent: unspecified (490 lines, 6 files)
    • Agent/model: claude-code, cursor / claude-sonnet-4-6, composer-2
    • Prompt: You are picking up a task from another AI agent. Read the handoff document at .relay/handoffs/HANDOFF-2026-05-13T14-45-47-57be9f.md carefully before doing ..., so I continued the working on the issue with another agent and raised a PR can you see the PR and comments and code changes we did to verify everyting was do...

Files To Review First

File Lines Dominant Agent Intent Context
src/commands/status.rs 257 cursor unspecified trace 508a58bd
src/keys.rs 136 cursor unspecified trace 508a58bd
src/commands/keys.rs 64 cursor unspecified trace 508a58bd
src/commands/verify.rs 19 cursor unspecified trace 508a58bd
src/cli.rs 13 cursor unspecified trace 508a58bd
src/main.rs 1 cursor unspecified trace 508a58bd
Trace details
Trace Agent Intent Files Lines
508a58bd cursor unspecified src/cli.rs, src/commands/keys.rs, src/commands/status.rs, src/commands/verify.rs, src/keys.rs, +1 more 487
d1465c95 claude-code unspecified src/commands/status.rs, src/keys.rs 3

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 13, 2026

Greptile Summary

This PR replaces the old .key.bak rotation strategy with a proper timestamped archive under ~/.agentdiff/keys/archive/, allowing agentdiff verify to fall back to locally-archived keys when the git registry has no entry for a rotated key ID. It also adds an optional --resign-last N flag to re-sign recent local trace entries with the new key after rotation, and extends agentdiff status with a remote developer health table and a --since filter.

  • Key archiving (keys.rs, commands/keys.rs): rotation now moves the old keypair into a dated subdirectory with an archive.toml manifest; try_load_archived_verifying_key scans that directory during verification, falling back to the public key file for legacy (pre-manifest) folders.
  • --resign-last N (commands/keys.rs): re-signs the tail of the current branch's local JSONL trace buffer with the new key using an in-place std::fs::write, which is not atomic and can corrupt the file on a mid-write failure.
  • Status improvements (commands/status.rs, cli.rs): adds Gemini tools.enableHooks and Codex features.codex_hooks validation, a per-ref and per-developer activity table from remote trace branches, and --since duration filtering.

Confidence Score: 4/5

Safe to merge after addressing the non-atomic write in the re-sign path.

The key archiving and verify fallback logic are sound. The one concrete defect is in resign_last_local_traces: it uses std::fs::write directly on the live trace file, which truncates before writing — a disk-full or I/O error mid-write silently destroys all trace lines, including those not being re-signed. An atomic write (write to a .tmp sibling, then rename) would eliminate this risk.

src/commands/keys.rs — the resign_last_local_traces write path.

Important Files Changed

Filename Overview
src/commands/keys.rs Rotation now archives instead of .bak-renaming keys; new resign_last_local_traces uses a non-atomic std::fs::write that can corrupt the trace buffer if interrupted mid-write.
src/keys.rs New archive_current_keypair and try_load_archived_verifying_key functions implement a proper timestamped archive; minor double Utc::now() call produces a slight timestamp inconsistency between the directory name and archive.toml.
src/commands/verify.rs Falls back to the local archive via try_load_archived_verifying_key when the git registry has no entry for a key ID; clean two-level lookup without regressions.
src/commands/status.rs Adds Gemini tools.enableHooks and Codex features.codex_hooks checks, remote developer health table, and --since filter; --since parsing accepts negative integers only caught by the downstream ensure!.
src/cli.rs Adds RotateKeysArgs with optional --resign-last and --since to StatusArgs; straightforward CLI plumbing.
src/main.rs One-line change to destructure Rotate(args) and pass &args to run_rotate; no issues.

Reviews (2): Last reviewed commit: "fix: address Greptile review bugs in arc..." | Re-trigger Greptile

Comment thread src/keys.rs Outdated
Comment thread src/commands/status.rs
- keys.rs: remove redundant pub_path.exists() guard after private key
  rename — public key existence is already ensured before the rename,
  so re-checking creates a silent partial-archive path that leaves old
  traces unverifiable after rotation
- status.rs: fix misleading ok() prefix in Gemini (true,true) arm when
  tools.enableHooks is not set; consolidate into a single warn() line

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@codeprakhar25 codeprakhar25 changed the title fix: updates keys lifecycle feat: keys rotate, status Codex/Gemini checks, remote developer health (#20 #21 #22) May 13, 2026
@codeprakhar25 codeprakhar25 merged commit e11a569 into main May 13, 2026
3 checks passed
@codeprakhar25 codeprakhar25 deleted the feat/issues-20-21-22 branch May 13, 2026 18:34
Comment thread src/commands/keys.rs
Comment on lines +106 to +130
let raw = std::fs::read_to_string(&path)
.with_context(|| format!("reading {}", path.display()))?;
let mut lines: Vec<String> = raw.lines().map(String::from).collect();
while lines.last().map(|l| l.trim().is_empty()).unwrap_or(false) {
lines.pop();
}
anyhow::ensure!(!lines.is_empty(), "local trace buffer is empty");

let take = n.min(lines.len());
let start = lines.len() - take;
for i in start..lines.len() {
let mut val: serde_json::Value = serde_json::from_str(&lines[i])
.with_context(|| format!("parsing trace line {}", i + 1))?;
if let Some(obj) = val.as_object_mut() {
obj.remove("sig");
}
let sig = keys::sign_record(&val)?;
val.as_object_mut()
.context("trace entry must be a JSON object")?
.insert("sig".to_string(), serde_json::to_value(&sig)?);
lines[i] = serde_json::to_string(&val)?;
}

std::fs::write(&path, lines.join("\n") + "\n")
.with_context(|| format!("writing {}", path.display()))?;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Non-atomic write can corrupt the trace buffer on failure

std::fs::write opens the file with O_TRUNC, zeroing its contents before writing the new data. If the write is interrupted (e.g. disk-full mid-write, process killed, I/O error), the file is left truncated and partially written — all original trace lines, including the ones not being re-signed, are lost. Because this is an audit-trail store, a silently half-written file is worse than a failed write.

The fix is to write to a .tmp sibling and atomically replace the original via std::fs::rename.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant