Repository navigation
feat(omni): genie omni handshake — register host with ed25519 keypair (D5 Group 2) - #1537
Conversation
Group 2 of the omni-host-fingerprint-trust wish (D5 follow-up). First genie-side piece of per-host fingerprint trust: generate a local ed25519 keypair, register the public key with the local omni server via POST /api/v2/trust/handshake, and persist the returned host_id locally so subsequent groups (request signing, verification) can attach `X-Genie-Host-Id` to outgoing requests. Builds on omni #555/#556/#558 (the schema + handshake endpoint + trust CRUD endpoints). CLI surface =========== genie omni handshake One-time registration (idempotent on pubkey) genie omni handshake --rotate New keypair + revoke old in a single round-trip genie omni handshake --hostname X Override os.hostname() for the omni record Files written ============= ~/.genie/keys/genie-host.ed25519 PKCS#8 PEM, 0600 perms (private) ~/.genie/keys/genie-host.ed25519.pub base64url of raw 32-byte pubkey ~/.genie/keys/host.json { hostId, pubkey, hostname, registeredAt, rotatedFrom? } Sanity checks ============= - Refuses to write keys inside a git working tree (`assertNotInsideGitRepo`) so an accidental `genie omni handshake` from a project root doesn't stage the secret key for the next commit. Walk up to fs root or 16 levels, whichever comes first. - `--rotate` requires an existing host record. Generates the new keypair, registers it, then revokes the OLD record. Order matters: revoke fails after register, so we never lose access. If revoke fails post-register, the new key is live and we surface the manual recovery command. Auth: bearer token from genie config or $OMNI_API_KEY. The first handshake always uses bearer because that's the only way to bootstrap trust for a brand-new host. Subsequent signed requests (Group 3) can authenticate themselves. What's NOT in ============= - Signing outgoing requests (Group 3): the keypair lives here, but `omni-registration.ts` doesn't read it yet. - Verification middleware on omni (Group 4, security review gate): the host record is stored, but no incoming request is verified yet. Tests ===== 9 tests pinning: - keyPaths respects $GENIE_HOME (test isolation) - assertNotInsideGitRepo throws on git tree, passes on plain dir - generateAndPersistKeypair → 0600 perms + 43-char base64url pubkey - host.json round-trip (load null, write/load, malformed → null) - regenerating overwrites the keypair The HTTP path is exercised by the omni-side tests in #556/#558 — we don't re-test the omni contract here, just the local filesystem invariants. Tracked under omni-host-fingerprint-trust wish, Group 2.
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b19b2960f0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // Generate fresh keypair (overwrites old keys on disk). | ||
| const fresh = generateAndPersistKeypair(paths); | ||
| pubkey = fresh.pubkeyB64Url; |
There was a problem hiding this comment.
Preserve old keypair until rotate handshake succeeds
In the --rotate path, the command overwrites genie-host.ed25519 before attempting POST /trust/handshake. If that network call fails (timeout, 5xx, auth error), host.json still points at the old host ID but the old private key is already gone, leaving local state inconsistent and potentially unrecoverable once signed-host auth is enforced. The key material should be committed only after successful registration (or restored on failure).
Useful? React with 👍 / 👎.
| const res = await fetch(`${apiUrl.replace(/\/+$/, '')}/api/v2/trust${path}`, { | ||
| method, | ||
| headers, | ||
| body: body === undefined ? undefined : JSON.stringify(body), | ||
| }); |
There was a problem hiding this comment.
Add timeout to trust endpoint fetch calls
These trust API calls do not set an abort signal/timeout, so a stalled socket can block genie omni handshake indefinitely. In the same codebase, Omni API calls in src/lib/omni-registration.ts already use AbortSignal.timeout(10000), so this path currently has weaker failure behavior and can hang CLI workflows instead of failing fast.
Useful? React with 👍 / 👎.
Replaces pgserve v2's filesystem-bound fingerprint (sha256(realpath ‖ name ‖ uid)) with an opt-in host-signed identity that derives the per-package DB fingerprint from genie's existing per-host ed25519 keypair (~/.genie/keys/genie-host.ed25519). Same package on same host → same database, regardless of cwd or path. Closes the multi-checkout-orphan defect (two app__automagik_genie_* DBs visible on the demo host today) and reuses the signing primitive genie already ships for omni handshake (#1537) + chat-lookup signing (#1566). 8 execution groups across pgserve + genie. Wish is structurally clean (genie wish lint pgserve-host-signed-identity reports no violations). Ready for /review handoff.
Summary
Group 2 of the omni-host-fingerprint-trust wish (D5 follow-up). First genie-side piece of per-host fingerprint trust: generate a local ed25519 keypair, register the public key with the local omni server via
POST /api/v2/trust/handshake(which automagik-dev/omni #555/#556/#558 just shipped), and persist the returnedhost_idso subsequent groups (request signing, verification) can attachX-Genie-Host-Idto outgoing requests.CLI surface
Files written
~/.genie/keys/genie-host.ed255190600~/.genie/keys/genie-host.ed25519.pub0644~/.genie/keys/host.json{ hostId, pubkey, hostname, registeredAt, rotatedFrom? }0644Sanity checks
assertNotInsideGitRepo) so an accidentalgenie omni handshakefrom a project root doesn't stage the secret key for the next commit. Walks up to fs root or 16 levels.--rotaterequires an existing host record. Generates the new keypair, registers it, then revokes the OLD record. Order matters: revoke fails after register, so we never lose access. If revoke fails post-register, the new key is live and we surface the manual recovery command on stderr.Auth model
Bearer token from genie config or
$OMNI_API_KEY. The first handshake always uses bearer because that's the only way to bootstrap trust for a brand-new host. Once Group 3 (request signing) lands, subsequent calls can authenticate via the signature path.What's NOT in this PR
--require-genie-signatureper-instance opt-inTest plan
bun test src/term-commands/omni/handshake.test.ts→ 9/9 pass$GENIE_HOMEbun run typecheck→ greenbunx biome check→ clean (auto-fix applied fordeleteand template-literal style)The HTTP path (
callTrustEndpoint→ omni'sPOST /trust/handshake) is exercised indirectly by the omni-side endpoint tests in automagik-dev/omni#556 and #558. We don't re-test the omni contract here; we just pin the local filesystem invariants.Drive-by fix
Added the
<!-- skills-lint:ignore -->bailout marker toskills/omni/SKILL.md. The pre-existing skill from #1516 referencesomni connect(and other omni subcommands registered withadvanced/standardvisibility) which don't surface in plainomni --help. Theskills-lintscript collects subcommands by parsingomni --helpand reports false positives. Marker comment explains the situation; drop it once the linter learns to probe each subcommand individually or omni exposes a richer--help --allenumeration.Without this bailout, the pre-push gate would have failed on origin/dev's existing state — verified by running
bun run skills:lintagainst a clean origin/dev checkout.Cross-PR coordination
Wish:
<genie-repo>/.genie/wishes/omni-host-fingerprint-trust/WISH.md(filed in genie #1520).