Skip to main content

wyrd_runtime/
permission.rs

1//! Runtime RBAC permission model.
2
3use std::fmt;
4use std::str::FromStr;
5
6use serde::{Deserialize, Serialize};
7pub use wyrd_spec::auth::{
8    BifrostPermissionScope, BifrostSchemaScope, BifrostTableScope, GatewayAccess, PermissionScope,
9    PermissionScopeError,
10};
11use wyrd_spec::card::agent::BuiltinTool;
12use wyrd_spec::ids::CardUid;
13
14/// One operation/object authorization triple.
15///
16/// Wyrd RBAC is `(resource, action, scope)`: the first two axes name the
17/// operation and the third names the objects it reaches. Scope is required, so
18/// a grant that targets no particular object states [`PermissionScope::All`]
19/// explicitly rather than leaving the object axis unsaid.
20#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
21#[serde(try_from = "PermissionWire")]
22pub struct Permission {
23    /// Resource the permission applies to.
24    pub resource: Resource,
25    /// Action allowed on the resource.
26    pub action: Action,
27    /// Objects of that resource the permission reaches.
28    pub scope: PermissionScope,
29}
30
31/// Exact persisted and wire projection of one permission, before validation.
32///
33/// [`Permission`] deserializes through this shape so every decode — role JSON
34/// in `wyrd.auth_roles.permissions`, a request body, a signed forwarding
35/// envelope — runs the same validation. Scope has no default: a two-field
36/// object is rejected rather than silently promoted to
37/// [`PermissionScope::All`].
38#[derive(Deserialize)]
39#[serde(deny_unknown_fields)]
40struct PermissionWire {
41    /// Resource the permission applies to.
42    resource: Resource,
43    /// Action allowed on the resource.
44    action: Action,
45    /// Objects of that resource the permission reaches.
46    scope: PermissionScope,
47}
48
49impl TryFrom<PermissionWire> for Permission {
50    type Error = PermissionScopeError;
51
52    /// Validates one decoded permission before it becomes an effective grant.
53    ///
54    /// # Errors
55    ///
56    /// Returns the scope's own identity failure, or
57    /// [`PermissionScopeError::InvalidIdentifier`] with field `resource` when a
58    /// Bifrost object scope is attached to a resource that owns no Bifrost
59    /// object.
60    fn try_from(wire: PermissionWire) -> Result<Self, Self::Error> {
61        let permission = Self {
62            resource: wire.resource,
63            action: wire.action,
64            scope: wire.scope,
65        };
66        permission.validate()?;
67        Ok(permission)
68    }
69}
70
71/// Resource the permission applies to.
72#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
73#[serde(rename_all = "snake_case")]
74pub enum Resource {
75    /// Cards and specs.
76    Cards,
77    /// Service cards and deployment-facing service operations.
78    Services,
79    /// Operator cards and invocations.
80    Operators,
81    /// Explicit invocation of a Verifier Card, optionally scoped to one UID.
82    Verifier,
83    /// Drift-backed Verifier Cards and drift observations.
84    Drift,
85    /// Artifact bytes and metadata.
86    Artifacts,
87    /// Audit records and audit cards.
88    Audit,
89    /// Policy cards and policy administration.
90    Policy,
91    /// Trigger cards.
92    Triggers,
93    /// Service and agent API-key principals.
94    ServiceAccounts,
95    /// Human user administration.
96    Users,
97    /// The tenant's human OIDC login connection: staging, testing,
98    /// activating, deactivating, and removing the provider its people sign in
99    /// through.
100    ///
101    /// Separate from [`Resource::ServiceAccounts`] so that holding credential
102    /// administration alone never confers authority over who may sign in to
103    /// the tenant. The built-in `admin` role reaches it through its wildcard;
104    /// no other built-in role grants it.
105    IdentityConnections,
106    /// The tenant directory itself: creating, inspecting, suspending, and
107    /// recovering administration for a tenant.
108    ///
109    /// This is a platform-control-plane resource. It governs the lifecycle of a
110    /// tenant, never the resources inside one, so holding it grants no access
111    /// to any tenant's data.
112    Tenants,
113    /// The deployment's platform-scope human identity: its one OIDC connection
114    /// and the human platform principals registered against it.
115    ///
116    /// A platform-control-plane resource, distinct from [`Resource::Tenants`]
117    /// so that administering who may sign in to the platform is visible in a
118    /// grant and in audit separately from administering tenants. Holding it
119    /// grants no access to any tenant's data.
120    PlatformIdentity,
121    /// Platform-scope credentials: the secrets that authenticate a platform
122    /// principal to the control plane.
123    ///
124    /// Separate from [`Resource::PlatformIdentity`] because the two answer
125    /// different questions. Identity administration decides *who* the platform
126    /// recognizes; credential administration mints and retires the secrets that
127    /// prove it. An operator may need to rotate the deployment's root
128    /// credential without also being able to register new administrators.
129    PlatformCredentials,
130    /// Bifrost table definitions (DDL: create/list/describe).
131    BifrostTable,
132    /// Bifrost record ingest (the streaming write surface).
133    BifrostRecord,
134    /// Bifrost table reads (SQL/scan).
135    BifrostQuery,
136    /// Private Bifrost peer plane: reservation, execution, tail, and lifecycle.
137    ///
138    /// Role-neutral on purpose. Every peer-bearing target answers the same
139    /// private services, so a Scribe and an Oracle are authorized by the same
140    /// permission rather than by role-specific resources.
141    BifrostPeer,
142    /// Gateway administration (provider credentials, deployments, policies)
143    /// and deployment/model invocation.
144    Gateway,
145    /// Captured gateway request and response payload fields.
146    GatewayPayload,
147    /// Built-in Agent tool invocation, optionally scoped to one tool.
148    ///
149    /// Invocation authority only: a tool's underlying Cards or Bifrost access
150    /// is checked separately against those resources, and holding them never
151    /// implies this one.
152    Tool,
153    /// One of several resources.
154    AnyOf(Vec<Resource>),
155    /// All resources.
156    Wildcard,
157}
158
159/// Action performed against a resource.
160#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
161#[serde(rename_all = "snake_case")]
162pub enum Action {
163    /// Read.
164    Read,
165    /// Create or update.
166    Write,
167    /// Delete.
168    Delete,
169    /// Invoke runtime behavior.
170    Invoke,
171    /// Install or deploy.
172    Install,
173    /// Lock.
174    Lock,
175    /// Run.
176    Run,
177    /// Suspend a resource without destroying it, and resume it.
178    Suspend,
179    /// Restore administrative access a tenant can no longer reach itself.
180    ///
181    /// Named separately from [`Self::Write`] so the one operation that lets the
182    /// platform plane act on a tenant's administration is distinguishable in a
183    /// grant and in audit, rather than hiding inside a general write.
184    Recover,
185    /// One of several actions.
186    AnyOf(Vec<Action>),
187    /// All actions.
188    Wildcard,
189}
190
191impl Resource {
192    /// True if this resource covers `other`.
193    #[must_use]
194    pub fn covers(&self, other: &Resource) -> bool {
195        match (self, other) {
196            (Self::Wildcard, _) => true,
197            (Self::AnyOf(resources), other) => {
198                resources.iter().any(|resource| resource.covers(other))
199            }
200            (self_resource, other_resource) => self_resource == other_resource,
201        }
202    }
203
204    /// The single resources this one names: an `AnyOf` flattened, anything
205    /// else itself. Used to meet two grants member by member.
206    fn members(&self) -> Vec<&Self> {
207        match self {
208            Self::AnyOf(resources) => resources.iter().flat_map(Self::members).collect(),
209            resource => vec![resource],
210        }
211    }
212
213    /// True when this resource owns Bifrost objects a grant may be scoped to.
214    ///
215    /// Bifrost query reads and record writes name a table. `AnyOf` and
216    /// `Wildcard` are deliberately excluded: a multi-resource or wildcard grant
217    /// reaches objects only through [`PermissionScope::All`].
218    #[must_use]
219    pub const fn accepts_bifrost_scope(&self) -> bool {
220        matches!(self, Self::BifrostQuery | Self::BifrostRecord)
221    }
222
223    /// Stable label for this resource, or `None` for a composite `AnyOf`.
224    ///
225    /// Audit records the label rather than the enum's debug shape so a stored
226    /// decision stays readable and comparable across releases.
227    #[must_use]
228    pub fn as_str(&self) -> Option<&'static str> {
229        Some(match self {
230            Self::Cards => "cards",
231            Self::Services => "services",
232            Self::Operators => "operators",
233            Self::Verifier => "verifier",
234            Self::Drift => "drift",
235            Self::Artifacts => "artifacts",
236            Self::Audit => "audit",
237            Self::Policy => "policy",
238            Self::Triggers => "triggers",
239            Self::ServiceAccounts => "service_accounts",
240            Self::Users => "users",
241            Self::IdentityConnections => "identity_connections",
242            Self::Tenants => "tenants",
243            Self::PlatformIdentity => "platform_identity",
244            Self::PlatformCredentials => "platform_credentials",
245            Self::BifrostTable => "bifrost_table",
246            Self::BifrostRecord => "bifrost_record",
247            Self::BifrostQuery => "bifrost_query",
248            Self::BifrostPeer => "bifrost_peer",
249            Self::Gateway => "gateway",
250            Self::GatewayPayload => "gateway_payload",
251            Self::Tool => "tool",
252            Self::Wildcard => "wildcard",
253            Self::AnyOf(_) => return None,
254        })
255    }
256}
257
258impl Action {
259    /// True if this action covers `other`.
260    #[must_use]
261    pub fn covers(&self, other: &Action) -> bool {
262        match (self, other) {
263            (Self::Wildcard, _) => true,
264            (Self::AnyOf(actions), other) => actions.iter().any(|action| action.covers(other)),
265            (self_action, other_action) => self_action == other_action,
266        }
267    }
268
269    /// The single actions this one names: an `AnyOf` flattened, anything else
270    /// itself. Used to meet two grants member by member.
271    fn members(&self) -> Vec<&Self> {
272        match self {
273            Self::AnyOf(actions) => actions.iter().flat_map(Self::members).collect(),
274            action => vec![action],
275        }
276    }
277
278    /// Stable label for this action, or `None` for a composite `AnyOf`.
279    ///
280    /// Audit records the label rather than the enum's debug shape so a stored
281    /// decision stays readable and comparable across releases.
282    #[must_use]
283    pub fn as_str(&self) -> Option<&'static str> {
284        Some(match self {
285            Self::Read => "read",
286            Self::Write => "write",
287            Self::Delete => "delete",
288            Self::Invoke => "invoke",
289            Self::Install => "install",
290            Self::Lock => "lock",
291            Self::Run => "run",
292            Self::Suspend => "suspend",
293            Self::Recover => "recover",
294            Self::Wildcard => "wildcard",
295            Self::AnyOf(_) => return None,
296        })
297    }
298}
299
300impl Permission {
301    /// True if this permission covers `required`.
302    #[must_use]
303    pub fn covers(&self, required: &Permission) -> bool {
304        self.resource.covers(&required.resource)
305            && self.action.covers(&required.action)
306            && self.scope.covers(&required.scope)
307    }
308
309    /// True when holding `self` makes `other` redundant in a [`PermissionSet`].
310    ///
311    /// Subsumption is [`Self::covers`] except that an `all`-scoped grant never
312    /// subsumes an explicit gateway grant: `all` reaches only `open`
313    /// deployments, while the explicit grant is the only one reaching a
314    /// `granted` deployment. Requirement matching still uses `covers`.
315    #[must_use]
316    pub fn subsumes(&self, other: &Permission) -> bool {
317        self.covers(other) && !(self.scope == PermissionScope::All && other.scope.is_gateway())
318    }
319
320    /// Rejects a permission whose object scope cannot apply to its operation.
321    ///
322    /// Bifrost object scope is meaningful only for Bifrost query reads and
323    /// record writes, gateway object scope only for gateway invocation, and
324    /// Verifier object scope only for `verifier:run`, and tool object scope only
325    /// for `tool:invoke`. A wildcard or
326    /// multi-resource grant on either axis keeps object-wide reach only with
327    /// [`PermissionScope::All`], which is what stops a wildcard from inheriting
328    /// one object's narrow authority.
329    ///
330    /// # Errors
331    ///
332    /// Returns the scope's own identity failure, or
333    /// [`PermissionScopeError::InvalidIdentifier`] with field `resource` when an
334    /// object scope is attached to a resource that does not own that object, or
335    /// with field `action` when a Bifrost scope is attached to an action other
336    /// than [`Action::Read`] for query or [`Action::Write`] for record, a
337    /// gateway or tool scope to any action other than [`Action::Invoke`], or a
338    /// Verifier scope to any action other than [`Action::Run`].
339    pub fn validate(&self) -> Result<(), PermissionScopeError> {
340        self.scope.validate()?;
341        let (resource_accepts, required_action) = match &self.scope {
342            PermissionScope::All => return Ok(()),
343            PermissionScope::Bifrost(_) => (
344                self.resource.accepts_bifrost_scope(),
345                if self.resource == Resource::BifrostRecord {
346                    Action::Write
347                } else {
348                    Action::Read
349                },
350            ),
351            PermissionScope::Gateway(_) => (self.resource == Resource::Gateway, Action::Invoke),
352            PermissionScope::Verifier(_) => (self.resource == Resource::Verifier, Action::Run),
353            PermissionScope::Tool(_) => (self.resource == Resource::Tool, Action::Invoke),
354        };
355        if !resource_accepts {
356            return Err(PermissionScopeError::InvalidIdentifier {
357                field: "resource",
358                value: self.resource.as_str().unwrap_or("any_of").to_owned(),
359            });
360        }
361        if self.action != required_action {
362            return Err(PermissionScopeError::InvalidIdentifier {
363                field: "action",
364                value: self.action.as_str().unwrap_or("any_of").to_owned(),
365            });
366        }
367        Ok(())
368    }
369
370    /// Read cards.
371    #[must_use]
372    pub const fn card_read() -> Self {
373        Self {
374            resource: Resource::Cards,
375            action: Action::Read,
376            scope: PermissionScope::All,
377        }
378    }
379
380    /// Write cards.
381    #[must_use]
382    pub const fn card_write() -> Self {
383        Self {
384            resource: Resource::Cards,
385            action: Action::Write,
386            scope: PermissionScope::All,
387        }
388    }
389
390    /// Delete cards.
391    #[must_use]
392    pub const fn card_delete() -> Self {
393        Self {
394            resource: Resource::Cards,
395            action: Action::Delete,
396            scope: PermissionScope::All,
397        }
398    }
399
400    /// Read artifacts.
401    #[must_use]
402    pub const fn artifact_read() -> Self {
403        Self {
404            resource: Resource::Artifacts,
405            action: Action::Read,
406            scope: PermissionScope::All,
407        }
408    }
409
410    /// Write artifacts.
411    #[must_use]
412    pub const fn artifact_write() -> Self {
413        Self {
414            resource: Resource::Artifacts,
415            action: Action::Write,
416            scope: PermissionScope::All,
417        }
418    }
419
420    /// Install services.
421    #[must_use]
422    pub const fn service_install() -> Self {
423        Self {
424            resource: Resource::Services,
425            action: Action::Install,
426            scope: PermissionScope::All,
427        }
428    }
429
430    /// Write service accounts.
431    #[must_use]
432    pub const fn service_accounts_write() -> Self {
433        Self {
434            resource: Resource::ServiceAccounts,
435            action: Action::Write,
436            scope: PermissionScope::All,
437        }
438    }
439
440    /// Administer the tenant's human OIDC login connection.
441    #[must_use]
442    pub const fn identity_connections_write() -> Self {
443        Self {
444            resource: Resource::IdentityConnections,
445            action: Action::Write,
446            scope: PermissionScope::All,
447        }
448    }
449
450    /// Read the tenant's redacted Operator connections.
451    #[must_use]
452    pub const fn operators_read() -> Self {
453        Self {
454            resource: Resource::Operators,
455            action: Action::Read,
456            scope: PermissionScope::All,
457        }
458    }
459
460    /// Create, update, rotate, and disable the tenant's Operator connections.
461    #[must_use]
462    pub const fn operators_write() -> Self {
463        Self {
464            resource: Resource::Operators,
465            action: Action::Write,
466            scope: PermissionScope::All,
467        }
468    }
469
470    /// Invoke operators.
471    #[must_use]
472    pub const fn operator_invoke() -> Self {
473        Self {
474            resource: Resource::Operators,
475            action: Action::Invoke,
476            scope: PermissionScope::All,
477        }
478    }
479
480    /// Invoke the one Verifier Card `verifier`, directly or as a manual run.
481    ///
482    /// The permission an explicit invocation requires: only a grant of this
483    /// exact UID or [`Self::verifier_run_any`] covers it.
484    #[must_use]
485    pub const fn verifier_run(verifier: CardUid) -> Self {
486        Self {
487            resource: Resource::Verifier,
488            action: Action::Run,
489            scope: PermissionScope::Verifier(verifier),
490        }
491    }
492
493    /// Invoke every Verifier Card in the tenant.
494    ///
495    /// The unscoped grant the built-in `workload` and `editor` Roles carry; a
496    /// narrower custom Role grants [`Self::verifier_run`] for one UID.
497    #[must_use]
498    pub const fn verifier_run_any() -> Self {
499        Self {
500            resource: Resource::Verifier,
501            action: Action::Run,
502            scope: PermissionScope::All,
503        }
504    }
505
506    /// Write triggers.
507    #[must_use]
508    pub const fn trigger_write() -> Self {
509        Self {
510            resource: Resource::Triggers,
511            action: Action::Write,
512            scope: PermissionScope::All,
513        }
514    }
515
516    /// Read audit.
517    #[must_use]
518    pub const fn audit_read() -> Self {
519        Self {
520            resource: Resource::Audit,
521            action: Action::Read,
522            scope: PermissionScope::All,
523        }
524    }
525
526    /// Lock policy.
527    #[must_use]
528    pub const fn policy_lock() -> Self {
529        Self {
530            resource: Resource::Policy,
531            action: Action::Lock,
532            scope: PermissionScope::All,
533        }
534    }
535
536    /// Create a tenant on the platform control plane.
537    #[must_use]
538    pub const fn tenant_create() -> Self {
539        Self {
540            resource: Resource::Tenants,
541            action: Action::Write,
542            scope: PermissionScope::All,
543        }
544    }
545
546    /// List tenants and inspect one tenant's lifecycle state.
547    #[must_use]
548    pub const fn tenant_read() -> Self {
549        Self {
550            resource: Resource::Tenants,
551            action: Action::Read,
552            scope: PermissionScope::All,
553        }
554    }
555
556    /// Suspend and resume a tenant without destroying its state.
557    #[must_use]
558    pub const fn tenant_suspend() -> Self {
559        Self {
560            resource: Resource::Tenants,
561            action: Action::Suspend,
562            scope: PermissionScope::All,
563        }
564    }
565
566    /// Restore administrative access to a tenant that has lost its own.
567    ///
568    /// The single named capability by which the platform plane reaches a
569    /// tenant, kept distinct so it is visible in a grant and in audit rather
570    /// than hidden inside a general write.
571    #[must_use]
572    pub const fn tenant_recover_admin() -> Self {
573        Self {
574            resource: Resource::Tenants,
575            action: Action::Recover,
576            scope: PermissionScope::All,
577        }
578    }
579
580    /// Read the platform-scope identity configuration.
581    ///
582    /// Reading never yields provider secrets; the connection view is redacted
583    /// at the boundary regardless of who holds this.
584    #[must_use]
585    pub const fn platform_identity_read() -> Self {
586        Self {
587            resource: Resource::PlatformIdentity,
588            action: Action::Read,
589            scope: PermissionScope::All,
590        }
591    }
592
593    /// Configure the platform OIDC connection and register human platform
594    /// principals.
595    ///
596    /// Deliberately the same permission for both: registering a principal
597    /// against a connection you also control is one capability, and splitting
598    /// it would suggest a separation the plane does not actually have.
599    #[must_use]
600    pub const fn platform_identity_write() -> Self {
601        Self {
602            resource: Resource::PlatformIdentity,
603            action: Action::Write,
604            scope: PermissionScope::All,
605        }
606    }
607
608    /// List the non-secret metadata of a platform principal's credentials.
609    ///
610    /// Listing never yields credential material; it exists so an operator can
611    /// see what is live, expired, or already retired before rotating.
612    #[must_use]
613    pub const fn platform_credential_read() -> Self {
614        Self {
615            resource: Resource::PlatformCredentials,
616            action: Action::Read,
617            scope: PermissionScope::All,
618        }
619    }
620
621    /// Issue and revoke platform credentials.
622    ///
623    /// One permission for both halves of rotation: an operator who can mint a
624    /// replacement must be able to retire what it replaces, and splitting them
625    /// would leave a superseded credential live because the second half was
626    /// separately granted.
627    #[must_use]
628    pub const fn platform_credential_write() -> Self {
629        Self {
630            resource: Resource::PlatformCredentials,
631            action: Action::Write,
632            scope: PermissionScope::All,
633        }
634    }
635
636    /// Manage users.
637    #[must_use]
638    pub const fn users_manage() -> Self {
639        Self {
640            resource: Resource::Users,
641            action: Action::Write,
642            scope: PermissionScope::All,
643        }
644    }
645
646    /// Write Bifrost records (the ingest surface).
647    #[must_use]
648    pub const fn bifrost_record_write() -> Self {
649        Self {
650            resource: Resource::BifrostRecord,
651            action: Action::Write,
652            scope: PermissionScope::All,
653        }
654    }
655
656    /// Invoke the private Bifrost peer protocol.
657    #[must_use]
658    pub const fn bifrost_peer_invoke() -> Self {
659        Self {
660            resource: Resource::BifrostPeer,
661            action: Action::Invoke,
662            scope: PermissionScope::All,
663        }
664    }
665
666    /// Read Bifrost table definitions.
667    #[must_use]
668    pub const fn bifrost_table_read() -> Self {
669        Self {
670            resource: Resource::BifrostTable,
671            action: Action::Read,
672            scope: PermissionScope::All,
673        }
674    }
675
676    /// Write Bifrost table definitions (DDL).
677    #[must_use]
678    pub const fn bifrost_table_write() -> Self {
679        Self {
680            resource: Resource::BifrostTable,
681            action: Action::Write,
682            scope: PermissionScope::All,
683        }
684    }
685
686    /// Install a `SystemShared` Bifrost table (cross-tenant DDL).
687    #[must_use]
688    pub const fn bifrost_table_install() -> Self {
689        Self {
690            resource: Resource::BifrostTable,
691            action: Action::Install,
692            scope: PermissionScope::All,
693        }
694    }
695
696    /// Read Bifrost tables (SQL/scan).
697    #[must_use]
698    pub const fn bifrost_query_read() -> Self {
699        Self {
700            resource: Resource::BifrostQuery,
701            action: Action::Read,
702            scope: PermissionScope::All,
703        }
704    }
705
706    /// Read gateway deployments, redacted credential metadata, and policies.
707    #[must_use]
708    pub const fn gateway_read() -> Self {
709        Self {
710            resource: Resource::Gateway,
711            action: Action::Read,
712            scope: PermissionScope::All,
713        }
714    }
715
716    /// Create, replace, rotate, or revoke gateway configuration and policies.
717    #[must_use]
718    pub const fn gateway_write() -> Self {
719        Self {
720            resource: Resource::Gateway,
721            action: Action::Write,
722            scope: PermissionScope::All,
723        }
724    }
725
726    /// Delete gateway credentials, deployments, and policies.
727    #[must_use]
728    pub const fn gateway_delete() -> Self {
729        Self {
730            resource: Resource::Gateway,
731            action: Action::Delete,
732            scope: PermissionScope::All,
733        }
734    }
735
736    /// Invoke the gateway for the deployment or model named by `access`.
737    #[must_use]
738    pub const fn gateway_invoke(access: GatewayAccess) -> Self {
739        Self {
740            resource: Resource::Gateway,
741            action: Action::Invoke,
742            scope: PermissionScope::Gateway(access),
743        }
744    }
745
746    /// Invoke the gateway for every `open` deployment and its models.
747    ///
748    /// The unscoped grant the built-in `workload` Role carries; a narrower
749    /// custom Role uses [`Self::gateway_invoke`] with one deployment or model,
750    /// which is also the only grant reaching a `granted` deployment.
751    #[must_use]
752    pub const fn gateway_invoke_any() -> Self {
753        Self {
754            resource: Resource::Gateway,
755            action: Action::Invoke,
756            scope: PermissionScope::All,
757        }
758    }
759
760    /// Read captured gateway payload fields.
761    #[must_use]
762    pub const fn gateway_payload_read() -> Self {
763        Self {
764            resource: Resource::GatewayPayload,
765            action: Action::Read,
766            scope: PermissionScope::All,
767        }
768    }
769
770    /// Invoke the one built-in Agent tool `tool`.
771    ///
772    /// The permission each tool call requires before it runs: only a grant of
773    /// this exact tool, [`Self::tool_invoke_any`], or a covering wildcard
774    /// covers it. It grants none of the tool's underlying data access.
775    #[must_use]
776    pub const fn tool_invoke(tool: BuiltinTool) -> Self {
777        Self {
778            resource: Resource::Tool,
779            action: Action::Invoke,
780            scope: PermissionScope::Tool(tool),
781        }
782    }
783
784    /// Invoke every built-in Agent tool.
785    #[must_use]
786    pub const fn tool_invoke_any() -> Self {
787        Self {
788            resource: Resource::Tool,
789            action: Action::Invoke,
790            scope: PermissionScope::All,
791        }
792    }
793
794    /// All permissions.
795    #[must_use]
796    pub const fn wildcard() -> Self {
797        Self {
798            resource: Resource::Wildcard,
799            action: Action::Wildcard,
800            scope: PermissionScope::All,
801        }
802    }
803}
804
805/// Renders the permission's *operation* as `resource:action`.
806///
807/// The object axis is deliberately absent: this string names which operation
808/// was exercised for audit text and diagnostics, and is never the authorization
809/// decision. Scoped authority travels as the typed [`Permission`] and, for
810/// distributed execution, inside the scoped permission digest.
811impl fmt::Display for Permission {
812    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
813        let resource = self.resource.as_str().ok_or(fmt::Error)?;
814        let action = self.action.as_str().ok_or(fmt::Error)?;
815        write!(f, "{resource}:{action}")
816    }
817}
818
819/// Permission parse failure.
820#[derive(Debug, thiserror::Error)]
821#[error("permission must be formatted as resource:action")]
822pub struct PermissionParseError;
823
824/// Parses one `resource:action` operation token into an [`PermissionScope::All`]
825/// permission.
826///
827/// The token language carries no object, so a parsed permission is the
828/// object-wide form. Scoped grants are expressed only through the typed JSON
829/// projection; there is no second string spelling for an object.
830impl FromStr for Permission {
831    type Err = PermissionParseError;
832
833    fn from_str(value: &str) -> Result<Self, Self::Err> {
834        let Some((resource, action)) = value.split_once(':') else {
835            return Err(PermissionParseError);
836        };
837        Ok(Self {
838            resource: parse_resource(resource)?,
839            action: parse_action(action)?,
840            scope: PermissionScope::All,
841        })
842    }
843}
844
845fn parse_resource(value: &str) -> Result<Resource, PermissionParseError> {
846    Ok(match value {
847        "cards" => Resource::Cards,
848        "services" => Resource::Services,
849        "operators" => Resource::Operators,
850        "verifier" => Resource::Verifier,
851        "drift" => Resource::Drift,
852        "artifacts" => Resource::Artifacts,
853        "audit" => Resource::Audit,
854        "policy" => Resource::Policy,
855        "triggers" => Resource::Triggers,
856        "service_accounts" => Resource::ServiceAccounts,
857        "users" => Resource::Users,
858        "identity_connections" => Resource::IdentityConnections,
859        "bifrost_table" => Resource::BifrostTable,
860        "bifrost_record" => Resource::BifrostRecord,
861        "bifrost_peer" => Resource::BifrostPeer,
862        "bifrost_query" => Resource::BifrostQuery,
863        "gateway" => Resource::Gateway,
864        "gateway_payload" => Resource::GatewayPayload,
865        "tool" => Resource::Tool,
866        "wildcard" => Resource::Wildcard,
867        _ => return Err(PermissionParseError),
868    })
869}
870
871fn parse_action(value: &str) -> Result<Action, PermissionParseError> {
872    Ok(match value {
873        "read" => Action::Read,
874        "write" => Action::Write,
875        "delete" => Action::Delete,
876        "invoke" => Action::Invoke,
877        "install" => Action::Install,
878        "lock" => Action::Lock,
879        "run" => Action::Run,
880        "wildcard" => Action::Wildcard,
881        _ => return Err(PermissionParseError),
882    })
883}
884
885/// Two permission sets whose exact shared authority no permission can state.
886///
887/// Delegation refuses on this rather than signing a token that widens either
888/// party or drops authority both hold.
889#[derive(Debug, thiserror::Error)]
890#[error("shared authority for {permission} depends on deployment access and cannot be represented")]
891pub struct UnrepresentableIntersection {
892    /// The gateway grant one side holds only through `all`.
893    pub permission: Permission,
894}
895
896/// Subsumption-aware permission collection.
897#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
898#[serde(transparent)]
899pub struct PermissionSet(Vec<Permission>);
900
901impl PermissionSet {
902    /// Construct an empty set.
903    #[must_use]
904    pub fn new() -> Self {
905        Self(Vec::new())
906    }
907
908    /// Insert one permission while keeping the set minimal.
909    ///
910    /// A grant is dropped only when another grant [`Permission::subsumes`] it,
911    /// so an explicit gateway grant survives beside `all` in either order.
912    pub fn insert(&mut self, permission: Permission) {
913        if self.0.iter().any(|existing| existing.subsumes(&permission)) {
914            return;
915        }
916        self.0.retain(|existing| !permission.subsumes(existing));
917        self.0.push(permission);
918    }
919
920    /// True if the set covers `required`.
921    #[must_use]
922    pub fn contains(&self, required: &Permission) -> bool {
923        self.0.iter().any(|permission| permission.covers(required))
924    }
925
926    /// True if a grant naming specific objects, not [`PermissionScope::All`],
927    /// covers `required`.
928    ///
929    /// A `granted` gateway deployment admits only such a grant, so the
930    /// unscoped invoke grant of the built-in Roles never reaches it.
931    #[must_use]
932    pub fn contains_object_scoped(&self, required: &Permission) -> bool {
933        self.0.iter().any(|permission| {
934            permission.scope != PermissionScope::All && permission.covers(required)
935        })
936    }
937
938    /// True when the set grants `action` on `resource` under *any* object scope.
939    ///
940    /// This is coarse admission, not authorization: it answers "does this
941    /// principal hold this capability at all", which is what a public route
942    /// needs before it can resolve the objects a request actually touches. The
943    /// authoritative decision is [`Self::contains`] against the resolved
944    /// object, and no caller may substitute this for it.
945    #[must_use]
946    pub fn covers_operation(&self, resource: &Resource, action: &Action) -> bool {
947        self.0.iter().any(|permission| {
948            permission.resource.covers(resource) && permission.action.covers(action)
949        })
950    }
951
952    /// The authority both sets grant: every permission covered by `self` and by
953    /// `other`, and nothing else.
954    ///
955    /// Delegation mints with this so a delegated token can never carry more
956    /// than its caller holds. Each pair of grants meets axis by axis through the
957    /// same `covers` relations authorization uses: an `AnyOf` is split into its
958    /// members, each axis keeps the narrower side when one covers the other, and
959    /// a pair with a disjoint axis contributes nothing. A wildcard therefore
960    /// yields the other grant, and a schema grant meeting a table grant in that
961    /// schema yields the table grant.
962    ///
963    /// The one meet no scope can state is `all` against an explicit gateway
964    /// grant: `all` reaches only `open` deployments, so their shared authority
965    /// is "that deployment, only while it is open". Such a meet is accepted only
966    /// when the rest of the result already covers it — both sides also hold
967    /// `all`, or both grant the deployment explicitly; otherwise the complete
968    /// intersection is unrepresentable.
969    ///
970    /// # Errors
971    ///
972    /// Returns [`UnrepresentableIntersection`] naming the first gateway grant
973    /// whose shared authority depends on deployment access, so delegation can
974    /// refuse instead of widening or silently dropping it.
975    pub fn intersection(&self, other: &Self) -> Result<Self, UnrepresentableIntersection> {
976        let mut meet = Self::new();
977        let mut open_only = Vec::new();
978        for left in &self.0 {
979            for right in &other.0 {
980                let Some(scope) = narrower(&left.scope, &right.scope, PermissionScope::covers)
981                else {
982                    continue;
983                };
984                let conditional = scope.is_gateway()
985                    && (left.scope == PermissionScope::All || right.scope == PermissionScope::All);
986                for resource in meets(
987                    &left.resource.members(),
988                    &right.resource.members(),
989                    Resource::covers,
990                ) {
991                    for action in meets(
992                        &left.action.members(),
993                        &right.action.members(),
994                        Action::covers,
995                    ) {
996                        let permission = Permission {
997                            resource: resource.clone(),
998                            action: action.clone(),
999                            scope: scope.clone(),
1000                        };
1001                        if permission.validate().is_err() {
1002                            continue;
1003                        }
1004                        if conditional {
1005                            open_only.push(permission);
1006                        } else {
1007                            meet.insert(permission);
1008                        }
1009                    }
1010                }
1011            }
1012        }
1013        match open_only
1014            .into_iter()
1015            .find(|permission| !meet.contains(permission))
1016        {
1017            Some(permission) => Err(UnrepresentableIntersection { permission }),
1018            None => Ok(meet),
1019        }
1020    }
1021
1022    /// Iterate over stored permissions.
1023    pub fn iter(&self) -> impl Iterator<Item = &Permission> {
1024        self.0.iter()
1025    }
1026
1027    /// Number of stored permissions.
1028    #[must_use]
1029    pub fn len(&self) -> usize {
1030        self.0.len()
1031    }
1032
1033    /// True when empty.
1034    #[must_use]
1035    pub fn is_empty(&self) -> bool {
1036        self.0.is_empty()
1037    }
1038}
1039
1040/// The narrower of two values on one axis, or `None` when neither covers the
1041/// other and the axis is disjoint.
1042///
1043/// Exact for the resource, action, and scope axes because each is a tree under
1044/// its `covers` relation once `AnyOf` is split into members.
1045fn narrower<'a, T>(left: &'a T, right: &'a T, covers: fn(&T, &T) -> bool) -> Option<&'a T> {
1046    if covers(left, right) {
1047        Some(right)
1048    } else if covers(right, left) {
1049        Some(left)
1050    } else {
1051        None
1052    }
1053}
1054
1055/// Every non-empty [`narrower`] meet between two lists of axis members.
1056fn meets<'a, T>(left: &[&'a T], right: &[&'a T], covers: fn(&T, &T) -> bool) -> Vec<&'a T> {
1057    left.iter()
1058        .flat_map(|l| right.iter().filter_map(move |r| narrower(*l, *r, covers)))
1059        .collect()
1060}
1061
1062impl FromIterator<Permission> for PermissionSet {
1063    fn from_iter<T: IntoIterator<Item = Permission>>(iter: T) -> Self {
1064        let mut set = Self::new();
1065        for permission in iter {
1066            set.insert(permission);
1067        }
1068        set
1069    }
1070}
1071
1072#[cfg(test)]
1073mod tests {
1074    use super::{Action, Permission, PermissionScope, PermissionSet, Resource};
1075    use serde_json::json;
1076    use wyrd_spec::auth::{BifrostPermissionScope, BifrostSchemaScope, BifrostTableScope};
1077
1078    #[test]
1079    fn serde_round_trip_every_variant() {
1080        let resources = [
1081            Resource::Cards,
1082            Resource::Services,
1083            Resource::Operators,
1084            Resource::Verifier,
1085            Resource::Drift,
1086            Resource::Artifacts,
1087            Resource::Audit,
1088            Resource::Policy,
1089            Resource::Triggers,
1090            Resource::ServiceAccounts,
1091            Resource::Users,
1092            Resource::IdentityConnections,
1093            Resource::AnyOf(vec![Resource::Operators, Resource::Triggers]),
1094            Resource::Wildcard,
1095        ];
1096        for resource in resources {
1097            let value = serde_json::to_value(&resource).expect("resource serializes");
1098            let round_trip: Resource =
1099                serde_json::from_value(value).expect("resource deserializes");
1100            assert_eq!(round_trip, resource);
1101        }
1102
1103        let actions = [
1104            Action::Read,
1105            Action::Write,
1106            Action::Delete,
1107            Action::Invoke,
1108            Action::Install,
1109            Action::Lock,
1110            Action::Run,
1111            Action::AnyOf(vec![Action::Read, Action::Write]),
1112            Action::Wildcard,
1113        ];
1114        for action in actions {
1115            let value = serde_json::to_value(&action).expect("action serializes");
1116            let round_trip: Action = serde_json::from_value(value).expect("action deserializes");
1117            assert_eq!(round_trip, action);
1118        }
1119    }
1120
1121    #[test]
1122    fn bifrost_permissions_round_trip_through_wire_strings() {
1123        for (permission, wire) in [
1124            (Permission::bifrost_record_write(), "bifrost_record:write"),
1125            (Permission::bifrost_table_read(), "bifrost_table:read"),
1126            (Permission::bifrost_table_write(), "bifrost_table:write"),
1127            (Permission::bifrost_query_read(), "bifrost_query:read"),
1128            (Permission::bifrost_peer_invoke(), "bifrost_peer:invoke"),
1129        ] {
1130            assert_eq!(permission.to_string(), wire);
1131            assert_eq!(wire.parse::<Permission>().expect("wire parses"), permission);
1132            let json = serde_json::to_value(&permission).expect("serializes");
1133            assert_eq!(
1134                serde_json::from_value::<Permission>(json).expect("deserializes"),
1135                permission
1136            );
1137        }
1138    }
1139
1140    #[test]
1141    fn bifrost_record_write_is_distinct_from_table_write() {
1142        assert!(!Permission::bifrost_table_write().covers(&Permission::bifrost_record_write()));
1143        assert!(!Permission::bifrost_record_write().covers(&Permission::bifrost_table_write()));
1144        assert!(Permission::wildcard().covers(&Permission::bifrost_record_write()));
1145    }
1146
1147    #[test]
1148    fn covers_wildcard_resource() {
1149        let permission = Permission {
1150            resource: Resource::Wildcard,
1151            action: Action::Read,
1152            scope: PermissionScope::All,
1153        };
1154
1155        assert!(permission.covers(&Permission::card_read()));
1156        assert!(!permission.covers(&Permission::card_write()));
1157    }
1158
1159    #[test]
1160    fn covers_wildcard_action() {
1161        let permission = Permission {
1162            resource: Resource::Cards,
1163            action: Action::Wildcard,
1164            scope: PermissionScope::All,
1165        };
1166
1167        assert!(permission.covers(&Permission::card_read()));
1168        assert!(permission.covers(&Permission::card_write()));
1169        assert!(!permission.covers(&Permission::artifact_write()));
1170    }
1171
1172    /// The resource-and-action wildcard covers every concrete grant, including
1173    /// Bifrost record writes outside the Card family.
1174    ///
1175    /// # Panics
1176    /// Panics when the wildcard fails to cover any probed permission.
1177    #[test]
1178    fn covers_double_wildcard() {
1179        let permission = Permission::wildcard();
1180
1181        assert!(permission.covers(&Permission::card_read()));
1182        assert!(permission.covers(&Permission::card_write()));
1183        assert!(permission.covers(&Permission::bifrost_record_write()));
1184    }
1185
1186    #[test]
1187    fn covers_anyof_resource() {
1188        let permission = Permission {
1189            resource: Resource::AnyOf(vec![Resource::Operators, Resource::Triggers]),
1190            action: Action::Invoke,
1191            scope: PermissionScope::All,
1192        };
1193
1194        assert!(permission.covers(&Permission::operator_invoke()));
1195        assert!(permission.covers(&Permission {
1196            resource: Resource::Triggers,
1197            action: Action::Invoke,
1198            scope: PermissionScope::All,
1199        }));
1200        assert!(!permission.covers(&Permission {
1201            resource: Resource::Cards,
1202            action: Action::Invoke,
1203            scope: PermissionScope::All,
1204        }));
1205    }
1206
1207    #[test]
1208    fn permission_set_subsumption_drops_redundant() {
1209        let mut set = PermissionSet::from_iter([Permission::card_read()]);
1210        set.insert(Permission::wildcard());
1211
1212        assert_eq!(set.len(), 1);
1213        assert_eq!(set.iter().next(), Some(&Permission::wildcard()));
1214    }
1215
1216    #[test]
1217    fn permission_set_subsumption_skips_covered() {
1218        let mut set = PermissionSet::from_iter([Permission::wildcard()]);
1219        set.insert(Permission::card_read());
1220
1221        assert_eq!(set.len(), 1);
1222        assert_eq!(set.iter().next(), Some(&Permission::wildcard()));
1223    }
1224
1225    /// Set membership resolves through coverage, so a set holding only the
1226    /// wildcard contains every concrete permission.
1227    ///
1228    /// # Panics
1229    /// Panics when the wildcard-only set does not contain a probed permission.
1230    #[test]
1231    fn permission_set_contains_resolves_through_wildcard() {
1232        let set = PermissionSet::from_iter([Permission::wildcard()]);
1233
1234        assert!(set.contains(&Permission::card_read()));
1235        assert!(set.contains(&Permission::bifrost_record_write()));
1236    }
1237
1238    #[test]
1239    fn permission_set_jsonb_round_trip() {
1240        let permissions = vec![
1241            Permission::card_write(),
1242            Permission::card_read(),
1243            Permission {
1244                resource: Resource::AnyOf(vec![Resource::Operators, Resource::Triggers]),
1245                action: Action::Invoke,
1246                scope: PermissionScope::All,
1247            },
1248            Permission::wildcard(),
1249        ];
1250        let value = serde_json::to_value(&permissions).expect("permissions serialize");
1251
1252        assert_eq!(
1253            value,
1254            json!([
1255                {"resource": "cards", "action": "write", "scope": "all"},
1256                {"resource": "cards", "action": "read", "scope": "all"},
1257                {"resource": {"any_of": ["operators", "triggers"]}, "action": "invoke", "scope": "all"},
1258                {"resource": "wildcard", "action": "wildcard", "scope": "all"}
1259            ])
1260        );
1261
1262        let round_trip: Vec<Permission> =
1263            serde_json::from_value(value).expect("permissions deserialize");
1264        assert_eq!(round_trip, permissions);
1265    }
1266
1267    /// Proves gateway grants round-trip their approved wire names, a
1268    /// deployment invoke grant covers that deployment's models only, only an
1269    /// object-scoped grant counts as scoped, and a gateway object scope is
1270    /// refused anywhere but gateway invoke.
1271    ///
1272    /// # Panics
1273    /// Panics when a fixture fails to build or an assertion fails.
1274    #[test]
1275    fn gateway_permissions_are_typed_and_scope_checked() {
1276        use super::GatewayAccess;
1277        use wyrd_spec::ids::{ModelId, ProviderDeploymentName};
1278        let openai = ProviderDeploymentName::new("openai").expect("valid deployment");
1279        let provider = GatewayAccess::Deployment {
1280            deployment: openai.clone(),
1281        };
1282        let model = |name: &str| GatewayAccess::Model {
1283            deployment: openai.clone(),
1284            model: ModelId::new(name).expect("valid model"),
1285        };
1286        let required = Permission::gateway_invoke(model("gpt-4o"));
1287        assert!(PermissionSet::from_iter([Permission::gateway_invoke_any()]).contains(&required));
1288        assert!(
1289            !PermissionSet::from_iter([Permission::gateway_invoke_any()])
1290                .contains_object_scoped(&required),
1291            "the unscoped grant is not a grant of the deployment"
1292        );
1293        assert!(
1294            PermissionSet::from_iter([Permission::gateway_invoke(provider.clone())])
1295                .contains_object_scoped(&required)
1296        );
1297
1298        assert_eq!(
1299            Permission::gateway_payload_read().to_string(),
1300            "gateway_payload:read"
1301        );
1302        assert_eq!(
1303            "gateway:write".parse::<Permission>().expect("parses"),
1304            Permission::gateway_write()
1305        );
1306        assert!(
1307            Permission::gateway_invoke(provider.clone())
1308                .covers(&Permission::gateway_invoke(model("gpt-4o")))
1309        );
1310        assert!(
1311            !Permission::gateway_invoke(model("gpt-4o"))
1312                .covers(&Permission::gateway_invoke(model("gpt-4.1")))
1313        );
1314        assert!(
1315            !Permission::gateway_invoke(provider.clone()).covers(&Permission::gateway_read()),
1316            "invoke never grants admin metadata reads"
1317        );
1318        assert_eq!(
1319            serde_json::to_value(Permission::gateway_invoke(model("gpt-4o"))).expect("serializes"),
1320            json!({
1321                "resource": "gateway",
1322                "action": "invoke",
1323                "scope": {"gateway": {"model": {"deployment": "openai", "model": "gpt-4o"}}}
1324            })
1325        );
1326        for (resource, action) in [("gateway", "read"), ("cards", "invoke")] {
1327            assert!(
1328                serde_json::from_value::<Permission>(json!({
1329                    "resource": resource,
1330                    "action": action,
1331                    "scope": {"gateway": {"deployment": {"deployment": "openai"}}}
1332                }))
1333                .is_err(),
1334                "{resource}:{action}"
1335            );
1336        }
1337    }
1338
1339    /// Proves `verifier:run` parses, covers only its exact Verifier UID or the
1340    /// unscoped grant, and refuses a Verifier scope on any other operation.
1341    #[test]
1342    fn verifier_run_is_uid_scoped() {
1343        use wyrd_spec::ids::CardUid;
1344        let uid = |value: &str| CardUid::new(value).expect("valid uuid7");
1345        let bound = uid("01927b4c-0000-7000-8000-000000000001");
1346        let other = uid("01927b4c-0000-7000-8000-000000000002");
1347
1348        assert_eq!(
1349            "verifier:run".parse::<Permission>().expect("parses"),
1350            Permission::verifier_run_any()
1351        );
1352        assert!(Permission::verifier_run_any().covers(&Permission::verifier_run(bound.clone())));
1353        assert!(
1354            Permission::verifier_run(bound.clone())
1355                .covers(&Permission::verifier_run(bound.clone()))
1356        );
1357        assert!(
1358            !Permission::verifier_run(other).covers(&Permission::verifier_run(bound.clone())),
1359            "an unrelated UID grant does not cover"
1360        );
1361        assert_eq!(
1362            serde_json::to_value(Permission::verifier_run(bound.clone())).expect("serializes"),
1363            json!({"resource": "verifier", "action": "run", "scope": {"verifier": bound.as_str()}})
1364        );
1365        for (resource, action) in [("verifier", "read"), ("cards", "run")] {
1366            assert!(
1367                serde_json::from_value::<Permission>(json!({
1368                    "resource": resource,
1369                    "action": action,
1370                    "scope": {"verifier": bound.as_str()}
1371                }))
1372                .is_err(),
1373                "{resource}:{action}"
1374            );
1375        }
1376    }
1377
1378    /// Proves `tool:invoke` decodes in both approved wire forms, refuses
1379    /// non-built-in names and misplaced tool scopes, covers and intersects by
1380    /// exact name, and is neither implied by domain grants nor implies them.
1381    ///
1382    /// # Panics
1383    /// Panics when a tool grant decodes, covers, or intersects incorrectly.
1384    #[test]
1385    fn tool_invoke_is_exact_name_scoped_and_independent() {
1386        use wyrd_spec::card::agent::BuiltinTool;
1387        let query = Permission::tool_invoke(BuiltinTool::BifrostQuery);
1388        let read = Permission::tool_invoke(BuiltinTool::FsRead);
1389        let global = Permission::tool_invoke_any();
1390
1391        let decode = |value: serde_json::Value| serde_json::from_value::<Permission>(value);
1392        let global_json = json!({"resource": "tool", "action": "invoke", "scope": "all"});
1393        let exact_json =
1394            json!({"resource": "tool", "action": "invoke", "scope": {"tool": "bifrost.query"}});
1395        assert_eq!(decode(global_json.clone()).expect("global"), global);
1396        assert_eq!(decode(exact_json.clone()).expect("exact"), query);
1397        assert_eq!(
1398            serde_json::to_value(&global).expect("serialize"),
1399            global_json
1400        );
1401        assert_eq!(serde_json::to_value(&query).expect("serialize"), exact_json);
1402        assert_eq!("tool:invoke".parse::<Permission>().expect("parses"), global);
1403        for tool in BuiltinTool::ALL {
1404            let value =
1405                json!({"resource": "tool", "action": "invoke", "scope": {"tool": tool.as_str()}});
1406            assert_eq!(
1407                decode(value).expect("built-in"),
1408                Permission::tool_invoke(tool)
1409            );
1410        }
1411        for name in [
1412            "bifrost",
1413            "bifrost.*",
1414            "fs.read/etc",
1415            "/usr/bin/python",
1416            "FS.READ",
1417            "http.fetch",
1418        ] {
1419            let value = json!({"resource": "tool", "action": "invoke", "scope": {"tool": name}});
1420            assert!(decode(value).is_err(), "{name}");
1421        }
1422        for (resource, action) in [
1423            ("tool", "read"),
1424            ("tool", "run"),
1425            ("cards", "invoke"),
1426            ("bifrost_query", "read"),
1427            ("wildcard", "invoke"),
1428        ] {
1429            let value =
1430                json!({"resource": resource, "action": action, "scope": {"tool": "fs.read"}});
1431            assert!(decode(value).is_err(), "{resource}:{action}");
1432        }
1433
1434        assert!(global.covers(&query));
1435        assert!(query.covers(&query));
1436        assert!(!query.covers(&read));
1437        assert!(!query.covers(&global));
1438        assert!(Permission::wildcard().covers(&query));
1439        for domain in [
1440            Permission::card_read(),
1441            Permission::bifrost_query_read(),
1442            Permission::verifier_run_any(),
1443        ] {
1444            assert!(!domain.covers(&query), "{domain}");
1445            assert!(!global.covers(&domain), "{domain}");
1446        }
1447
1448        let set = |grants: &[Permission]| grants.iter().cloned().collect::<PermissionSet>();
1449        let meet = |left: &[Permission], right: &[Permission]| {
1450            set(left).intersection(&set(right)).expect("representable")
1451        };
1452        assert!(meet(std::slice::from_ref(&query), std::slice::from_ref(&read)).is_empty());
1453        for (left, right) in [(&global, &query), (&query, &global)] {
1454            let shared = meet(std::slice::from_ref(left), std::slice::from_ref(right));
1455            assert_eq!(shared.iter().collect::<Vec<_>>(), vec![&query]);
1456        }
1457        let attenuated = meet(&[Permission::wildcard()], std::slice::from_ref(&query));
1458        assert!(attenuated.contains(&query) && !attenuated.contains(&read));
1459
1460        let role: Vec<Permission> =
1461            serde_json::from_value(json!([global_json, exact_json, {"resource": "bifrost_query", "action": "read", "scope": "all"}]))
1462                .expect("role permissions decode");
1463        let role = role.into_iter().collect::<PermissionSet>();
1464        assert!(role.contains(&query) && role.contains(&read));
1465    }
1466
1467    /// Proves an explicit gateway grant survives merging beside `all` in either
1468    /// order and under wildcard or `AnyOf` axes, and that delegation meets
1469    /// gateway grants exactly: `all` with `all` stays `all`, explicit grants
1470    /// meet to the narrower grant, a mixed pair whose explicit result the other
1471    /// side also holds explicitly is representable, and `all` meeting an
1472    /// explicit grant alone — "that deployment, only if open" — is refused in
1473    /// both directions instead of minting the explicit grant.
1474    ///
1475    /// # Panics
1476    /// Panics when a merge drops an explicit grant, a representable meet
1477    /// differs from the exact shared authority, or an unrepresentable meet
1478    /// succeeds.
1479    #[test]
1480    fn gateway_grants_survive_merge_and_intersect_exactly() {
1481        use super::GatewayAccess;
1482        use wyrd_spec::ids::{ModelId, ProviderDeploymentName};
1483        let restricted = ProviderDeploymentName::new("restricted").expect("valid deployment");
1484        let deployment = Permission::gateway_invoke(GatewayAccess::Deployment {
1485            deployment: restricted.clone(),
1486        });
1487        let model = Permission::gateway_invoke(GatewayAccess::Model {
1488            deployment: restricted,
1489            model: ModelId::new("gpt-4o").expect("valid model"),
1490        });
1491        let any_resource_invoke = Permission {
1492            resource: Resource::AnyOf(vec![Resource::Gateway, Resource::Cards]),
1493            action: Action::Wildcard,
1494            scope: PermissionScope::All,
1495        };
1496        for broad in [
1497            Permission::gateway_invoke_any(),
1498            Permission::wildcard(),
1499            any_resource_invoke,
1500        ] {
1501            for set in [
1502                PermissionSet::from_iter([broad.clone(), deployment.clone()]),
1503                PermissionSet::from_iter([deployment.clone(), broad.clone()]),
1504            ] {
1505                assert!(set.contains_object_scoped(&model), "{broad} kept explicit");
1506                assert!(set.contains(&Permission::gateway_invoke_any()));
1507            }
1508        }
1509
1510        let all = PermissionSet::from_iter([Permission::gateway_invoke_any()]);
1511        let explicit = PermissionSet::from_iter([deployment.clone()]);
1512        let narrow = PermissionSet::from_iter([model.clone()]);
1513        assert_eq!(all.intersection(&all).expect("representable"), all);
1514        assert_eq!(
1515            explicit.intersection(&narrow).expect("representable"),
1516            narrow
1517        );
1518        assert!(all.intersection(&explicit).is_err());
1519        assert!(explicit.intersection(&all).is_err());
1520        let wildcard = PermissionSet::from_iter([Permission::wildcard()]);
1521        assert!(wildcard.intersection(&narrow).is_err());
1522
1523        let mixed = PermissionSet::from_iter([Permission::gateway_invoke_any(), deployment]);
1524        assert_eq!(mixed.intersection(&narrow).expect("representable"), narrow);
1525        assert_eq!(narrow.intersection(&mixed).expect("representable"), narrow);
1526        assert_eq!(
1527            mixed.intersection(&all).expect("representable"),
1528            all,
1529            "both hold all; only one holds the explicit grant"
1530        );
1531        assert!(
1532            PermissionSet::from_iter([Permission::card_read()])
1533                .intersection(&explicit)
1534                .expect("no overlap is the representable empty result")
1535                .is_empty()
1536        );
1537    }
1538
1539    /// Builds the schema-scoped Bifrost query-read grant the journey seeds.
1540    fn logs_schema_grant() -> Permission {
1541        Permission {
1542            resource: Resource::BifrostQuery,
1543            action: Action::Read,
1544            scope: PermissionScope::Bifrost(BifrostPermissionScope::Schema(BifrostSchemaScope {
1545                catalog: "bifrost".to_owned(),
1546                schema: "logs".to_owned(),
1547            })),
1548        }
1549    }
1550
1551    /// Builds the exact table-scoped Bifrost query-read requirement for one UID.
1552    fn table_requirement(schema: &str, uid: uuid::Uuid) -> Permission {
1553        Permission {
1554            resource: Resource::BifrostQuery,
1555            action: Action::Read,
1556            scope: PermissionScope::Bifrost(BifrostPermissionScope::Table(BifrostTableScope {
1557                catalog: "bifrost".to_owned(),
1558                schema: schema.to_owned(),
1559                table_uid: uid,
1560            })),
1561        }
1562    }
1563
1564    /// Proves the intersection keeps only authority both sides grant, at the
1565    /// narrower scope: wildcard, schema, exact-object, and disjoint grants.
1566    ///
1567    /// # Panics
1568    /// Panics when an intersection keeps broader, one-sided, or disjoint
1569    /// authority, or drops authority both sides grant.
1570    #[test]
1571    fn intersection_keeps_only_the_narrower_shared_authority() {
1572        let uid = uuid::Uuid::from_u128(0x77);
1573        let set = |permissions: Vec<Permission>| permissions.into_iter().collect::<PermissionSet>();
1574
1575        let wildcard = set(vec![Permission::wildcard()]);
1576        let target = set(vec![Permission::card_read(), logs_schema_grant()]);
1577        assert_eq!(
1578            wildcard.intersection(&target).expect("representable"),
1579            target
1580        );
1581        assert_eq!(
1582            target.intersection(&wildcard).expect("representable"),
1583            target
1584        );
1585
1586        let schema = set(vec![logs_schema_grant()]);
1587        let table = set(vec![table_requirement("logs", uid)]);
1588        assert_eq!(schema.intersection(&table).expect("representable"), table);
1589        assert_eq!(table.intersection(&schema).expect("representable"), table);
1590
1591        let other_table = set(vec![table_requirement("logs", uuid::Uuid::from_u128(0x78))]);
1592        assert!(
1593            table
1594                .intersection(&other_table)
1595                .expect("representable")
1596                .is_empty()
1597        );
1598        assert!(
1599            set(vec![table_requirement("traces", uid)])
1600                .intersection(&schema)
1601                .expect("representable")
1602                .is_empty()
1603        );
1604
1605        let all_reads = set(vec![Permission {
1606            resource: Resource::Wildcard,
1607            action: Action::Read,
1608            scope: PermissionScope::All,
1609        }]);
1610        let cards_any = set(vec![Permission {
1611            resource: Resource::AnyOf(vec![Resource::Cards, Resource::Audit]),
1612            action: Action::Wildcard,
1613            scope: PermissionScope::All,
1614        }]);
1615        assert_eq!(
1616            all_reads.intersection(&cards_any).expect("representable"),
1617            set(vec![Permission::card_read(), Permission::audit_read()])
1618        );
1619
1620        assert!(
1621            set(vec![Permission::card_write()])
1622                .intersection(&set(vec![
1623                    Permission::card_read(),
1624                    Permission::bifrost_record_write()
1625                ]))
1626                .expect("representable")
1627                .is_empty()
1628        );
1629    }
1630
1631    /// Proves the persisted permission JSON is exactly the approved three-field
1632    /// projection for `all`, schema, and table scope, in both directions.
1633    ///
1634    /// # Panics
1635    /// Panics when a permission fails to serialize or deserialize, or its
1636    /// JSON or decoded value differs from the approved projection.
1637    #[test]
1638    fn scoped_permission_json_matches_the_approved_projection() {
1639        let uid = uuid::Uuid::from_u128(0x99);
1640        let cases = [
1641            (
1642                Permission::bifrost_query_read(),
1643                json!({"resource": "bifrost_query", "action": "read", "scope": "all"}),
1644            ),
1645            (
1646                logs_schema_grant(),
1647                json!({
1648                    "resource": "bifrost_query",
1649                    "action": "read",
1650                    "scope": {"bifrost": {"schema": {"catalog": "bifrost", "schema": "logs"}}}
1651                }),
1652            ),
1653            (
1654                table_requirement("traces", uid),
1655                json!({
1656                    "resource": "bifrost_query",
1657                    "action": "read",
1658                    "scope": {"bifrost": {"table": {
1659                        "catalog": "bifrost",
1660                        "schema": "traces",
1661                        "table_uid": uid.to_string(),
1662                    }}}
1663                }),
1664            ),
1665        ];
1666
1667        for (permission, wire) in cases {
1668            assert_eq!(
1669                serde_json::to_value(&permission).expect("permission serializes"),
1670                wire
1671            );
1672            assert_eq!(
1673                serde_json::from_value::<Permission>(wire).expect("permission deserializes"),
1674                permission
1675            );
1676        }
1677    }
1678
1679    /// Proves Bifrost object scope applies to record writes as well as query
1680    /// reads, a table-scoped write covers only its own table, and the scope is
1681    /// still refused on any other resource or action pairing.
1682    ///
1683    /// # Panics
1684    /// Panics when the table-scoped record write fails validation or its
1685    /// round trip, covers another table or the unscoped requirement, is not
1686    /// covered by the unscoped write, or validates on record read, query
1687    /// write, or table write.
1688    #[test]
1689    fn bifrost_scope_applies_to_record_write_and_query_read_only() {
1690        let calls = uuid::Uuid::from_u128(0xca11);
1691        let spans = uuid::Uuid::from_u128(0x5a45);
1692        let table_write = |uid| Permission {
1693            resource: Resource::BifrostRecord,
1694            action: Action::Write,
1695            scope: PermissionScope::Bifrost(BifrostPermissionScope::Table(BifrostTableScope {
1696                catalog: "bifrost".to_owned(),
1697                schema: "gateway".to_owned(),
1698                table_uid: uid,
1699            })),
1700        };
1701        table_write(calls)
1702            .validate()
1703            .expect("scoped record write is valid");
1704        assert!(table_write(calls).covers(&table_write(calls)));
1705        assert!(!table_write(calls).covers(&table_write(spans)));
1706        assert!(
1707            !table_write(calls).covers(&Permission::bifrost_record_write()),
1708            "a table-scoped write never covers the unscoped requirement"
1709        );
1710        assert!(Permission::bifrost_record_write().covers(&table_write(spans)));
1711        let wire = serde_json::to_value(table_write(calls)).expect("serializes");
1712        assert_eq!(
1713            serde_json::from_value::<Permission>(wire).expect("round-trips"),
1714            table_write(calls)
1715        );
1716        for (resource, action) in [
1717            (Resource::BifrostRecord, Action::Read),
1718            (Resource::BifrostQuery, Action::Write),
1719            (Resource::BifrostTable, Action::Write),
1720        ] {
1721            let permission = Permission {
1722                resource,
1723                action,
1724                ..table_write(calls)
1725            };
1726            assert!(permission.validate().is_err(), "{permission}");
1727        }
1728    }
1729
1730    /// Proves `scope` is mandatory: the pre-scope two-field form is rejected
1731    /// rather than defaulted, so no compatibility decoder exists.
1732    #[test]
1733    fn scope_is_required_and_has_no_compatibility_decoder() {
1734        let error = serde_json::from_value::<Permission>(json!({
1735            "resource": "bifrost_query",
1736            "action": "read"
1737        }))
1738        .expect_err("the unscoped two-field form is not accepted");
1739
1740        assert!(error.to_string().contains("scope"), "{error}");
1741    }
1742
1743    /// Proves a Bifrost object scope only attaches to a resource that owns a
1744    /// Bifrost object; `cards` and `wildcard` both fail closed at decode.
1745    ///
1746    /// # Panics
1747    /// Panics when a schema scope decodes on `cards` or `wildcard`, or the
1748    /// decode error does not name the resource.
1749    #[test]
1750    fn bifrost_scope_is_rejected_on_an_unrelated_resource() {
1751        for resource in ["cards", "wildcard"] {
1752            let error = serde_json::from_value::<Permission>(json!({
1753                "resource": resource,
1754                "action": "read",
1755                "scope": {"bifrost": {"schema": {"catalog": "bifrost", "schema": "logs"}}}
1756            }))
1757            .expect_err("a Bifrost object scope needs a Bifrost object resource");
1758
1759            assert!(error.to_string().contains("resource"), "{error}");
1760        }
1761    }
1762
1763    /// Proves Bifrost object scope is valid only for the exact `read` action.
1764    ///
1765    /// Without this, persisted or signed JSON could carry a Bifrost scope on
1766    /// `wildcard`, which then covers the required read and turns corrupt role
1767    /// JSON into effective read authority instead of a decode failure.
1768    ///
1769    /// # Panics
1770    /// Panics when a schema scope decodes on `write`, `wildcard`, or an
1771    /// `any_of` action, a refusal does not name the action, or the exact
1772    /// query read is refused.
1773    #[test]
1774    fn bifrost_scope_is_rejected_on_a_non_read_action() {
1775        for action in [
1776            json!("write"),
1777            json!("wildcard"),
1778            json!({"any_of": ["read", "write"]}),
1779        ] {
1780            let error = serde_json::from_value::<Permission>(json!({
1781                "resource": "bifrost_query",
1782                "action": action,
1783                "scope": {"bifrost": {"schema": {"catalog": "bifrost", "schema": "logs"}}}
1784            }))
1785            .expect_err("a Bifrost object scope only applies to an exact read");
1786
1787            assert!(error.to_string().contains("action"), "{error}");
1788        }
1789
1790        serde_json::from_value::<Permission>(json!({
1791            "resource": "bifrost_query",
1792            "action": "read",
1793            "scope": {"bifrost": {"schema": {"catalog": "bifrost", "schema": "logs"}}}
1794        }))
1795        .expect("exact query read stays valid for Bifrost scope");
1796    }
1797
1798    /// Proves a structurally valid but empty object identity is not an object:
1799    /// malformed identities fail at decode, never at check time.
1800    ///
1801    /// # Panics
1802    /// Panics when a schema scope with an empty schema segment decodes.
1803    #[test]
1804    fn malformed_scope_identity_is_rejected_at_decode() {
1805        assert!(
1806            serde_json::from_value::<Permission>(json!({
1807                "resource": "bifrost_query",
1808                "action": "read",
1809                "scope": {"bifrost": {"schema": {"catalog": "bifrost", "schema": ""}}}
1810            }))
1811            .is_err(),
1812            "an empty schema segment is not an object identity"
1813        );
1814    }
1815
1816    /// Proves coverage requires resource, action, and scope together, and that
1817    /// wildcard authority stays object-wide only because its scope is `All`.
1818    #[test]
1819    fn coverage_is_three_axis() {
1820        let logs = uuid::Uuid::from_u128(1);
1821        let traces = uuid::Uuid::from_u128(2);
1822
1823        // Schema scope reaches current and future tables in that exact schema.
1824        assert!(logs_schema_grant().covers(&table_requirement("logs", logs)));
1825        assert!(logs_schema_grant().covers(&table_requirement("logs", traces)));
1826        assert!(!logs_schema_grant().covers(&table_requirement("traces", traces)));
1827
1828        // Exact table scope reaches only its own UID.
1829        let exact = table_requirement("traces", traces);
1830        assert!(exact.covers(&table_requirement("traces", traces)));
1831        assert!(!exact.covers(&table_requirement("traces", logs)));
1832
1833        // `All` reaches every object, but only for its own resource and action.
1834        assert!(Permission::bifrost_query_read().covers(&table_requirement("logs", logs)));
1835        assert!(!Permission::bifrost_table_read().covers(&table_requirement("logs", logs)));
1836
1837        // A wildcard grant is object-wide only because its scope is `All`.
1838        assert!(Permission::wildcard().covers(&table_requirement("logs", logs)));
1839
1840        // A scoped grant never satisfies an object-wide requirement.
1841        assert!(!logs_schema_grant().covers(&Permission::bifrost_query_read()));
1842    }
1843
1844    /// Proves coarse operation admission and authoritative object coverage are
1845    /// distinct: a scoped grant admits the operation but authorizes only its object.
1846    #[test]
1847    fn covers_operation_admits_a_scoped_grant_without_authorizing_an_object() {
1848        let set = PermissionSet::from_iter([logs_schema_grant()]);
1849
1850        assert!(set.covers_operation(&Resource::BifrostQuery, &Action::Read));
1851        assert!(!set.covers_operation(&Resource::BifrostTable, &Action::Read));
1852        assert!(!set.contains(&Permission::bifrost_query_read()));
1853        assert!(set.contains(&table_requirement("logs", uuid::Uuid::from_u128(7))));
1854        assert!(!set.contains(&table_requirement("traces", uuid::Uuid::from_u128(7))));
1855    }
1856
1857    /// Proves set subsumption runs on all three axes: disjoint scopes are both
1858    /// retained, and an `All`-scoped grant absorbs every narrower scope.
1859    #[test]
1860    fn permission_set_keeps_scopes_that_do_not_subsume_each_other() {
1861        let mut set = PermissionSet::from_iter([logs_schema_grant()]);
1862        set.insert(table_requirement("traces", uuid::Uuid::from_u128(3)));
1863
1864        assert_eq!(set.len(), 2);
1865
1866        set.insert(Permission::bifrost_query_read());
1867
1868        assert_eq!(set.len(), 1);
1869        assert_eq!(set.iter().next(), Some(&Permission::bifrost_query_read()));
1870    }
1871
1872    #[test]
1873    fn permission_wire_token_roundtrips() {
1874        let parsed = "cards:write"
1875            .parse::<Permission>()
1876            .expect("permission parses");
1877
1878        assert_eq!(parsed, Permission::card_write());
1879        assert_eq!(parsed.to_string(), "cards:write");
1880    }
1881
1882    /// Proves the retired `evals` resource is an ordinary unknown resource in
1883    /// every wire form — string grants, structured grants and `any_of`
1884    /// members, for `run` and `invoke` — while `verifier:run` and unrelated
1885    /// grants keep parsing and covering exactly as before.
1886    ///
1887    /// # Panics
1888    /// Panics when a retired form parses or a retained grant stops parsing or
1889    /// covering.
1890    #[test]
1891    fn retired_permission_is_unknown() {
1892        for action in ["run", "invoke"] {
1893            assert!(
1894                format!("evals:{action}").parse::<Permission>().is_err(),
1895                "evals:{action}"
1896            );
1897            assert!(
1898                serde_json::from_value::<Permission>(
1899                    json!({"resource": "evals", "action": action, "scope": "all"})
1900                )
1901                .is_err(),
1902                "structured evals:{action}"
1903            );
1904            assert!(
1905                serde_json::from_value::<Permission>(json!({
1906                    "resource": {"any_of": ["operators", "evals"]},
1907                    "action": action,
1908                    "scope": "all"
1909                }))
1910                .is_err(),
1911                "any_of evals:{action}"
1912            );
1913        }
1914        let verifier = "verifier:run"
1915            .parse::<Permission>()
1916            .expect("verifier:run parses");
1917        assert_eq!(verifier, Permission::verifier_run_any());
1918        assert!(verifier.covers(&Permission::verifier_run(
1919            wyrd_spec::ids::CardUid::new("01927b4c-0000-7000-8000-000000000001").expect("uid")
1920        )));
1921        assert_eq!(
1922            "operators:invoke"
1923                .parse::<Permission>()
1924                .expect("unrelated grant parses"),
1925            Permission::operator_invoke()
1926        );
1927    }
1928}