feat(audit): cryptographic call auditing via signet - #10
Conversation
Integrate Prismer-AI/signet so every relais exec emits an Ed25519-signed, hash-chained, offline-verifiable audit receipt through one Router::exec choke point. - relais-core `audit` feature (signet-core 0.10): AuditSink, redaction (key + value + scalar masking, opaque credential_ref), envelope mapping, on-host key + credential-ref store, single sequential writer (cross-process dir lock + monotonic timestamps), sidecar preimage store, and fail-closed verification (chain + per-signer windowed verify_compound + sidecar hash recompute + tail-truncation via expected head). - Router::exec choke point; server + CLI routed through it; CI choke-point guard. - `relais audit init|pubkey|verify|tail`; env-driven enablement (RELAIS_AUDIT_MODE open|closed); response-open vs response-closed semantics. - Design + impl specs under docs/design/ (signet-audit-integration v4 after 4 Codex rounds; signet-audit-impl after 3 + final whole-feature review). Audit log is redacted; caller-facing responses are unchanged (explicit non-goal). Default builds pull no audit deps; feature-gated throughout. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 137180885b
ℹ️ 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".
| // Apply the limit after site filtering (query was unbounded in that case). | ||
| if site.is_some() { | ||
| if let Some(n) = limit { | ||
| out.truncate(n); |
There was a problem hiding this comment.
Return the newest site records when tailing with a limit
When --site is used, the query is intentionally unbounded and signet_core::audit::query returns records oldest-to-newest (the verifier above relies on this ordering). This out.truncate(n) therefore keeps the oldest matching site records rather than the recent tail; after more than n records for a site, relais audit tail --site scs --limit 20 hides the latest calls. Take the last n after filtering, or reverse before truncating.
Useful? React with 👍 / 👎.
| std::fs::create_dir_all(parent).map_err(|e| AuditError::Io(e.to_string()))?; | ||
| } | ||
| let json = serde_json::to_string_pretty(self).map_err(|e| AuditError::Io(e.to_string()))?; | ||
| std::fs::write(&self.path, json).map_err(|e| AuditError::Io(e.to_string()))?; |
There was a problem hiding this comment.
Merge credential refs before rewriting the store
In deployments with more than one relais process sharing RELAIS_SIGNET_DIR (the writer explicitly supports this with a cross-process lock), each AuditSink loads credential_refs.json once and this write persists its stale in-memory map wholesale. If two processes mint refs concurrently—e.g. server handling github while a CLI exec handles scs—the later write can drop the other kref, leaving already-signed receipts with credential_ref values that cannot be resolved locally. Re-read/merge under an interprocess lock or use per-ref atomic files.
Useful? React with 👍 / 👎.
| params: request, | ||
| params_hash: String::new(), | ||
| target: base_url.to_string(), | ||
| transport: "https".to_string(), |
There was a problem hiding this comment.
Derive the audited transport from the target URL
For adapters configured with an http:// base URL (the SCS adapters default/support local HTTP endpoints), this signs receipts with action.target pointing to HTTP but action.transport claiming HTTPS. Auditors relying on the signed Action see a stronger transport than was actually used; derive this from base_url or avoid asserting HTTPS when the target scheme is not HTTPS.
Useful? React with 👍 / 👎.
What
Integrates
Prismer-AI/signet(signet-core0.10) so every relaisexecemits an Ed25519-signed, SHA-256 hash-chained, offline-verifiable audit receipt, written through a singleRouter::execchoke point. Answers "记录每个 API 调用" — as proof, not a mutable log.How
relais-coreauditfeature (opt-in; default builds pull zero audit deps):redact.rs): masks by key name, by secret value (allCredentialDatavariants incl. OAuth refresh token), and scalar-equal secrets; opaque non-reversiblecredential_ref; error-message redaction.envelope.rs):ExecContext/Response→ signetAction+ response envelope; request value is byte-identical to bothAction.paramsand the sidecar.key.rs): on-host Ed25519 key under~/.relais/signet/keys/relais.{key,pub}(via signet fs_ops), pubkey derived from the signing key;credential_refs.json0600.writer.rs): sign + sidecar +audit::appendunder a cross-process dir lock, monotonic timestamps re-clamped to the on-disk latest, bounded mpsc + ack contract, never-aborted append.sidecar.rs): the chain holds only the response hash, so the redacted{request,response}preimage is persisted per receipt for legibility + recompute.verify.rs): fail-closed —verify_chain+ per-recordverify_compoundagainst the receipt's signer pubkey within a trusted, time-windowed anchor (trusted_keys.json) + sidecarsha256:+hex(JCS) recompute + tail-truncation detection via expected head.Router::execchoke point: server (handlers.rs) and CLI (exec.rs) both routed through it; CI choke-point guard forbids direct adapterexecin production paths.relais audit init|pubkey|verify|tail; env-driven enablement (RELAIS_AUDIT_MODE=open|closed); response-open (lossy, logged) vs response-closed (withhold result).ResponseMeta.receipt: Option<ReceiptHandle>.docs/design/:signet-audit-integration.md(design, v4 after 4 Codex rounds) +signet-audit-impl.md(impl plan, 3 rounds + final whole-feature review).Scope / non-goals
Testing
cargo test --workspace(default) green;cargo test -p relais-core --features audit→ 39 pass (redaction leak-guards, chain integrity, cross-midnight + restart monotonicity, backpressure, fail-closed/untrusted/tampered-sidecar/tail-truncation verify, key 0600).cargo clippy --workspaceclean;cargo clippy -p relais-core --features audit --all-targets -D warningsclean.🤖 Generated with Claude Code