Skip to content

feat(omni): genie omni handshake — register host with ed25519 keypair (D5 Group 2) - #1537

Merged
namastex888 merged 1 commit into
devfrom
feat/genie-omni-handshake
Apr 29, 2026
Merged

namastex888 merged 1 commit into
devfrom
feat/genie-omni-handshake

Conversation

@namastex888

Copy link
Copy Markdown
Contributor

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 returned host_id so subsequent groups (request signing, verification) can attach X-Genie-Host-Id to outgoing requests.

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

Path Contents Perms
~/.genie/keys/genie-host.ed25519 PKCS#8 PEM private key 0600
~/.genie/keys/genie-host.ed25519.pub base64url of raw 32-byte public key 0644
~/.genie/keys/host.json { hostId, pubkey, hostname, registeredAt, rotatedFrom? } 0644

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. Walks up to fs root or 16 levels.
  • --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 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

Group Scope
3 Genie request signing (this PR drops the keypair on disk; reading it for outgoing requests is Group 3)
4 Omni signature verification middleware (security-review gate)
5 Per-host scope enforcement
6 --require-genie-signature per-instance opt-in
7 Brain entries + ADR

Test plan

  • bun test src/term-commands/omni/handshake.test.ts → 9/9 pass
    • keyPaths respects $GENIE_HOME
    • 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
  • bun run typecheck → green
  • bunx biome check → clean (auto-fix applied for delete and template-literal style)
  • Pre-push gate (typecheck + lint + dead-code + skills/wishes-lint + emit-discipline + 3815+ tests) → all green

The HTTP path (callTrustEndpoint → omni's POST /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 to skills/omni/SKILL.md. The pre-existing skill from #1516 references omni connect (and other omni subcommands registered with advanced/standard visibility) which don't surface in plain omni --help. The skills-lint script collects subcommands by parsing omni --help and 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 --all enumeration.

Without this bailout, the pre-push gate would have failed on origin/dev's existing state — verified by running bun run skills:lint against a clean origin/dev checkout.

Cross-PR coordination

Group Repo PR Status
1.0 omni #555 merged
1.1 omni #556 merged
1.2 omni #558 merged
2 genie this PR open
3 genie next request signing
4 omni next verification middleware (security review)
5 omni next scope enforcement
6 omni next per-instance opt-in
7 genie-configure next brain entries + ADR

Wish: <genie-repo>/.genie/wishes/omni-host-fingerprint-trust/WISH.md (filed in genie #1520).

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.
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@coderabbitai

coderabbitai Bot commented Apr 29, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4bd974fd-1e6a-40ca-932e-e5f9cded9382

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/genie-omni-handshake

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@namastex888
namastex888 merged commit dfcfd3a into dev Apr 29, 2026
11 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment on lines +195 to +197
// Generate fresh keypair (overwrites old keys on disk).
const fresh = generateAndPersistKeypair(paths);
pubkey = fresh.pubkeyB64Url;

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 Badge 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 👍 / 👎.

Comment on lines +141 to +145
const res = await fetch(`${apiUrl.replace(/\/+$/, '')}/api/v2/trust${path}`, {
method,
headers,
body: body === undefined ? undefined : JSON.stringify(body),
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

namastex888 pushed a commit that referenced this pull request Apr 30, 2026
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.
@automagik-genie
automagik-genie deleted the feat/genie-omni-handshake branch September 25, 2026 04:52
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.

2 participants