polyc-facts 2026.9.0

Shared semantic-fold library: decode-to-fact functions reused by every consumer that reads the event log, so a payment receipt or a tool call means the same thing everywhere it's read.
//! The `grant_replay`-event fold — the SINGLE definition of "how does this
//! signed `grant_replay` audit record's embedded signature read", shared by
//! every consumer that classifies one
//! (`forensics::parse_grant_replay_entry`, `trace::decode_grant_replay_fields`,
//! and the `polyc-query` `grant_replays` typed table), so none can drift on
//! which records they call verified versus a genuine tamper/forgery signal.
//!
//! Mirrors [`crate::approvals`]'s module shape: this file owns the fold,
//! never the decode primitives underneath it — every signature check routes
//! through `polyc_crypto::approval`'s existing verifiers
//! ([`polyc_crypto::approval::verify_grant_replay_pinned`]) — this module
//! adds only the mechanical "pull these fields, classify this signature"
//! plumbing those primitives don't already assemble into one fact. Unlike
//! `approval_request`/`approval_response`, `grant_replay` has no field-level
//! decode helper of its own in `polyc-crypto` (there is no pre-binding schema
//! history to reconcile — `grant_replay` shipped once, already covering
//! `tool` in its signature, `#594`), so the plain-JSON field pull this module
//! does is the same one `forensics::parse_grant_replay_entry` and
//! `trace::decode_grant_replay_fields` already ran independently before this
//! fold existed — now run once, here.
//!
//! # Keep-tagged, like [`crate::approvals`] — NOT dropped like [`crate::receipts`]
//!
//! [`crate::receipts::verified_receipts`] DROPS a payment receipt that fails
//! signature verification. A `grant_replay` audit record is different, for
//! the same reason an `approval_response` is: the `/grant-replays` forensics
//! surface (`crates/control-plane/src/forensics.rs`'s `collect_grant_replays`)
//! exists to show exactly this as an audit signal — a remembered grant that
//! claims to have kept a capability taint would have revoked, tagged
//! `INVALID` when the claim doesn't check out, not silently hidden.
//! [`fold_grant_replay_event`] therefore returns a fact for every
//! structurally-decodable `grant_replay` payload regardless of its signature
//! status; only a payload too malformed to yield even `tool`/`grant_ref`/
//! `covered_capabilities`/`coverage_hash` decodes to `None`. Redaction of the
//! raw `signer_public_key` for a non-maintainer query scope is the query
//! layer's job (`crates/query/src/decode/grant_replays.rs`), not this fold's
//! — this fold returns everything it can recover.
//!
//! # No legacy variant
//!
//! [`GrantReplaySignatureStatus`] has no third `LegacyUnverifiable` arm the
//! way [`crate::approvals::ApprovalSignatureStatus`] does: `grant_replay`
//! (`#594`) was never shipped in a pre-binding shape this build must still
//! read, so every structurally-decodable record is unambiguously
//! [`GrantReplaySignatureStatus::Verified`] or
//! [`GrantReplaySignatureStatus::Invalid`].

use polyc_crypto::approval;
use polyc_eventlog_model::Event;
use polyc_proto::kinds;
use serde_json::Value;

/// How a `grant_replay`'s embedded signature reads.
///
/// The shared classification [`fold_grant_replay_event`] computes, and the
/// single enum `forensics::parse_grant_replay_entry`/
/// `trace::decode_grant_replay_fields` both now read through instead of each
/// independently re-deriving it.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GrantReplaySignatureStatus {
    /// Verifies against the embedded key AND that key is a member of the
    /// deployment's trusted-signer allow-list
    /// ([`polyc_crypto::approval::verify_grant_replay_pinned`]).
    Verified,
    /// The signature does NOT verify, or verifies against a key outside the
    /// trusted-signer allow-list — a genuine tamper/forgery signal.
    Invalid,
}

/// A decoded historical `polychrome.events.v1.GrantReplayEvent` (`#594`), plus
/// the classified signature.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GrantReplayFact {
    /// The tool whose call the grant cleared.
    pub tool: String,
    /// The grant that cleared it: sha256-hex of its full signed payload.
    pub grant_ref: String,
    /// The capability names the grant kept against taint.
    pub covered_capabilities: Vec<String>,
    /// The template-coverage hash the grant matched (`#618`).
    pub coverage_hash: String,
    /// How this record's embedded signature reads — see
    /// [`GrantReplaySignatureStatus`].
    pub signature_status: GrantReplaySignatureStatus,
    /// The embedded signer public key, when the payload carried a
    /// non-empty `signed_by`, regardless of whether it verifies — a
    /// consumer needs the raw key to show WHO signed an `Invalid` record,
    /// same as [`crate::approvals::ApprovalResponseFact::signer_public_key`].
    pub signer_public_key: Option<Vec<u8>>,
}

/// Fold one event into a [`GrantReplayFact`].
///
/// `None` if it is not a `grant_replay` event, or its payload is too
/// malformed to yield even the minimal identity a fact needs: `tool`,
/// `grant_ref`, `covered_capabilities` (a JSON array), and `coverage_hash`
/// all present — the same four-field gate
/// `forensics::parse_grant_replay_entry` always applied. A record that
/// fails [`polyc_crypto::approval::verify_grant_replay_pinned`] is still
/// returned, tagged [`GrantReplaySignatureStatus::Invalid`] — see the module
/// docs' "Keep-tagged" section.
#[must_use]
pub fn fold_grant_replay_event(
    event: &Event,
    trusted_signers: &[Vec<u8>],
) -> Option<GrantReplayFact> {
    let (base, _turn_id) = kinds::parse(&event.kind);
    if base != kinds::GRANT_REPLAY {
        return None;
    }
    fold_grant_replay_payload(&event.payload, trusted_signers)
}

/// Fold a `grant_replay` payload — `None` only if `tool`/`grant_ref`/
/// `covered_capabilities`/`coverage_hash` cannot all be recovered (a
/// structurally-malformed payload).
fn fold_grant_replay_payload(
    payload: &[u8],
    trusted_signers: &[Vec<u8>],
) -> Option<GrantReplayFact> {
    let v: Value = serde_json::from_slice(payload).ok()?;
    let tool = v.get("tool")?.as_str()?.to_owned();
    let grant_ref = v.get("grant_ref")?.as_str()?.to_owned();
    let covered_capabilities: Vec<String> = v
        .get("covered_capabilities")?
        .as_array()?
        .iter()
        .filter_map(|c| c.as_str().map(str::to_owned))
        .collect();
    let coverage_hash = v.get("coverage_hash")?.as_str()?.to_owned();
    let signature_status = if approval::verify_grant_replay_pinned(payload, trusted_signers) {
        GrantReplaySignatureStatus::Verified
    } else {
        GrantReplaySignatureStatus::Invalid
    };
    let signer_public_key = v
        .get("signed_by")
        .and_then(Value::as_str)
        .and_then(polyc_crypto::hex::decode);
    Some(GrantReplayFact {
        tool,
        grant_ref,
        covered_capabilities,
        coverage_hash,
        signature_status,
        signer_public_key,
    })
}

#[cfg(test)]
mod tests {
    #![allow(clippy::pedantic, clippy::nursery, missing_docs, clippy::unwrap_used)]

    use polyc_crypto::approval::{ApprovalSigner, test_util::grant_replay_payload};

    use super::*;

    fn grant_replay_event(
        conversation_id: &str,
        turn_id: &str,
        tool: &str,
        grant_ref: &str,
        covered_capabilities: &[String],
        coverage_hash: &str,
        signer: &ApprovalSigner,
    ) -> Event {
        let (payload, _sig, _pk) = grant_replay_payload(
            conversation_id,
            turn_id,
            tool,
            grant_ref,
            covered_capabilities,
            coverage_hash,
            signer,
        );
        Event::new(kinds::GRANT_REPLAY.to_owned(), payload)
    }

    #[test]
    fn fold_grant_replay_event_verifies_a_trusted_signer_record() {
        let signer = ApprovalSigner::from_seed(11);
        let event = grant_replay_event(
            "conv-1",
            "turn-1",
            "read_file",
            "grant-ref-1",
            &["fs.read".to_owned()],
            "hash-1",
            &signer,
        );
        let fact = fold_grant_replay_event(&event, &[signer.public_key_bytes()])
            .expect("a structurally valid record always folds to a fact");
        assert_eq!(fact.tool, "read_file");
        assert_eq!(fact.grant_ref, "grant-ref-1");
        assert_eq!(fact.covered_capabilities, vec!["fs.read".to_owned()]);
        assert_eq!(fact.coverage_hash, "hash-1");
        assert_eq!(fact.signature_status, GrantReplaySignatureStatus::Verified);
        assert_eq!(
            fact.signer_public_key.as_deref(),
            Some(signer.public_key_bytes().as_slice())
        );
    }

    /// The contrast with [`crate::receipts`]'s drop test: a record signed
    /// by a key OUTSIDE `trusted_signers` is internally consistent but
    /// untrusted — [`crate::receipts::verified_receipts`] would drop this,
    /// but the grant-replay fold keeps the row, tagged `Invalid`, as the
    /// audit signal the `/grant-replays` surface exists to show.
    #[test]
    fn fold_grant_replay_event_keeps_an_untrusted_signer_record_tagged_invalid() {
        let trusted = ApprovalSigner::from_seed(12);
        let untrusted = ApprovalSigner::from_seed(13);
        let event = grant_replay_event(
            "conv-1",
            "turn-1",
            "read_file",
            "grant-ref-2",
            &["fs.read".to_owned()],
            "hash-2",
            &untrusted,
        );
        let fact = fold_grant_replay_event(&event, &[trusted.public_key_bytes()])
            .expect("a structurally valid record always folds to a fact");
        assert_eq!(fact.signature_status, GrantReplaySignatureStatus::Invalid);
        assert_eq!(
            fact.tool, "read_file",
            "the row is still present with its claimed fields"
        );
        assert_eq!(
            fact.signer_public_key.as_deref(),
            Some(untrusted.public_key_bytes().as_slice()),
            "the untrusted signer's own key is still surfaced, for audit"
        );
    }

    #[test]
    fn fold_grant_replay_event_drops_a_structurally_malformed_record() {
        let event = Event::new(kinds::GRANT_REPLAY.to_owned(), vec![0xFF, 0xFE]);
        assert_eq!(fold_grant_replay_event(&event, &[]), None);
    }

    #[test]
    fn fold_grant_replay_event_drops_a_record_missing_a_required_field() {
        let event = Event::new(
            kinds::GRANT_REPLAY.to_owned(),
            br#"{"tool":"read_file","grant_ref":"g1"}"#.to_vec(),
        );
        assert_eq!(fold_grant_replay_event(&event, &[]), None);
    }

    #[test]
    fn fold_grant_replay_event_ignores_unrelated_kinds() {
        let event = Event::new(kinds::USAGE.to_owned(), Vec::new());
        assert_eq!(fold_grant_replay_event(&event, &[]), None);
    }
}