Skip to content

feat(audit): cryptographic call auditing via signet - #10

Merged
willamhou merged 1 commit into
masterfrom
feat/signet-audit-integration
Jun 22, 2026
Merged

feat(audit): cryptographic call auditing via signet#10
willamhou merged 1 commit into
masterfrom
feat/signet-audit-integration

Conversation

@willamhou

Copy link
Copy Markdown
Owner

What

Integrates Prismer-AI/signet (signet-core 0.10) so every relais exec emits an Ed25519-signed, SHA-256 hash-chained, offline-verifiable audit receipt, written through a single Router::exec choke point. Answers "记录每个 API 调用" — as proof, not a mutable log.

How

  • relais-core audit feature (opt-in; default builds pull zero audit deps):
    • Redaction boundary (redact.rs): masks by key name, by secret value (all CredentialData variants incl. OAuth refresh token), and scalar-equal secrets; opaque non-reversible credential_ref; error-message redaction.
    • Envelope mapping (envelope.rs): ExecContext/Response → signet Action + response envelope; request value is byte-identical to both Action.params and the sidecar.
    • Key + credential-ref store (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.json 0600.
    • Single sequential writer (writer.rs): sign + sidecar + audit::append under a cross-process dir lock, monotonic timestamps re-clamped to the on-disk latest, bounded mpsc + ack contract, never-aborted append.
    • Sidecar preimage store (sidecar.rs): the chain holds only the response hash, so the redacted {request,response} preimage is persisted per receipt for legibility + recompute.
    • Verification (verify.rs): fail-closedverify_chain + per-record verify_compound against the receipt's signer pubkey within a trusted, time-windowed anchor (trusted_keys.json) + sidecar sha256:+hex(JCS) recompute + tail-truncation detection via expected head.
  • Router::exec choke point: server (handlers.rs) and CLI (exec.rs) both routed through it; CI choke-point guard forbids direct adapter exec in production paths.
  • CLI: relais audit init|pubkey|verify|tail; env-driven enablement (RELAIS_AUDIT_MODE=open|closed); response-open (lossy, logged) vs response-closed (withhold result).
  • Core types: additive, wire-compatible ResponseMeta.receipt: Option<ReceiptHandle>.
  • Specs under 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

  • Redacts the audit log only; caller-facing responses are unchanged (redacting them would break the gateway). No new caller-path exposure.
  • Policy gate + per-agent JWT delegation (C8) deferred to M3.

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 --workspace clean; cargo clippy -p relais-core --features audit --all-targets -D warnings clean.
  • Choke-point guard passes; CLI e2e (init/pubkey/verify fail-closed/verify --head/tail/audit-off) verified.

🤖 Generated with Claude Code

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>

@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: 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);

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 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()))?;

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 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(),

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

@willamhou
willamhou merged commit 123f8b4 into master Jun 22, 2026
2 checks passed
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.

1 participant