Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T02:02:57.607Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

community_verification.rs

585 lines · 22.9 KB · rust
1//! The verifier-signed independent verification event (omega#48).
2//!
3//! Contract: `SARAH-CW-00` §8.4, amendment `SARAH-CW-00-A1` (2026-07-25), in
4//! `docs/omega/2026-07-24-community-workroom-contract.md` in the openagents
5//! repository.
6//!
7//! ## Why this exists on the Rust side at all
8//!
9//! The contract's §4 writable-authority table already reserved a row for
10//! independent verification with its authority named and its wire form left
11//! unspecified. Every client therefore read independence off Sarah's
12//! *arbitration decision* — the deciding key asserting, on the verifier's
13//! behalf, that a verification happened. Step 6 of the §8.1 lifecycle was the
14//! one step nobody signed.
15//!
16//! The amendment specifies the wire form. A second implementation must be able
17//! to reach the same admission from the contract and the fixtures alone, so
18//! this module decodes the **same fixture bytes** as
19//! `packages/sarah/src/community-arbitration/verification.ts`. The digests in
20//! [`SHARED_FIXTURE_DIGESTS`] are what makes that enforceable rather than
21//! conventional: edit a fixture in either repository and that repository's test
22//! goes red until the other agrees.
23//!
24//! ## What this module does not do
25//!
26//! It admits a verification. It never accepts a unit — acceptance is Sarah's,
27//! per the §4 acceptance row — and it holds no state.
28
29use std::collections::{BTreeMap, BTreeSet};
30
31use serde::Deserialize;
32
33/// Schema id of the record the verifying agent signs.
34pub const COMMUNITY_INDEPENDENT_VERIFICATION_SCHEMA: &str =
35    "openagents.sarah.community_independent_verification.v1";
36
37/// Amendment packet that specified this wire form.
38pub const COMMUNITY_INDEPENDENT_VERIFICATION_PACKET: &str = "SARAH-CW-00-A1";
39
40/// NIP-90 feedback kind. The carrier the contract already used for the quote,
41/// the acceptance, the decision, the appeal and the ruling. No new kind.
42pub const COMMUNITY_FEEDBACK_KIND: u32 = 7000;
43
44/// The `cw_feedback_type` value. A fourth value in an existing discriminator.
45pub const INDEPENDENT_VERIFICATION_FEEDBACK_TYPE: &str = "independent_verification";
46
47/// The verifier's verdict.
48///
49/// Deliberately not `accepted` / `rejected`: those are Sarah's words and this
50/// event must not be mistakable for the decision.
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum VerificationVerdict {
53    Reproduced,
54    NotReproduced,
55    Inconclusive,
56}
57
58impl VerificationVerdict {
59    #[must_use]
60    pub const fn token(self) -> &'static str {
61        match self {
62            Self::Reproduced => "reproduced",
63            Self::NotReproduced => "not_reproduced",
64            Self::Inconclusive => "inconclusive",
65        }
66    }
67
68    #[must_use]
69    pub fn parse_token(token: &str) -> Option<Self> {
70        match token {
71            "reproduced" => Some(Self::Reproduced),
72            "not_reproduced" => Some(Self::NotReproduced),
73            "inconclusive" => Some(Self::Inconclusive),
74            _ => None,
75        }
76    }
77}
78
79/// Why a verification event did not become an admitted verification.
80///
81/// Every one of these is a refusal a reader can render. A verification that
82/// vanishes silently is indistinguishable from one that was never published.
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum VerificationRefusal {
85    NotAVerificationEvent,
86    Malformed,
87    /// Somebody other than the verifier signed the verifier's claim. The state
88    /// this amendment exists to end.
89    VerifierNotAuthor,
90    VerifierKeyBurned,
91    VerifierBindingUnconfirmed,
92    ProducerBindingUnconfirmed,
93    SelfDealingOperators,
94    DecidesPaymentForbidden,
95}
96
97impl VerificationRefusal {
98    #[must_use]
99    pub const fn token(self) -> &'static str {
100        match self {
101            Self::NotAVerificationEvent => "not_a_verification_event",
102            Self::Malformed => "malformed",
103            Self::VerifierNotAuthor => "verifier_not_author",
104            Self::VerifierKeyBurned => "verifier_key_burned",
105            Self::VerifierBindingUnconfirmed => "verifier_binding_unconfirmed",
106            Self::ProducerBindingUnconfirmed => "producer_binding_unconfirmed",
107            Self::SelfDealingOperators => "self_dealing_operators",
108            Self::DecidesPaymentForbidden => "decides_payment_forbidden",
109        }
110    }
111}
112
113/// An admitted verification, with both operators read from the folded record.
114#[derive(Debug, Clone, PartialEq, Eq)]
115pub struct AdmittedVerification {
116    pub source_event_id: String,
117    pub result_event_id: String,
118    pub verifier_agent_pubkey: String,
119    /// From the binding. Never from a tag.
120    pub verifier_operator_pubkey: String,
121    pub producer_agent_pubkey: String,
122    /// From the binding. Never from a tag.
123    pub producer_operator_pubkey: String,
124    pub verdict: VerificationVerdict,
125}
126
127/// A signed event as it arrives from a relay.
128#[derive(Debug, Clone, Deserialize)]
129pub struct VerificationEvent {
130    pub id: String,
131    pub pubkey: String,
132    pub created_at: i64,
133    pub kind: u32,
134    pub tags: Vec<Vec<String>>,
135    pub content: String,
136}
137
138/// The agent → operator binding a caller resolves from its folded record.
139///
140/// A map plus a burn set rather than the whole ledger: this module has no
141/// business folding membership, and taking only what it reads makes it obvious
142/// that nothing here can be talked into a binding by an event.
143#[derive(Debug, Clone, Default)]
144pub struct CommunityBinding {
145    operator_by_agent: BTreeMap<String, String>,
146    burned_agent_keys: BTreeSet<String>,
147}
148
149impl CommunityBinding {
150    #[must_use]
151    pub fn new() -> Self {
152        Self::default()
153    }
154
155    #[must_use]
156    pub fn bind(mut self, agent_pubkey: &str, operator_pubkey: &str) -> Self {
157        self.operator_by_agent
158            .insert(agent_pubkey.trim().to_ascii_lowercase(), operator_pubkey.trim().to_ascii_lowercase());
159        self
160    }
161
162    #[must_use]
163    pub fn burn(mut self, agent_pubkey: &str) -> Self {
164        self.burned_agent_keys
165            .insert(agent_pubkey.trim().to_ascii_lowercase());
166        self
167    }
168
169    #[must_use]
170    pub fn operator_for_agent(&self, agent_pubkey: &str) -> Option<&str> {
171        self.operator_by_agent
172            .get(&agent_pubkey.trim().to_ascii_lowercase())
173            .map(String::as_str)
174    }
175
176    #[must_use]
177    pub fn is_agent_key_burned(&self, agent_pubkey: &str) -> bool {
178        self.burned_agent_keys
179            .contains(&agent_pubkey.trim().to_ascii_lowercase())
180    }
181}
182
183fn tag_value<'a>(event: &'a VerificationEvent, name: &str) -> Option<&'a str> {
184    event
185        .tags
186        .iter()
187        .find(|tag| tag.first().map(String::as_str) == Some(name))
188        .and_then(|tag| tag.get(1))
189        .map(String::as_str)
190}
191
192fn tagged_event_id<'a>(event: &'a VerificationEvent, marker: &str) -> Option<&'a str> {
193    event
194        .tags
195        .iter()
196        .find(|tag| {
197            tag.first().map(String::as_str) == Some("e")
198                && tag.get(3).map(String::as_str) == Some(marker)
199        })
200        .and_then(|tag| tag.get(1))
201        .map(String::as_str)
202}
203
204fn is_hex_64(value: &str) -> bool {
205    value.len() == 64 && value.bytes().all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
206}
207
208/// Admit one signed verification event against the folded record.
209///
210/// Both operators come from `binding`, never from a tag. `cw_verifier_operator_ref`
211/// is what the *verifier* asserts about itself, and an agent key asserting its
212/// own operator is exactly the self-dealing shape refused on the decision path.
213///
214/// # Errors
215///
216/// Returns the typed refusal. There is no silent drop.
217pub fn admit_independent_verification(
218    event: &VerificationEvent,
219    binding: &CommunityBinding,
220) -> Result<AdmittedVerification, VerificationRefusal> {
221    let verifier_key = event.pubkey.trim().to_ascii_lowercase();
222
223    let feedback_type = tag_value(event, "cw_feedback_type")
224        .or_else(|| tag_value(event, "lbr_feedback_type"));
225    if event.kind != COMMUNITY_FEEDBACK_KIND
226        || feedback_type != Some(INDEPENDENT_VERIFICATION_FEEDBACK_TYPE)
227    {
228        return Err(VerificationRefusal::NotAVerificationEvent);
229    }
230
231    let result_event_id = tagged_event_id(event, "result").unwrap_or_default().to_owned();
232    let producer_key = tag_value(event, "cw_producer_agent_pubkey")
233        .or_else(|| tag_value(event, "p"))
234        .unwrap_or_default()
235        .trim()
236        .to_ascii_lowercase();
237    let verdict = tag_value(event, "status").and_then(VerificationVerdict::parse_token);
238
239    let Some(verdict) = verdict else {
240        return Err(VerificationRefusal::Malformed);
241    };
242    if !is_hex_64(&result_event_id) || !is_hex_64(&producer_key) {
243        return Err(VerificationRefusal::Malformed);
244    }
245
246    if tag_value(event, "cw_decides_payment") != Some("false") {
247        return Err(VerificationRefusal::DecidesPaymentForbidden);
248    }
249
250    // The whole point of the amendment.
251    if let Some(claimed) = tag_value(event, "cw_verifier_agent_pubkey")
252        && claimed.trim().to_ascii_lowercase() != verifier_key
253    {
254        return Err(VerificationRefusal::VerifierNotAuthor);
255    }
256    if verifier_key == producer_key {
257        return Err(VerificationRefusal::SelfDealingOperators);
258    }
259
260    // Revocation binds the subject whatever it signs and whenever it arrives.
261    // Checked before the binding lookup so a burned key with a live binding row
262    // cannot pass as merely "unconfirmed".
263    if binding.is_agent_key_burned(&verifier_key) {
264        return Err(VerificationRefusal::VerifierKeyBurned);
265    }
266
267    let Some(verifier_operator) = binding.operator_for_agent(&verifier_key) else {
268        return Err(VerificationRefusal::VerifierBindingUnconfirmed);
269    };
270    if let Some(claimed_operator) = tag_value(event, "cw_verifier_operator_ref")
271        && claimed_operator.trim().to_ascii_lowercase() != verifier_operator
272    {
273        return Err(VerificationRefusal::VerifierBindingUnconfirmed);
274    }
275
276    let Some(producer_operator) = binding.operator_for_agent(&producer_key) else {
277        return Err(VerificationRefusal::ProducerBindingUnconfirmed);
278    };
279
280    // Distinct *operators*, not merely distinct keys. One operator holding two
281    // agent keys passes every key comparison above.
282    if producer_operator == verifier_operator {
283        return Err(VerificationRefusal::SelfDealingOperators);
284    }
285
286    Ok(AdmittedVerification {
287        source_event_id: event.id.clone(),
288        result_event_id,
289        verifier_agent_pubkey: verifier_key,
290        verifier_operator_pubkey: verifier_operator.to_owned(),
291        producer_agent_pubkey: producer_key,
292        producer_operator_pubkey: producer_operator.to_owned(),
293        verdict,
294    })
295}
296
297/// The exact bytes both repositories hold, by SHA-256 of the file content.
298///
299/// Not a convenience. A fixture is only "byte-shared" if drift is detectable
300/// from inside one repository without reading the other.
301pub const SHARED_FIXTURE_DIGESTS: &[(&str, &str)] = &[
302    (
303        "openagents.sarah.community_independent_verification.v1.canonical.json",
304        "992085428cf36e74ee0f1b6bdb6b38c494e7b7cc1b68d668e82bef74fcfa928f",
305    ),
306    (
307        "openagents.sarah.community_independent_verification.v1.negative-operators-not-independent.json",
308        "35061fb7846a3fa961ac7335e96dd9cbd21b080ba5bf2fe0389402ce22d3333b",
309    ),
310    (
311        "openagents.sarah.community_independent_verification.v1.negative-verifier-key-burned.json",
312        "0a01c8665d52cf0542084897bc79eac52a5cca3b3ae0dfb725ead4f5a8dbcc36",
313    ),
314    (
315        "openagents.sarah.community_independent_verification.v1.negative-verifier-not-author.json",
316        "c2f91af69fe979acdcbe8550060d782c72d963c7346d8d6febb5705f87338271",
317    ),
318];
319
320#[cfg(test)]
321mod tests {
322    use super::*;
323
324    const CANONICAL: &str = include_str!(
325        "../fixtures/openagents.sarah.community_independent_verification.v1.canonical.json"
326    );
327    const NOT_AUTHOR: &str = include_str!(
328        "../fixtures/openagents.sarah.community_independent_verification.v1.negative-verifier-not-author.json"
329    );
330    const NOT_INDEPENDENT: &str = include_str!(
331        "../fixtures/openagents.sarah.community_independent_verification.v1.negative-operators-not-independent.json"
332    );
333    const BURNED: &str = include_str!(
334        "../fixtures/openagents.sarah.community_independent_verification.v1.negative-verifier-key-burned.json"
335    );
336
337    #[derive(Debug, Deserialize)]
338    struct BoundAgent {
339        #[serde(rename = "agentPubkey")]
340        agent_pubkey: String,
341        #[serde(rename = "operatorPubkey")]
342        operator_pubkey: String,
343    }
344
345    #[derive(Debug, Deserialize)]
346    struct FixtureBinding {
347        agents: Vec<BoundAgent>,
348        #[serde(rename = "burnedAgentKeys")]
349        burned_agent_keys: Vec<String>,
350    }
351
352    #[derive(Debug, Deserialize)]
353    struct Fixture {
354        fixture: String,
355        packet: String,
356        expect: String,
357        binding: FixtureBinding,
358        event: VerificationEvent,
359        expected: serde_json::Value,
360    }
361
362    fn load(raw: &str) -> (Fixture, CommunityBinding) {
363        let fixture: Fixture = serde_json::from_str(raw).expect("fixture decodes");
364        let mut binding = CommunityBinding::new();
365        for agent in &fixture.binding.agents {
366            binding = binding.bind(&agent.agent_pubkey, &agent.operator_pubkey);
367        }
368        for key in &fixture.binding.burned_agent_keys {
369            binding = binding.burn(key);
370        }
371        (fixture, binding)
372    }
373
374    /// The cross-repository check. These bytes are the same bytes
375    /// `packages/sarah` decodes, and this test is what notices if they stop
376    /// being.
377    #[test]
378    fn the_shared_fixtures_are_the_exact_bytes_the_typescript_side_holds() {
379        use std::fmt::Write as _;
380        for (name, expected) in SHARED_FIXTURE_DIGESTS {
381            let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
382                .join("fixtures")
383                .join(name);
384            let bytes = std::fs::read(&path).expect("shared fixture is readable");
385            let digest = sha256_hex(&bytes, &mut String::new());
386            let mut rendered = String::new();
387            write!(&mut rendered, "{digest}").expect("write");
388            assert_eq!(
389                &rendered, expected,
390                "{name} drifted from the byte-shared copy in packages/sarah. \
391                 Re-share the file rather than re-pinning one side."
392            );
393        }
394    }
395
396    /// Minimal SHA-256 so this crate keeps its two-dependency footprint.
397    fn sha256_hex(bytes: &[u8], scratch: &mut String) -> String {
398        const K: [u32; 64] = [
399            0x428a_2f98, 0x7137_4491, 0xb5c0_fbcf, 0xe9b5_dba5, 0x3956_c25b, 0x59f1_11f1,
400            0x923f_82a4, 0xab1c_5ed5, 0xd807_aa98, 0x1283_5b01, 0x2431_85be, 0x550c_7dc3,
401            0x72be_5d74, 0x80de_b1fe, 0x9bdc_06a7, 0xc19b_f174, 0xe49b_69c1, 0xefbe_4786,
402            0x0fc1_9dc6, 0x240c_a1cc, 0x2de9_2c6f, 0x4a74_84aa, 0x5cb0_a9dc, 0x76f9_88da,
403            0x983e_5152, 0xa831_c66d, 0xb003_27c8, 0xbf59_7fc7, 0xc6e0_0bf3, 0xd5a7_9147,
404            0x06ca_6351, 0x1429_2967, 0x27b7_0a85, 0x2e1b_2138, 0x4d2c_6dfc, 0x5338_0d13,
405            0x650a_7354, 0x766a_0abb, 0x81c2_c92e, 0x9272_2c85, 0xa2bf_e8a1, 0xa81a_664b,
406            0xc24b_8b70, 0xc76c_51a3, 0xd192_e819, 0xd699_0624, 0xf40e_3585, 0x106a_a070,
407            0x19a4_c116, 0x1e37_6c08, 0x2748_774c, 0x34b0_bcb5, 0x391c_0cb3, 0x4ed8_aa4a,
408            0x5b9c_ca4f, 0x682e_6ff3, 0x748f_82ee, 0x78a5_636f, 0x84c8_7814, 0x8cc7_0208,
409            0x90be_fffa, 0xa450_6ceb, 0xbef9_a3f7, 0xc671_78f2,
410        ];
411        let mut h: [u32; 8] = [
412            0x6a09_e667, 0xbb67_ae85, 0x3c6e_f372, 0xa54f_f53a, 0x510e_527f, 0x9b05_688c,
413            0x1f83_d9ab, 0x5be0_cd19,
414        ];
415        let mut message = bytes.to_vec();
416        let bit_len = (bytes.len() as u64) * 8;
417        message.push(0x80);
418        while message.len() % 64 != 56 {
419            message.push(0);
420        }
421        message.extend_from_slice(&bit_len.to_be_bytes());
422
423        for chunk in message.chunks_exact(64) {
424            let mut w = [0u32; 64];
425            for (index, word) in w.iter_mut().enumerate().take(16) {
426                let base = index * 4;
427                *word = u32::from_be_bytes([
428                    chunk[base],
429                    chunk[base + 1],
430                    chunk[base + 2],
431                    chunk[base + 3],
432                ]);
433            }
434            for index in 16..64 {
435                let s0 = w[index - 15].rotate_right(7)
436                    ^ w[index - 15].rotate_right(18)
437                    ^ (w[index - 15] >> 3);
438                let s1 = w[index - 2].rotate_right(17)
439                    ^ w[index - 2].rotate_right(19)
440                    ^ (w[index - 2] >> 10);
441                w[index] = w[index - 16]
442                    .wrapping_add(s0)
443                    .wrapping_add(w[index - 7])
444                    .wrapping_add(s1);
445            }
446            let mut v = h;
447            for index in 0..64 {
448                let s1 = v[4].rotate_right(6) ^ v[4].rotate_right(11) ^ v[4].rotate_right(25);
449                let ch = (v[4] & v[5]) ^ ((!v[4]) & v[6]);
450                let temp1 = v[7]
451                    .wrapping_add(s1)
452                    .wrapping_add(ch)
453                    .wrapping_add(K[index])
454                    .wrapping_add(w[index]);
455                let s0 = v[0].rotate_right(2) ^ v[0].rotate_right(13) ^ v[0].rotate_right(22);
456                let maj = (v[0] & v[1]) ^ (v[0] & v[2]) ^ (v[1] & v[2]);
457                let temp2 = s0.wrapping_add(maj);
458                v[7] = v[6];
459                v[6] = v[5];
460                v[5] = v[4];
461                v[4] = v[3].wrapping_add(temp1);
462                v[3] = v[2];
463                v[2] = v[1];
464                v[1] = v[0];
465                v[0] = temp1.wrapping_add(temp2);
466            }
467            for (slot, value) in h.iter_mut().zip(v) {
468                *slot = slot.wrapping_add(value);
469            }
470        }
471        scratch.clear();
472        let mut out = String::with_capacity(64);
473        for word in h {
474            out.push_str(&format!("{word:08x}"));
475        }
476        out
477    }
478
479    #[test]
480    fn the_canonical_fixture_admits_with_both_operators_from_the_record() {
481        let (fixture, binding) = load(CANONICAL);
482        assert_eq!(fixture.fixture, COMMUNITY_INDEPENDENT_VERIFICATION_SCHEMA);
483        assert_eq!(fixture.packet, COMMUNITY_INDEPENDENT_VERIFICATION_PACKET);
484        assert_eq!(fixture.expect, "admit");
485
486        let admitted = admit_independent_verification(&fixture.event, &binding)
487            .expect("the canonical verification is admitted");
488        assert_eq!(admitted.verifier_agent_pubkey, fixture.event.pubkey);
489        assert_ne!(
490            admitted.verifier_operator_pubkey,
491            admitted.producer_operator_pubkey
492        );
493        assert_eq!(admitted.verdict, VerificationVerdict::Reproduced);
494        assert_eq!(
495            admitted.verifier_operator_pubkey,
496            fixture.expected["verifierOperatorPubkey"].as_str().unwrap()
497        );
498        assert_eq!(
499            admitted.producer_operator_pubkey,
500            fixture.expected["producerOperatorPubkey"].as_str().unwrap()
501        );
502    }
503
504    /// The exact reason the amendment exists: the verifier signs, or nobody
505    /// did.
506    #[test]
507    fn a_verification_signed_by_somebody_other_than_the_verifier_is_refused() {
508        let (fixture, binding) = load(NOT_AUTHOR);
509        let refusal = admit_independent_verification(&fixture.event, &binding)
510            .expect_err("an impostor signature is refused");
511        assert_eq!(refusal, VerificationRefusal::VerifierNotAuthor);
512        assert_eq!(refusal.token(), fixture.expected["code"].as_str().unwrap());
513    }
514
515    #[test]
516    fn distinct_keys_under_one_operator_are_not_independent() {
517        let (fixture, binding) = load(NOT_INDEPENDENT);
518        let refusal = admit_independent_verification(&fixture.event, &binding)
519            .expect_err("one operator on both sides is self-dealing");
520        assert_eq!(refusal, VerificationRefusal::SelfDealingOperators);
521        assert_eq!(refusal.token(), fixture.expected["code"].as_str().unwrap());
522    }
523
524    /// A revocation binds the subject regardless of arrival order, and it binds
525    /// it whatever the key goes on to sign.
526    #[test]
527    fn a_burned_verifier_key_is_refused_even_with_a_live_binding_row() {
528        let (fixture, binding) = load(BURNED);
529        assert!(
530            binding.operator_for_agent(&fixture.event.pubkey).is_some(),
531            "the burn must be what refuses this, not a missing binding"
532        );
533        let refusal = admit_independent_verification(&fixture.event, &binding)
534            .expect_err("a burned key verifies nothing");
535        assert_eq!(refusal, VerificationRefusal::VerifierKeyBurned);
536    }
537
538    /// `cw_verifier_operator_ref` is the verifier's own claim about itself.
539    /// Believing it would rebuild the self-dealing hole on the new carrier.
540    #[test]
541    fn a_verifier_claiming_an_operator_the_record_denies_is_refused() {
542        let (mut fixture, binding) = load(CANONICAL);
543        for tag in &mut fixture.event.tags {
544            if tag.first().map(String::as_str) == Some("cw_verifier_operator_ref") {
545                tag[1] = "5".repeat(64);
546            }
547        }
548        let refusal = admit_independent_verification(&fixture.event, &binding)
549            .expect_err("a self-asserted operator is not a binding");
550        assert_eq!(refusal, VerificationRefusal::VerifierBindingUnconfirmed);
551    }
552
553    /// This event is testimony, not a verdict. A verification that could say
554    /// `accepted` would be a second acceptance authority.
555    #[test]
556    fn a_verification_cannot_carry_sarahs_words() {
557        assert!(VerificationVerdict::parse_token("accepted").is_none());
558        assert!(VerificationVerdict::parse_token("rejected").is_none());
559        let (mut fixture, binding) = load(CANONICAL);
560        for tag in &mut fixture.event.tags {
561            if tag.first().map(String::as_str) == Some("status") {
562                tag[1] = "accepted".to_owned();
563            }
564        }
565        assert_eq!(
566            admit_independent_verification(&fixture.event, &binding),
567            Err(VerificationRefusal::Malformed)
568        );
569    }
570
571    #[test]
572    fn a_verification_that_does_not_disclaim_payment_is_refused() {
573        let (mut fixture, binding) = load(CANONICAL);
574        for tag in &mut fixture.event.tags {
575            if tag.first().map(String::as_str) == Some("cw_decides_payment") {
576                tag[1] = "true".to_owned();
577            }
578        }
579        assert_eq!(
580            admit_independent_verification(&fixture.event, &binding),
581            Err(VerificationRefusal::DecidesPaymentForbidden)
582        );
583    }
584}
585
Served at tenant.openagents/omega Member data and write actions are omitted.