newt_core/agentic/crew_attest.rs
1//! crew_attest.rs — the human-presence (`attest`) authority surface for the
2//! crew/team tools (#479, ROADMAP 23.2).
3//!
4//! Wraps agent-bridle's `step_up` (the merged `attest` primitive, agent-bridle#24)
5//! for the crew. A crew **dispatch** is an *amplify* — it grants a sub-agent the
6//! authority to act — which §7.5 (knowledge#40) says must ride a **live human
7//! gesture**: the `{allow, attest, deny}` surface, not a bare env toggle.
8//! `compose_roster` only *proposes* (no effects), so it needs nothing.
9//!
10//! This is the **structure** — the decision surface + the policy. The real Passkey
11//! enforcement (a WebAuthn `DischargeVerifier` + the ceremony) lands with **BOOT**
12//! (#472). Today the established presence is `Prompt` (the operator's `/team`
13//! enable — a soft human affirmation, exactly `Presence::Prompt`); a
14//! `Passkey`-required action surfaces `NeedsAttest`, awaiting BOOT's verifier.
15//!
16//! Pure (no I/O); the caller surfaces `NeedsAttest` to the overseer.
17
18use agent_bridle::{AttestRequirement, CallRequest, Rule, StepUpPolicy};
19
20// Re-export the presence ladder so callers (e.g. the CLI's LocalCrewRunner) can
21// name the established presence without a direct agent-bridle dependency.
22pub use agent_bridle::Presence;
23
24/// The default crew/team step-up policy. `crew`/`team` dispatch requires a human
25/// gesture (`Prompt` today; raise to `Passkey` for amplifying crews once BOOT
26/// lands); `compose_roster` only proposes, so it needs none. Unknown crew ops
27/// fail *toward* attestation (the default demands a gesture).
28#[must_use]
29pub fn crew_step_up_policy() -> StepUpPolicy {
30 StepUpPolicy::new(
31 vec![
32 Rule {
33 selector: "compose_roster".to_string(),
34 requirement: AttestRequirement::NONE,
35 },
36 Rule {
37 selector: "crew".to_string(),
38 requirement: AttestRequirement::presence(Presence::Prompt),
39 },
40 Rule {
41 selector: "team".to_string(),
42 requirement: AttestRequirement::presence(Presence::Prompt),
43 },
44 ],
45 AttestRequirement::presence(Presence::Prompt),
46 )
47}
48
49/// The crew authority decision under an established human presence.
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub enum CrewAuthz {
52 /// The established presence satisfies the policy — run it.
53 Allow,
54 /// Hold: needs a human gesture of at least this strength (the §7.5 `attest`).
55 /// The overseer surfaces it; post-BOOT it drives the discharge ceremony.
56 NeedsAttest(Presence),
57}
58
59/// Decide whether `op` (over `resource`) may run given the presence the session
60/// has already established. `Prompt` = the operator's `/team` enable; `Passkey` =
61/// a hardware gesture. Pure — the caller acts on `NeedsAttest`.
62#[must_use]
63pub fn crew_authz(
64 policy: &StepUpPolicy,
65 op: &str,
66 resource: &str,
67 established: Presence,
68) -> CrewAuthz {
69 let req = policy.required_for(&CallRequest::new(op, serde_json::Value::Null, resource));
70 if req.demands_gesture() && established < req.presence {
71 CrewAuthz::NeedsAttest(req.presence)
72 } else {
73 CrewAuthz::Allow
74 }
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80
81 #[test]
82 fn compose_roster_never_needs_a_gesture() {
83 let p = crew_step_up_policy();
84 // Even with NO established presence, proposing a roster is allowed (no effects).
85 assert_eq!(
86 crew_authz(&p, "compose_roster", "", Presence::None),
87 CrewAuthz::Allow
88 );
89 }
90
91 #[test]
92 fn crew_dispatch_needs_at_least_prompt() {
93 let p = crew_step_up_policy();
94 // No human presence -> hold for an attest.
95 assert_eq!(
96 crew_authz(&p, "crew", "add a flag", Presence::None),
97 CrewAuthz::NeedsAttest(Presence::Prompt)
98 );
99 // The operator's /team enable (Prompt) satisfies it.
100 assert_eq!(
101 crew_authz(&p, "crew", "add a flag", Presence::Prompt),
102 CrewAuthz::Allow
103 );
104 // A stronger gesture also satisfies it (presence is ordered).
105 assert_eq!(
106 crew_authz(&p, "crew", "add a flag", Presence::Passkey),
107 CrewAuthz::Allow
108 );
109 }
110
111 #[test]
112 fn team_and_unknown_ops_default_to_attest() {
113 let p = crew_step_up_policy();
114 assert_eq!(
115 crew_authz(&p, "team", "build X", Presence::None),
116 CrewAuthz::NeedsAttest(Presence::Prompt)
117 );
118 // An op no rule matches falls to the policy default (gate it).
119 assert_eq!(
120 crew_authz(&p, "mystery_op", "", Presence::None),
121 CrewAuthz::NeedsAttest(Presence::Prompt)
122 );
123 }
124
125 #[test]
126 fn a_passkey_required_action_is_not_satisfied_by_prompt() {
127 // Forward-looking: an amplifying crew that demands a hardware gesture is NOT
128 // satisfied by the soft /team enable — this is the seam BOOT's verifier fills.
129 let p = StepUpPolicy::new(
130 vec![Rule {
131 selector: "crew:secrets/*".to_string(),
132 requirement: AttestRequirement::presence(Presence::Passkey),
133 }],
134 AttestRequirement::NONE,
135 );
136 assert_eq!(
137 crew_authz(&p, "crew", "secrets/rotate", Presence::Prompt),
138 CrewAuthz::NeedsAttest(Presence::Passkey)
139 );
140 assert_eq!(
141 crew_authz(&p, "crew", "secrets/rotate", Presence::Passkey),
142 CrewAuthz::Allow
143 );
144 }
145}