R5: consent-gated recruitment + per-hop X25519 sealed credentials

Add the in-band, signature-gated consent handshake that governs whether a
node may be recruited into a measured SOR circuit, plus a per-recipient
sealed box so a hop credential decrypts only with the host's X25519 key.

- crypto.rs: seal_to_pubkey/open_sealed (ephemeral X25519 -> HKDF-SHA256 ->
  Fernet AEAD), reusing the existing fernet crate (no new symmetric primitive).
- sor/consent.rs: signed ConsentRequest (Ed25519 persona), node_evaluate
  (reject unless the signature verifies), CircuitBuilder that recruits a hop
  only on an accept it can open; parse_sor_frame with never-panic proptests.
- net.rs: {"_sor":...} control frames are live-only and classified out-of-band
  by the SOR layer (parse_sor), never surfaced as chat/app events.
- cmd_chat/sor/consent.py + bridge _sor case: bit-compatible Python mirror;
  the agent only observes/classifies consent frames — it never auto-accepts
  and stands up no forwarder (forwarding is R4, isolated-engine-only).

Acceptance check (roadmap R5) green both languages: host recruits a hop only
after an explicit accept; a reject leaves no entry; the credential opens only
with the host key (third party cannot); unsigned/forged request -> rejected.
Cross-language KDF parity pinned by a shared known-answer vector.
Rust 75 passed; Python SOR suite 37 passed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
leetcrypt
2026-07-19 16:35:11 -07:00
parent a9308edc5d
commit 7de0f57fa6
9 changed files with 1057 additions and 5 deletions
+12
View File
@@ -394,6 +394,18 @@ class AgentBridge(Client):
self.sbx_name = ""
self.sbx_backend = None
return
# R5 in-band SOR consent frames: classify only. Recruitment into a SOR
# circuit is opt-in + signature-gated (cmd_chat/sor/consent.py); the
# actual forwarder is R4 and runs isolated-engine-only. The bridge never
# auto-accepts and stands up no forwarder here — it only recognizes the
# frame so it is not mistaken for an addressed message.
if "_sor" in frame:
from cmd_chat.sor.consent import parse_sor_frame
parsed = parse_sor_frame(text)
if parsed is not None:
self.info(f"observed SOR consent frame ({parsed['kind']})")
return
if frame.get("_perm") != "acl":
return
was = self.granted