Skip to main content

qualia_core_db/mcp/
mcp_cooperation.rs

1//! MCP agent cooperation (Track M — task #17/#16/#18).
2//!
3//! The cooperation gate that has long been missing: every MCP call should carry a
4//! **verified, typed calling-agent identity + standpoint** (who is asking, in what role),
5//! and the request should be evaluated against the rights ontology *before* execution.
6//!
7//! This is load-bearing for the whole thesis — agents verify each other's conduct; **trust
8//! is behaviourally derived, not self-asserted** ([[feedback-trust-is-behaviourally-derived]]);
9//! and there is no platform-provider deciding for everyone. The gate composes three existing
10//! pieces rather than inventing a fourth:
11//!   1. **Verified, not asserted** — the caller's identity must be cryptographically verified
12//!      (a signed VC via [`crate::verifiable_credential`]), not merely claimed.
13//!   2. **Grounded** — an artificial agent with no human Principal is refused
14//!      ([`crate::agent::is_ungrounded_agency`], agency.n3 G1').
15//!   3. **Governed** — the request is run through the deontic policy gate
16//!      ([`crate::modalities::interaction_governance::map_policy`], Phase 6).
17//!
18//! Mandatory per-call enforcement in the dispatch is a deliberate MCP-contract change (it
19//! fails closed on unverified callers) and is gated on Timothy's sign-off — see
20//! DEONTIC_LOGIC_PLAN Track M. This module is the mechanism + an opt-in tool; it does not
21//! silently change every existing caller's behaviour.
22
23use crate::indexing::QuinIndex;
24use crate::modalities::interaction_governance::{
25    map_policy, permits_execution, Governance, PolicyMode,
26};
27use crate::modalities::logic::deontic::DeonticStatus;
28
29/// Who is calling, in what typed role, and whether their identity was *verified* (vs merely
30/// asserted). `agent` and `role` are identifier hashes (one identity space, #14).
31#[derive(Debug, Clone, Copy)]
32pub struct CallerStandpoint {
33    /// The calling agent's identifier.
34    pub agent: u64,
35    /// Its typed role / standpoint for this call (a values class or capability).
36    pub role: u64,
37    /// True iff the identity was cryptographically verified (a signed VC), not just claimed.
38    pub verified: bool,
39}
40
41/// The outcome of the cooperation gate.
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum CooperationVerdict {
44    /// The call may proceed, under this runtime policy mode.
45    Authorized(PolicyMode),
46    /// Refused: the caller's identity was asserted, not verified.
47    DeniedUnverified,
48    /// Refused: the caller is an artificial agent with no human Principal (agency.n3 G1').
49    DeniedUngrounded,
50    /// Refused by the deontic gate (e.g. a non-derogable violation → PreventiveBlock, or an
51    /// ambiguous mapping → Interactive). Carries the mode so the caller knows why.
52    DeniedByPolicy(PolicyMode),
53}
54
55/// Is a caller grounded — i.e. NOT an ungrounded artificial agent? (A human, a legal person,
56/// or an AI with a declared `values:operatedBy` Principal all pass.)
57#[inline]
58pub fn caller_grounded(index: &QuinIndex, agent: u64) -> bool {
59    !crate::agent::is_ungrounded_agency(index, agent)
60}
61
62/// The cooperation gate, with grounding supplied explicitly (index-free; used by the tool and
63/// by callers that already know the caller's grounding). Order: verified → grounded → governed.
64pub fn authorize(
65    standpoint: &CallerStandpoint,
66    grounded: bool,
67    request_status: DeonticStatus,
68    governance: Governance,
69) -> CooperationVerdict {
70    if !standpoint.verified {
71        return CooperationVerdict::DeniedUnverified;
72    }
73    if !grounded {
74        return CooperationVerdict::DeniedUngrounded;
75    }
76    let mode = map_policy(request_status, governance);
77    if permits_execution(mode) {
78        CooperationVerdict::Authorized(mode)
79    } else {
80        CooperationVerdict::DeniedByPolicy(mode)
81    }
82}
83
84/// The cooperation gate over the live graph: resolves the caller's grounding from `index`
85/// (agency.n3 G1'), then applies [`authorize`].
86pub fn authorize_call(
87    index: &QuinIndex,
88    standpoint: &CallerStandpoint,
89    request_status: DeonticStatus,
90    governance: Governance,
91) -> CooperationVerdict {
92    authorize(
93        standpoint,
94        caller_grounded(index, standpoint.agent),
95        request_status,
96        governance,
97    )
98}
99
100/// Is mandatory per-call MCP enforcement turned ON? Default **OFF** (opt-in via the env flag
101/// `QUALIA_MCP_ENFORCE=1`), so today's callers are unaffected; flip it per-deployment when
102/// callers can supply a verified, grounded standpoint. When ON, every dispatched MCP call must
103/// pass [`authorize`] or it is refused. (Track M — the breaking-change switch, held by the operator.)
104pub fn enforcement_enabled() -> bool {
105    matches!(
106        std::env::var("QUALIA_MCP_ENFORCE").ok().as_deref(),
107        Some("1") | Some("true") | Some("TRUE") | Some("on")
108    )
109}
110
111/// Stable label for logs / MCP responses.
112pub const fn cooperation_label(v: CooperationVerdict) -> &'static str {
113    match v {
114        CooperationVerdict::Authorized(_) => "Authorized",
115        CooperationVerdict::DeniedUnverified => "DeniedUnverified",
116        CooperationVerdict::DeniedUngrounded => "DeniedUngrounded",
117        CooperationVerdict::DeniedByPolicy(_) => "DeniedByPolicy",
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    use crate::agent::{A_ARTIFICIAL_AGENT, A_NATURAL_PERSON, P_OPERATED_BY, P_RDF_TYPE};
125    use crate::q_hash;
126    use crate::NQuin;
127
128    fn sp(agent: u64, verified: bool) -> CallerStandpoint {
129        CallerStandpoint {
130            agent,
131            role: q_hash("role:requester"),
132            verified,
133        }
134    }
135    fn t(s: u64, p: u64, o: u64) -> NQuin {
136        let mut q = NQuin {
137            subject: s,
138            predicate: p,
139            object: o,
140            context: 0,
141            metadata: 0,
142            parity: 0,
143        };
144        q.parity = q.subject ^ q.predicate ^ q.object ^ q.context;
145        q
146    }
147
148    #[test]
149    fn unverified_caller_is_denied() {
150        let v = authorize(
151            &sp(q_hash("did:x"), false),
152            true,
153            DeonticStatus::Active,
154            Governance::default(),
155        );
156        assert_eq!(v, CooperationVerdict::DeniedUnverified);
157    }
158
159    #[test]
160    fn ungrounded_caller_is_denied() {
161        let v = authorize(
162            &sp(q_hash("did:bot"), true),
163            false,
164            DeonticStatus::Active,
165            Governance::default(),
166        );
167        assert_eq!(v, CooperationVerdict::DeniedUngrounded);
168    }
169
170    #[test]
171    fn verified_grounded_ordinary_call_is_authorized() {
172        let v = authorize(
173            &sp(q_hash("did:alice"), true),
174            true,
175            DeonticStatus::Active,
176            Governance::default(),
177        );
178        assert_eq!(v, CooperationVerdict::Authorized(PolicyMode::Allow));
179    }
180
181    #[test]
182    fn non_derogable_violation_request_is_blocked_by_policy() {
183        let g = Governance {
184            non_derogable: true,
185            humanitarian: false,
186            ambiguous: false,
187        };
188        let v = authorize(
189            &sp(q_hash("did:alice"), true),
190            true,
191            DeonticStatus::Violated,
192            g,
193        );
194        assert_eq!(
195            v,
196            CooperationVerdict::DeniedByPolicy(PolicyMode::PreventiveBlock)
197        );
198    }
199
200    #[test]
201    fn authorize_call_resolves_grounding_from_the_graph() {
202        // An artificial agent with NO operatedBy Principal → ungrounded → denied.
203        let bot = q_hash("did:bot");
204        let idx = QuinIndex::from_slice(&[t(bot, P_RDF_TYPE, A_ARTIFICIAL_AGENT)]);
205        assert_eq!(
206            authorize_call(
207                &idx,
208                &sp(bot, true),
209                DeonticStatus::Active,
210                Governance::default()
211            ),
212            CooperationVerdict::DeniedUngrounded
213        );
214        // The same agent WITH a human Principal → grounded → authorized.
215        let human = q_hash("did:alice");
216        let idx2 = QuinIndex::from_slice(&[
217            t(bot, P_RDF_TYPE, A_ARTIFICIAL_AGENT),
218            t(bot, P_OPERATED_BY, human),
219        ]);
220        assert_eq!(
221            authorize_call(
222                &idx2,
223                &sp(bot, true),
224                DeonticStatus::Active,
225                Governance::default()
226            ),
227            CooperationVerdict::Authorized(PolicyMode::Allow)
228        );
229        // A natural person is never ungrounded.
230        let alice = q_hash("did:alice");
231        let idx3 = QuinIndex::from_slice(&[t(alice, P_RDF_TYPE, A_NATURAL_PERSON)]);
232        assert!(matches!(
233            authorize_call(
234                &idx3,
235                &sp(alice, true),
236                DeonticStatus::Active,
237                Governance::default()
238            ),
239            CooperationVerdict::Authorized(_)
240        ));
241    }
242}