Skip to main content

golem_rust/
permissions.rs

1// Copyright 2024-2026 Golem Cloud
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Typed permission-card management over the `golem:permissions` WIT interfaces.
16//!
17//! A [`PermissionCard`] is an unforgeable capability held by the host. Guest code
18//! can only obtain one from the host ([`wallet`], [`derive_from_wallet`],
19//! [`PermissionCard::derive`], [`PolymorphicPermissionCard::derive`]) or
20//! receive one as an agent method argument.
21//! A [`PolymorphicPermissionCard`] is a template card whose grants may contain
22//! slot variables; the agent's initial card is one. A [`ScopeCard`]
23//! ([`derive_scope`]) lives only in memory for the current invocation and can
24//! only be lent to awaited agent calls. This module offers no conversion
25//! between the three card types, and nothing in it turns a [`CardId`], a grant
26//! list or any other data into a card. A card value that goes through the
27//! schema-value codec takes the type that the decoder declares; the host keeps
28//! the real kind of the card.
29//!
30//! Every operation is checked by the host against the caller's own cards:
31//! deriving requires `card:derive`, inspecting requires `card:inspect`,
32//! revoking requires `card:revoke` and installing requires `card:install`.
33//! An expected refusal is returned as a [`PermissionError`].
34//!
35//! # Typical usage
36//!
37//! ```rust,ignore
38//! use golem_rust::permissions::{self, Bounds, Lifespan, PatternGrant};
39//!
40//! let grant = PatternGrant::new("card", "*", "*", "inspect", "*");
41//! let card = permissions::derive_from_wallet(
42//!     &Bounds {
43//!         lower_positive: vec![grant.clone()],
44//!         ..Bounds::default()
45//!     },
46//!     None,
47//! )?;
48//! let child = card.derive(
49//!     &Bounds {
50//!         lower_positive: vec![grant],
51//!         ..Bounds::default()
52//!     },
53//!     Some(&Lifespan::iso8601("PT1H")?),
54//! )?;
55//! let revoked = card.revoke()?; // revokes `card` and `child`
56//! ```
57
58use crate::bindings::golem::permissions::{
59    derive as raw_derive, inspect as raw_inspect, kernel_introspection as raw_kernel,
60    revoke as raw_revoke, types as raw, wallet as raw_wallet,
61};
62use crate::schema::wit::GuestPermissionCardHandle;
63use crate::schema::wit::direct::{
64    WireError, WirePreflight, WireReader, WireSchemaBuilder, WireWriter,
65};
66use crate::schema::wit::wire;
67use crate::{AgentId, FromSchema, FromWire, IntoSchema, IntoWire, Uuid, WireSchema};
68use std::collections::HashMap;
69use std::sync::Arc;
70#[cfg(feature = "export_golem_agentic")]
71use std::sync::Weak;
72use std::time::Duration;
73
74#[cfg(feature = "export_golem_agentic")]
75use crate::schema::{
76    FromSchemaError, PermissionCardSpec, SchemaBuilder, SchemaType, SchemaValue, TypeId,
77};
78
79/// A permission card held by the calling component.
80///
81/// The card wraps a host resource handle. It has no public constructor, it
82/// cannot be built from a [`CardId`] or from data, and it cannot be serialized.
83///
84/// Borrowing operations ([`id`](Self::id), [`parents`](Self::parents),
85/// [`expires_at`](Self::expires_at), [`inspect`](Self::inspect),
86/// [`derive`](Self::derive)) leave the card in place. Consuming operations
87/// ([`revoke`](Self::revoke), [`install`](Self::install)) and passing the card
88/// as an agent method argument or return value move the host handle out of
89/// the card. Every later use of a moved card panics.
90///
91/// Dropping a card releases the caller's handle. It does not revoke the card
92/// and does not remove it from the caller's wallet.
93///
94/// Decoding a card from an agent method argument or result
95/// ([`FromSchema`](crate::FromSchema), [`FromWire`](crate::FromWire)) takes
96/// the handle out of the decoded value, so a second decode of the same value
97/// fails and two card objects never share one handle. Safe code cannot build
98/// such a value without an existing handle. The `golem-schema` codec handle
99/// types expose hidden constructors and accessors that can alias a handle the
100/// component already holds, and wit-bindgen's `unsafe` resource constructors
101/// can name any handle number. Neither path creates authority: the host
102/// validates every handle against the component's own resource table.
103pub struct PermissionCard {
104    handle: GuestPermissionCardHandle,
105}
106
107/// An owned persistent card, labelled with the kind returned by the host.
108#[derive(Debug)]
109pub enum PersistentCard {
110    Concrete(PermissionCard),
111    Template(PolymorphicPermissionCard),
112}
113
114impl PersistentCard {
115    fn from_raw(raw: raw::PersistentCard) -> Self {
116        match raw {
117            raw::PersistentCard::Concrete(card) => Self::Concrete(PermissionCard::from_raw(card)),
118            raw::PersistentCard::Template(card) => {
119                Self::Template(PolymorphicPermissionCard::from_raw(card))
120            }
121        }
122    }
123
124    /// The identifier of this card.
125    pub fn id(&self) -> CardId {
126        match self {
127            Self::Concrete(card) => card.id(),
128            Self::Template(card) => card.id(),
129        }
130    }
131
132    /// The identifiers of the cards this card was derived from.
133    pub fn parents(&self) -> Vec<CardId> {
134        match self {
135            Self::Concrete(card) => card.parents(),
136            Self::Template(card) => card.parents(),
137        }
138    }
139
140    /// The expiry time, or `None` for an unbounded lifespan.
141    pub fn expires_at(&self) -> Option<Timestamp> {
142        match self {
143            Self::Concrete(card) => card.expires_at(),
144            Self::Template(card) => card.expires_at(),
145        }
146    }
147
148    /// Returns the grant lists. Requires `card:inspect`.
149    pub fn inspect(&self) -> Result<CardView, PermissionError> {
150        match self {
151            Self::Concrete(card) => card.inspect(),
152            Self::Template(card) => card.inspect(),
153        }
154    }
155
156    /// Derives a child whose kind is selected by the host.
157    pub fn derive(
158        &self,
159        bounds: &Bounds,
160        lifespan: Option<&Lifespan>,
161    ) -> Result<Self, PermissionError> {
162        match self {
163            Self::Concrete(card) => card.derive(bounds, lifespan),
164            Self::Template(card) => card.derive(bounds, lifespan),
165        }
166    }
167
168    /// Revokes this card and its descendants, consuming the handle.
169    pub fn revoke(self) -> Result<u32, PermissionError> {
170        match self {
171            Self::Concrete(card) => card.revoke(),
172            Self::Template(card) => card.revoke(),
173        }
174    }
175
176    /// Installs this card for the target, consuming the handle.
177    pub fn install(self, target: &Holder) -> Result<(), PermissionError> {
178        match self {
179            Self::Concrete(card) => card.install(target),
180            Self::Template(card) => card.install(target),
181        }
182    }
183}
184
185const CARD_MOVED: &str = "permission card has already been moved (revoked, installed, or \
186     transferred to another agent) and can no longer be used";
187
188impl PermissionCard {
189    fn from_raw(raw: wire::PermissionCard) -> Self {
190        Self {
191            handle: GuestPermissionCardHandle::new(raw),
192        }
193    }
194
195    fn with_raw<R>(&self, f: impl FnOnce(&wire::PermissionCard) -> R) -> R {
196        with_raw_card(&self.handle, f)
197    }
198
199    /// The identifier of this card.
200    ///
201    /// # Panics
202    ///
203    /// Panics if the card was moved. The host traps if the card was revoked
204    /// or is unknown.
205    pub fn id(&self) -> CardId {
206        CardId::from_raw(self.with_raw(raw::id))
207    }
208
209    /// The identifiers of the cards this card was derived from.
210    ///
211    /// # Panics
212    ///
213    /// Panics if the card was moved. The host traps if the card was revoked
214    /// or is unknown.
215    pub fn parents(&self) -> Vec<CardId> {
216        card_parents(&self.handle)
217    }
218
219    /// The expiry time of this card, or `None` for a card without a lifespan.
220    ///
221    /// # Panics
222    ///
223    /// Panics if the card was moved. The host traps if the card was revoked
224    /// or is unknown.
225    pub fn expires_at(&self) -> Option<Timestamp> {
226        self.with_raw(raw::expires_at).map(Timestamp::from)
227    }
228
229    /// Returns the four grant lists of this card. Requires `card:inspect`.
230    ///
231    /// # Panics
232    ///
233    /// Panics if the card was moved.
234    pub fn inspect(&self) -> Result<CardView, PermissionError> {
235        inspect_card(&self.handle)
236    }
237
238    /// Derives a child card from this card. Requires `card:derive`.
239    ///
240    /// The positive lists of `bounds` select the grants of this card that the
241    /// child retains. The negative lists of `bounds` are denials that the child
242    /// adds. The child's lifespan cannot exceed the lifespan of this card.
243    /// The host selects the child's kind, independently of this card's kind.
244    ///
245    /// # Panics
246    ///
247    /// Panics if the card was moved.
248    pub fn derive(
249        &self,
250        bounds: &Bounds,
251        lifespan: Option<&Lifespan>,
252    ) -> Result<PersistentCard, PermissionError> {
253        derive_child(&self.handle, bounds, lifespan)
254    }
255
256    /// Revokes this card and every card derived from it. Requires `card:revoke`.
257    ///
258    /// Returns the number of revoked cards. The card is moved even when the
259    /// host refuses the revocation.
260    ///
261    /// # Panics
262    ///
263    /// Panics if the card was already moved.
264    pub fn revoke(self) -> Result<u32, PermissionError> {
265        revoke_card(&self.handle)
266    }
267
268    /// Installs this card into the wallet of `target`. Requires `card:install`.
269    ///
270    /// The host currently accepts only [`Holder::Agent`] targets and refuses
271    /// account and application targets with [`PermissionError::NotPermitted`].
272    /// A target that does not match the card's recipient pattern fails with
273    /// [`PermissionError::InstallRecipientMismatch`]. The card is moved even
274    /// when the host refuses the installation.
275    ///
276    /// # Panics
277    ///
278    /// Panics if the card was already moved.
279    pub fn install(self, target: &Holder) -> Result<(), PermissionError> {
280        install_card(&self.handle, target)
281    }
282}
283
284impl std::fmt::Debug for PermissionCard {
285    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
286        write!(f, "PermissionCard(<{}>)", handle_state(&self.handle))
287    }
288}
289
290/// A template permission card held by the calling component.
291///
292/// The grants of a template card may contain slot variables, such as `?agent`
293/// or `?env` in an owner, which the host resolves for each holder. The
294/// agent's initial card is a template card. [`wallet`] returns template cards
295/// as [`WalletCard::Template`].
296///
297/// A template card follows the same rules as a [`PermissionCard`]: it has no
298/// public constructor, it cannot be built from a [`CardId`] or from data, it
299/// cannot be serialized, and [`revoke`](Self::revoke),
300/// [`install`](Self::install) and passing it as an agent method argument or
301/// return value move its host handle. Every later use of a moved card panics.
302///
303/// The SDK offers no conversion between a template card and a
304/// [`PermissionCard`].
305/// [`derive`](Self::derive) returns the persistent kind selected by the host.
306/// As an agent method parameter or result, this
307/// type declares a `permission-card` with `polymorphic: true`, and a
308/// [`PermissionCard`] declares `polymorphic: false`. The host refuses an agent
309/// method input or output whose card does not match the declared kind. The
310/// guest codecs see only the handle: a decoded card takes the type that the
311/// decoder declares.
312pub struct PolymorphicPermissionCard {
313    handle: GuestPermissionCardHandle,
314}
315
316impl PolymorphicPermissionCard {
317    fn from_raw(raw: wire::PermissionCard) -> Self {
318        Self {
319            handle: GuestPermissionCardHandle::new(raw),
320        }
321    }
322
323    fn with_raw<R>(&self, f: impl FnOnce(&wire::PermissionCard) -> R) -> R {
324        with_raw_card(&self.handle, f)
325    }
326
327    /// The identifier of this card.
328    ///
329    /// # Panics
330    ///
331    /// Panics if the card was moved. The host traps if the card was revoked
332    /// or is unknown.
333    pub fn id(&self) -> CardId {
334        CardId::from_raw(self.with_raw(raw::id))
335    }
336
337    /// The identifiers of the cards this card was derived from.
338    ///
339    /// # Panics
340    ///
341    /// Panics if the card was moved. The host traps if the card was revoked
342    /// or is unknown.
343    pub fn parents(&self) -> Vec<CardId> {
344        card_parents(&self.handle)
345    }
346
347    /// The expiry time of this card, or `None` for a card without a lifespan.
348    ///
349    /// # Panics
350    ///
351    /// Panics if the card was moved. The host traps if the card was revoked
352    /// or is unknown.
353    pub fn expires_at(&self) -> Option<Timestamp> {
354        self.with_raw(raw::expires_at).map(Timestamp::from)
355    }
356
357    /// Returns the four grant lists of this card, with their slot variables.
358    /// Requires `card:inspect`.
359    ///
360    /// # Panics
361    ///
362    /// Panics if the card was moved.
363    pub fn inspect(&self) -> Result<CardView, PermissionError> {
364        inspect_card(&self.handle)
365    }
366
367    /// Derives a persistent child card from this card. Requires `card:derive`.
368    ///
369    /// The host first resolves this template card for the caller, so this
370    /// card must be in the caller's wallet. The bounds then apply as in
371    /// [`PermissionCard::derive`].
372    ///
373    /// # Panics
374    ///
375    /// Panics if the card was moved.
376    pub fn derive(
377        &self,
378        bounds: &Bounds,
379        lifespan: Option<&Lifespan>,
380    ) -> Result<PersistentCard, PermissionError> {
381        derive_child(&self.handle, bounds, lifespan)
382    }
383
384    /// Revokes this card and every card derived from it, as
385    /// [`PermissionCard::revoke`] does. Requires `card:revoke`.
386    ///
387    /// The card is moved even when the host refuses the revocation.
388    ///
389    /// # Panics
390    ///
391    /// Panics if the card was already moved.
392    pub fn revoke(self) -> Result<u32, PermissionError> {
393        revoke_card(&self.handle)
394    }
395
396    /// Installs this card into the wallet of `target`, as
397    /// [`PermissionCard::install`] does. Requires `card:install`.
398    ///
399    /// The host resolves the template for `target` and installs the resolved
400    /// card; the template card stays in the caller's wallet. The handle is
401    /// moved even when the host refuses the installation.
402    ///
403    /// # Panics
404    ///
405    /// Panics if the card was already moved.
406    pub fn install(self, target: &Holder) -> Result<(), PermissionError> {
407        install_card(&self.handle, target)
408    }
409}
410
411impl std::fmt::Debug for PolymorphicPermissionCard {
412    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
413        write!(
414            f,
415            "PolymorphicPermissionCard(<{}>)",
416            handle_state(&self.handle)
417        )
418    }
419}
420
421fn handle_state(handle: &GuestPermissionCardHandle) -> &'static str {
422    if handle.is_present() {
423        "present"
424    } else {
425        "moved"
426    }
427}
428
429fn with_raw_card<R>(
430    handle: &GuestPermissionCardHandle,
431    f: impl FnOnce(&wire::PermissionCard) -> R,
432) -> R {
433    handle
434        .with_handle(f)
435        .unwrap_or_else(|| panic!("{CARD_MOVED}"))
436}
437
438fn take_raw_card(handle: &GuestPermissionCardHandle) -> wire::PermissionCard {
439    handle.take().unwrap_or_else(|| panic!("{CARD_MOVED}"))
440}
441
442fn card_parents(handle: &GuestPermissionCardHandle) -> Vec<CardId> {
443    with_raw_card(handle, raw::parents)
444        .into_iter()
445        .map(CardId::from_raw)
446        .collect()
447}
448
449fn inspect_card(handle: &GuestPermissionCardHandle) -> Result<CardView, PermissionError> {
450    with_raw_card(handle, raw_inspect::inspect_card)
451        .map(CardView::from)
452        .map_err(PermissionError::from)
453}
454
455fn derive_child(
456    handle: &GuestPermissionCardHandle,
457    bounds: &Bounds,
458    lifespan: Option<&Lifespan>,
459) -> Result<PersistentCard, PermissionError> {
460    let bounds = RawBounds::from(bounds);
461    let lifespan = lifespan.map(Lifespan::to_raw);
462    with_raw_card(handle, |card| {
463        raw_derive::derive(
464            card,
465            &bounds.lower_positive,
466            &bounds.lower_negative,
467            &bounds.upper_positive,
468            &bounds.upper_negative,
469            lifespan.as_ref(),
470        )
471    })
472    .map(PersistentCard::from_raw)
473    .map_err(PermissionError::from)
474}
475
476fn revoke_card(handle: &GuestPermissionCardHandle) -> Result<u32, PermissionError> {
477    raw_revoke::revoke_card(take_raw_card(handle)).map_err(PermissionError::from)
478}
479
480fn install_card(
481    handle: &GuestPermissionCardHandle,
482    target: &Holder,
483) -> Result<(), PermissionError> {
484    let target = target.to_raw();
485    raw_wallet::install_card(take_raw_card(handle), &target).map_err(PermissionError::from)
486}
487
488/// A card in the caller's [`wallet`], labelled with its kind.
489///
490/// The three cases hold three distinct card types, and this module offers no
491/// conversion between them.
492#[derive(Debug)]
493pub enum WalletCard {
494    /// A concrete card: every grant names concrete owners and resources.
495    Concrete(PermissionCard),
496    /// A template card: its grants may contain slot variables (`?agent`,
497    /// `?env`, ...), which resolve for each holder.
498    Template(PolymorphicPermissionCard),
499    /// The scope card lent to the current invocation. The host lists it only
500    /// if the agent holds `card:inspect`, and retires it when the invocation
501    /// ends.
502    Scope(ScopeCard),
503}
504
505impl WalletCard {
506    fn from_raw(raw: raw::WalletCard) -> Self {
507        match raw {
508            raw::WalletCard::Concrete(card) => Self::Concrete(PermissionCard::from_raw(card)),
509            raw::WalletCard::Template(card) => {
510                Self::Template(PolymorphicPermissionCard::from_raw(card))
511            }
512            raw::WalletCard::Scope(card) => Self::Scope(ScopeCard {
513                raw: Arc::new(card),
514            }),
515        }
516    }
517
518    /// The identifier of this card.
519    ///
520    /// # Panics
521    ///
522    /// Panics if the card was moved. The host traps if the card was revoked
523    /// or is unknown.
524    pub fn id(&self) -> CardId {
525        match self {
526            Self::Concrete(card) => card.id(),
527            Self::Template(card) => card.id(),
528            Self::Scope(card) => card.id(),
529        }
530    }
531}
532
533/// Returns every card in the caller's wallet, labelled with its kind.
534///
535/// Each returned card is an owned handle. Drop the cards that you do not keep
536/// to release their handles; dropping a card does not change the wallet.
537///
538/// During an invocation that received a scope card, the result also contains
539/// that scope card as [`WalletCard::Scope`], if the agent holds
540/// `card:inspect`.
541pub fn wallet() -> Vec<WalletCard> {
542    raw_wallet::self_wallet()
543        .into_iter()
544        .map(WalletCard::from_raw)
545        .collect()
546}
547
548/// Returns the version token of the caller's wallet. The token changes
549/// whenever a card is installed into or removed from the wallet.
550pub fn wallet_version() -> WalletVersionToken {
551    raw_wallet::self_version().into()
552}
553
554/// Derives a card from the caller's wallet. Requires `card:derive`.
555///
556/// Unlike [`PermissionCard::derive`], all four lists of `bounds` are the full
557/// requested grant lists of the new card. The host refuses the request if the
558/// wallet does not cover them.
559///
560/// The host returns a concrete or template card. Requested slots name the
561/// eventual installed holder; template parents remain scoped to the caller.
562/// The host validates slots in every requested grant, including denials.
563/// Tool owners with slots use metadata-free normalization: short aliases
564/// that need metadata are refused.
565pub fn derive_from_wallet(
566    bounds: &Bounds,
567    lifespan: Option<&Lifespan>,
568) -> Result<PersistentCard, PermissionError> {
569    let bounds = RawBounds::from(bounds);
570    let lifespan = lifespan.map(Lifespan::to_raw);
571    raw_derive::derive_from_wallet(
572        &bounds.lower_positive,
573        &bounds.lower_negative,
574        &bounds.upper_positive,
575        &bounds.upper_negative,
576        lifespan.as_ref(),
577    )
578    .map(PersistentCard::from_raw)
579    .map_err(PermissionError::from)
580}
581
582/// Derives a scope card from one or more parents. Requires `card:derive`.
583///
584/// A parent is a card in the caller's wallet, concrete or template, or a scope
585/// card of the current invocation. The host resolves a template parent for
586/// the caller. All four lists of `bounds` are the full requested grant lists
587/// of the new card.
588/// Requested slots are refused with [`PermissionError::InvalidPattern`]:
589/// scope grants must be concrete.
590///
591/// # Panics
592///
593/// Panics if a parent card was moved.
594pub fn derive_scope(
595    parents: &[ScopeParent<'_>],
596    bounds: &Bounds,
597) -> Result<ScopeCard, PermissionError> {
598    let bounds = RawBounds::from(bounds);
599    with_raw_parents(parents, |parents| {
600        raw_derive::derive_scope(
601            parents,
602            &bounds.lower_positive,
603            &bounds.lower_negative,
604            &bounds.upper_positive,
605            &bounds.upper_negative,
606        )
607    })
608    .map(|raw| ScopeCard { raw: Arc::new(raw) })
609    .map_err(PermissionError::from)
610}
611
612/// A parent of [`derive_scope`]: a persistent card, a template card or a
613/// scope card.
614#[derive(Clone, Copy, Debug)]
615pub enum ScopeParent<'a> {
616    Card(&'a PermissionCard),
617    Template(&'a PolymorphicPermissionCard),
618    Scope(&'a ScopeCard),
619}
620
621impl<'a> From<&'a PermissionCard> for ScopeParent<'a> {
622    fn from(card: &'a PermissionCard) -> Self {
623        Self::Card(card)
624    }
625}
626
627impl<'a> From<&'a PersistentCard> for ScopeParent<'a> {
628    fn from(card: &'a PersistentCard) -> Self {
629        match card {
630            PersistentCard::Concrete(card) => Self::Card(card),
631            PersistentCard::Template(card) => Self::Template(card),
632        }
633    }
634}
635
636impl<'a> From<&'a PolymorphicPermissionCard> for ScopeParent<'a> {
637    fn from(card: &'a PolymorphicPermissionCard) -> Self {
638        Self::Template(card)
639    }
640}
641
642impl<'a> From<&'a WalletCard> for ScopeParent<'a> {
643    fn from(card: &'a WalletCard) -> Self {
644        match card {
645            WalletCard::Concrete(card) => Self::Card(card),
646            WalletCard::Template(card) => Self::Template(card),
647            WalletCard::Scope(card) => Self::Scope(card),
648        }
649    }
650}
651
652impl<'a> From<&'a ScopeCard> for ScopeParent<'a> {
653    fn from(card: &'a ScopeCard) -> Self {
654        Self::Scope(card)
655    }
656}
657
658/// An invocation-local card derived with [`derive_scope`].
659///
660/// A scope card is valid only during the invocation that derived it. Lend it
661/// to the callee of an awaited agent call with a generated client's
662/// `with_scope_card` or `set_default_scope_card`, or with the
663/// `*_with_scope_card` methods of the dynamic clients; the callee holds its
664/// authority until that call ends. Lending borrows the card and never
665/// consumes it. When the invocation ends, the host retires the scope card:
666/// the handle stays valid until it is dropped, but lending it to an awaited
667/// call, directly or as a client's default, fails with a `denied` RPC error,
668/// and using it as a [`derive_scope`] parent fails with
669/// [`PermissionError::NotPermitted`].
670///
671/// A scope card cannot be revoked, installed, derived into a persistent card,
672/// serialized, or passed as an agent method argument or result. Dropping it
673/// releases the handle.
674pub struct ScopeCard {
675    raw: Arc<wire::PermissionCard>,
676}
677
678impl ScopeCard {
679    /// The identifier of this scope card.
680    pub fn id(&self) -> CardId {
681        CardId::from_raw(raw::id(&self.raw))
682    }
683
684    /// The identifiers of the persistent root cards of this scope card.
685    pub fn parents(&self) -> Vec<CardId> {
686        raw::parents(&self.raw)
687            .into_iter()
688            .map(CardId::from_raw)
689            .collect()
690    }
691
692    /// The earliest expiry of the root cards of this scope card, or `None`
693    /// when no root card expires. The scope card stops authorizing then.
694    pub fn expires_at(&self) -> Option<Timestamp> {
695        raw::expires_at(&self.raw).map(Timestamp::from)
696    }
697
698    /// Returns the four grant lists of this scope card. Requires `card:inspect`.
699    pub fn inspect(&self) -> Result<CardView, PermissionError> {
700        raw_inspect::inspect_card(&self.raw)
701            .map(CardView::from)
702            .map_err(PermissionError::from)
703    }
704
705    #[cfg(feature = "export_golem_agentic")]
706    pub(crate) fn raw(&self) -> &wire::PermissionCard {
707        &self.raw
708    }
709}
710
711impl std::fmt::Debug for ScopeCard {
712    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
713        f.write_str("ScopeCard(..)")
714    }
715}
716
717/// Lists the resource classes that the permission kernel knows, with their verbs.
718pub fn list_modules() -> Vec<KernelModule> {
719    raw_kernel::list_modules()
720        .into_iter()
721        .map(KernelModule::from)
722        .collect()
723}
724
725/// Checks that `grant` names a known class and verb and uses valid patterns.
726pub fn validate_grant(grant: &PatternGrant) -> Result<(), PermissionError> {
727    raw_kernel::validate_grant(&grant.to_raw()).map_err(PermissionError::from)
728}
729
730#[cfg(feature = "export_golem_agentic")]
731const DEFAULT_SCOPE_CARD_DROPPED: &str = "the client's default scope card was dropped; clear \
732     the default before dropping the scope card";
733
734/// The default scope card of a generated agent client.
735///
736/// The slot only borrows the scope card: it keeps a weak reference, so
737/// dropping the scope card releases its handle even while a client still
738/// names it as its default. An awaited call through such a client panics.
739#[doc(hidden)]
740#[cfg(feature = "export_golem_agentic")]
741#[derive(Clone, Default)]
742pub struct __DefaultScopeCard(Option<Weak<wire::PermissionCard>>);
743
744#[cfg(feature = "export_golem_agentic")]
745impl __DefaultScopeCard {
746    pub fn set(&mut self, card: &ScopeCard) {
747        self.0 = Some(Arc::downgrade(&card.raw));
748    }
749
750    pub fn clear(&mut self) {
751        self.0 = None;
752    }
753
754    /// The scope card to lend to one awaited call: the per-call card if there
755    /// is one, else the default.
756    ///
757    /// Generated clients call this before they encode the call input, so a
758    /// dropped default fails before any argument card moves.
759    ///
760    /// # Panics
761    ///
762    /// Panics if the default scope card was dropped.
763    pub fn resolve(&self, scope_card: Option<&ScopeCard>) -> Option<ScopeCard> {
764        match (scope_card, &self.0) {
765            (Some(card), _) => Some(ScopeCard {
766                raw: card.raw.clone(),
767            }),
768            (None, Some(default)) => Some(ScopeCard {
769                raw: default
770                    .upgrade()
771                    .unwrap_or_else(|| panic!("{DEFAULT_SCOPE_CARD_DROPPED}")),
772            }),
773            (None, None) => None,
774        }
775    }
776}
777
778#[cfg(feature = "export_golem_agentic")]
779impl std::fmt::Debug for __DefaultScopeCard {
780    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
781        let state = if self.0.is_some() { "set" } else { "unset" };
782        write!(f, "DefaultScopeCard(<{state}>)")
783    }
784}
785
786/// Starts an awaited agent invocation, lending `scope_card` to the callee for
787/// the invocation. Resolve the scope card with [`__DefaultScopeCard::resolve`]
788/// before encoding `input`; this call then cannot fail before the host takes
789/// ownership of `input`.
790#[doc(hidden)]
791#[cfg(feature = "export_golem_agentic")]
792pub fn __async_invoke_and_await(
793    rpc: &crate::golem_agentic::golem::agent::host::WasmRpc,
794    method_name: &str,
795    input: wire::SchemaValueTree,
796    scope_card: Option<&ScopeCard>,
797) -> crate::golem_agentic::golem::agent::host::AsyncInvocationWithMetadata {
798    rpc.async_invoke_and_await(method_name, input, scope_card.map(ScopeCard::raw))
799}
800
801/// Borrows the host handles of all `parents` at the same time.
802///
803/// Each persistent card's handle is behind its own lock, so a card that
804/// appears more than once is locked once and its handle is passed again.
805fn with_raw_parents<R>(
806    parents: &[ScopeParent<'_>],
807    f: impl FnOnce(&[&wire::PermissionCard]) -> R,
808) -> R {
809    type WithRawCards<'a, R> = Box<dyn FnOnce(&[&wire::PermissionCard]) -> R + 'a>;
810
811    fn go<R>(
812        parents: &[ScopeParent<'_>],
813        first_index: &HashMap<*const (), usize>,
814        acc: &[&wire::PermissionCard],
815        f: WithRawCards<'_, R>,
816    ) -> R {
817        let Some((parent, rest)) = parents.split_first() else {
818            return f(acc);
819        };
820        let handle = match parent {
821            ScopeParent::Scope(scope) => {
822                let mut next = acc.to_vec();
823                next.push(scope.raw.as_ref());
824                return go(rest, first_index, &next, f);
825            }
826            ScopeParent::Card(card) => &card.handle,
827            ScopeParent::Template(card) => &card.handle,
828        };
829        let index = first_index[&handle.cell_id()];
830        if index < acc.len() {
831            let mut next = acc.to_vec();
832            next.push(acc[index]);
833            return go(rest, first_index, &next, f);
834        }
835        with_raw_card(handle, |raw| {
836            let mut next = acc.to_vec();
837            next.push(raw);
838            go(rest, first_index, &next, f)
839        })
840    }
841
842    let mut first_index = HashMap::new();
843    for (index, parent) in parents.iter().enumerate() {
844        let handle = match parent {
845            ScopeParent::Card(card) => &card.handle,
846            ScopeParent::Template(card) => &card.handle,
847            ScopeParent::Scope(_) => continue,
848        };
849        first_index.entry(handle.cell_id()).or_insert(index);
850    }
851    go(parents, &first_index, &[], Box::new(f))
852}
853
854/// An opaque card identifier.
855///
856/// Card identifiers support only equality and hashing. Compare
857/// [`PermissionCard::id`] with [`PermissionCard::parents`] to follow the
858/// derivation graph, or to find a card in the [`wallet`]. Formatting and serde
859/// serialization show a redacted placeholder. A card identifier has no
860/// constructor and no schema or wire codec, and it never grants authority.
861#[derive(Clone, PartialEq, Eq, Hash)]
862pub struct CardId {
863    uuid: Uuid,
864}
865
866impl CardId {
867    fn from_raw(raw: wire::CardId) -> Self {
868        Self {
869            uuid: raw.uuid.into(),
870        }
871    }
872}
873
874const REDACTED_CARD_ID: &str = "<redacted card id>";
875
876impl std::fmt::Debug for CardId {
877    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
878        write!(f, "CardId({REDACTED_CARD_ID})")
879    }
880}
881
882impl std::fmt::Display for CardId {
883    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
884        f.write_str(REDACTED_CARD_ID)
885    }
886}
887
888#[cfg(feature = "export_golem_agentic")]
889impl crate::serde::Serialize for CardId {
890    fn serialize<S: crate::serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
891        serializer.serialize_str(REDACTED_CARD_ID)
892    }
893}
894
895/// A grant pattern: `class(owner) @ recipient : verb : resource-id`.
896#[derive(
897    Clone, Debug, PartialEq, Eq, Hash, IntoSchema, FromSchema, IntoWire, FromWire, WireSchema,
898)]
899#[schema(named = "golem.permissions.PatternGrant")]
900pub struct PatternGrant {
901    pub class: String,
902    pub owner: String,
903    pub recipient: String,
904    pub verb: String,
905    pub resource_id: String,
906}
907
908impl PatternGrant {
909    pub fn new(
910        class: impl Into<String>,
911        owner: impl Into<String>,
912        recipient: impl Into<String>,
913        verb: impl Into<String>,
914        resource_id: impl Into<String>,
915    ) -> Self {
916        Self {
917            class: class.into(),
918            owner: owner.into(),
919            recipient: recipient.into(),
920            verb: verb.into(),
921            resource_id: resource_id.into(),
922        }
923    }
924
925    fn to_raw(&self) -> raw::PatternGrant {
926        raw::PatternGrant {
927            class: self.class.clone(),
928            owner: self.owner.clone(),
929            recipient: self.recipient.clone(),
930            verb: self.verb.clone(),
931            resource_id: self.resource_id.clone(),
932        }
933    }
934}
935
936impl From<raw::PatternGrant> for PatternGrant {
937    fn from(value: raw::PatternGrant) -> Self {
938        Self {
939            class: value.class,
940            owner: value.owner,
941            recipient: value.recipient,
942            verb: value.verb,
943            resource_id: value.resource_id,
944        }
945    }
946}
947
948/// The four grant lists of a card, as returned by [`PermissionCard::inspect`].
949#[derive(
950    Clone, Debug, Default, PartialEq, Eq, IntoSchema, FromSchema, IntoWire, FromWire, WireSchema,
951)]
952#[schema(named = "golem.permissions.CardView")]
953pub struct CardView {
954    pub lower_positive: Vec<PatternGrant>,
955    pub lower_negative: Vec<PatternGrant>,
956    pub upper_positive: Vec<PatternGrant>,
957    pub upper_negative: Vec<PatternGrant>,
958}
959
960impl From<raw::CardView> for CardView {
961    fn from(value: raw::CardView) -> Self {
962        let convert =
963            |grants: Vec<raw::PatternGrant>| grants.into_iter().map(PatternGrant::from).collect();
964        Self {
965            lower_positive: convert(value.lower_positive),
966            lower_negative: convert(value.lower_negative),
967            upper_positive: convert(value.upper_positive),
968            upper_negative: convert(value.upper_negative),
969        }
970    }
971}
972
973/// The requested grant lists of a derivation.
974///
975/// For [`PermissionCard::derive`] the positive lists are the parent grants to
976/// retain and the negative lists are the denials to add. For
977/// [`derive_from_wallet`] and [`derive_scope`] the four lists are the full
978/// requested grant lists of the new card.
979#[derive(Clone, Debug, Default, PartialEq, Eq)]
980pub struct Bounds {
981    pub lower_positive: Vec<PatternGrant>,
982    pub lower_negative: Vec<PatternGrant>,
983    pub upper_positive: Vec<PatternGrant>,
984    pub upper_negative: Vec<PatternGrant>,
985}
986
987struct RawBounds {
988    lower_positive: Vec<raw::PatternGrant>,
989    lower_negative: Vec<raw::PatternGrant>,
990    upper_positive: Vec<raw::PatternGrant>,
991    upper_negative: Vec<raw::PatternGrant>,
992}
993
994impl From<&Bounds> for RawBounds {
995    fn from(value: &Bounds) -> Self {
996        let convert = |grants: &[PatternGrant]| grants.iter().map(PatternGrant::to_raw).collect();
997        Self {
998            lower_positive: convert(&value.lower_positive),
999            lower_negative: convert(&value.lower_negative),
1000            upper_positive: convert(&value.upper_positive),
1001            upper_negative: convert(&value.upper_negative),
1002        }
1003    }
1004}
1005
1006/// The lifespan of a derived card, as an ISO-8601 duration of the form
1007/// `PnDTnHnMnS`, for example `"P1DT2H"` or `"PT30.5S"`.
1008#[derive(Clone, Debug, PartialEq, Eq, Hash)]
1009pub struct Lifespan {
1010    iso_8601: String,
1011}
1012
1013impl Lifespan {
1014    /// A lifespan from an ISO-8601 duration string of the form `PnDTnHnMnS`.
1015    /// Years, months and weeks are rejected.
1016    pub fn iso8601(value: impl Into<String>) -> Result<Self, LifespanError> {
1017        let iso_8601 = value.into();
1018        if !iso_8601.starts_with('P') {
1019            return Err(LifespanError(format!(
1020                "ISO-8601 duration must start with 'P': {iso_8601:?}"
1021            )));
1022        }
1023        crate::schema::canonical::duration::from_text(&iso_8601)
1024            .map_err(|error| LifespanError(format!("{iso_8601:?}: {error}")))?;
1025        Ok(Self { iso_8601 })
1026    }
1027
1028    /// The ISO-8601 representation of this lifespan.
1029    pub fn as_iso8601(&self) -> &str {
1030        &self.iso_8601
1031    }
1032
1033    fn to_raw(&self) -> raw::Duration {
1034        raw::Duration {
1035            iso_8601: self.iso_8601.clone(),
1036        }
1037    }
1038}
1039
1040impl TryFrom<Duration> for Lifespan {
1041    type Error = LifespanError;
1042
1043    /// Rejects a zero duration and a duration that does not fit in `i64`
1044    /// nanoseconds.
1045    fn try_from(value: Duration) -> Result<Self, Self::Error> {
1046        if value.is_zero() {
1047            return Err(LifespanError("duration must be positive".to_string()));
1048        }
1049        let nanoseconds = i64::try_from(value.as_nanos()).map_err(|_| {
1050            LifespanError(format!("duration {value:?} exceeds the supported range"))
1051        })?;
1052        Ok(Self {
1053            iso_8601: crate::schema::canonical::duration::to_text(
1054                &crate::schema::DurationValuePayload { nanoseconds },
1055            ),
1056        })
1057    }
1058}
1059
1060/// A lifespan that cannot be represented.
1061#[derive(Clone, Debug, PartialEq, Eq)]
1062pub struct LifespanError(String);
1063
1064impl std::fmt::Display for LifespanError {
1065    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1066        write!(f, "invalid lifespan: {}", self.0)
1067    }
1068}
1069
1070impl std::error::Error for LifespanError {}
1071
1072/// A point in time, in seconds and nanoseconds since the Unix epoch.
1073#[derive(
1074    Clone,
1075    Copy,
1076    Debug,
1077    PartialEq,
1078    Eq,
1079    PartialOrd,
1080    Ord,
1081    Hash,
1082    IntoSchema,
1083    FromSchema,
1084    IntoWire,
1085    FromWire,
1086    WireSchema,
1087)]
1088#[schema(named = "golem.permissions.Timestamp")]
1089pub struct Timestamp {
1090    pub seconds: i64,
1091    pub nanoseconds: u32,
1092}
1093
1094impl From<raw::Timestamp> for Timestamp {
1095    fn from(value: raw::Timestamp) -> Self {
1096        Self {
1097            seconds: value.seconds,
1098            nanoseconds: value.nanoseconds,
1099        }
1100    }
1101}
1102
1103/// A wallet version token, as returned by [`wallet_version`].
1104#[derive(Clone, Debug, PartialEq, Eq, Hash)]
1105pub struct WalletVersionToken {
1106    /// The 32-byte BLAKE3 hash of the wallet holder.
1107    pub wallet_id_hash: Vec<u8>,
1108    /// Increases whenever the wallet changes.
1109    pub generation: u64,
1110}
1111
1112impl From<raw::WalletVersionToken> for WalletVersionToken {
1113    fn from(value: raw::WalletVersionToken) -> Self {
1114        Self {
1115            wallet_id_hash: value.wallet_id_hash,
1116            generation: value.generation,
1117        }
1118    }
1119}
1120
1121/// A resource class of the permission kernel, as returned by [`list_modules`].
1122#[derive(Clone, Debug, PartialEq, Eq, IntoSchema, FromSchema, IntoWire, FromWire, WireSchema)]
1123#[schema(named = "golem.permissions.KernelModule")]
1124pub struct KernelModule {
1125    pub class_name: String,
1126    pub verbs: Vec<String>,
1127}
1128
1129impl From<raw_kernel::KernelModule> for KernelModule {
1130    fn from(value: raw_kernel::KernelModule) -> Self {
1131        Self {
1132            class_name: value.class_name,
1133            verbs: value.verbs,
1134        }
1135    }
1136}
1137
1138/// The holder of a wallet: the target of [`PermissionCard::install`].
1139///
1140/// The host currently accepts only [`Holder::Agent`] as an install target.
1141#[derive(Clone, Debug, PartialEq, Eq, Hash)]
1142pub enum Holder {
1143    Account(Uuid),
1144    App(Uuid),
1145    Agent(AgentId),
1146}
1147
1148impl Holder {
1149    fn to_raw(&self) -> raw::Holder {
1150        match self {
1151            Self::Account(uuid) => raw::Holder::Account((*uuid).into()),
1152            Self::App(uuid) => raw::Holder::App((*uuid).into()),
1153            Self::Agent(agent_id) => {
1154                raw::Holder::Agent(crate::schema_agent_id_to_wire(agent_id.clone()))
1155            }
1156        }
1157    }
1158}
1159
1160/// An expected refusal of a permission operation by the host.
1161///
1162/// Each case carries the host's explanation.
1163#[derive(Clone, Debug, PartialEq, Eq, IntoSchema, FromSchema, IntoWire, FromWire, WireSchema)]
1164#[schema(named = "golem.permissions.PermissionError")]
1165pub enum PermissionError {
1166    NotPermitted(String),
1167    LowerBoundTooBroad(String),
1168    UpperBoundTooBroad(String),
1169    WalletMultiSourceRequired(String),
1170    CardRevoked(String),
1171    CardExpired(String),
1172    InstallRecipientMismatch(String),
1173    UnknownResourceClass(String),
1174    InvalidPattern(String),
1175    InvalidVerb(String),
1176    InvalidOwner(String),
1177    InvalidRecipient(String),
1178}
1179
1180impl PermissionError {
1181    /// The host's explanation of the refusal.
1182    pub fn message(&self) -> &str {
1183        match self {
1184            Self::NotPermitted(message)
1185            | Self::LowerBoundTooBroad(message)
1186            | Self::UpperBoundTooBroad(message)
1187            | Self::WalletMultiSourceRequired(message)
1188            | Self::CardRevoked(message)
1189            | Self::CardExpired(message)
1190            | Self::InstallRecipientMismatch(message)
1191            | Self::UnknownResourceClass(message)
1192            | Self::InvalidPattern(message)
1193            | Self::InvalidVerb(message)
1194            | Self::InvalidOwner(message)
1195            | Self::InvalidRecipient(message) => message,
1196        }
1197    }
1198
1199    fn kind(&self) -> &'static str {
1200        match self {
1201            Self::NotPermitted(_) => "not-permitted",
1202            Self::LowerBoundTooBroad(_) => "lower-bound-too-broad",
1203            Self::UpperBoundTooBroad(_) => "upper-bound-too-broad",
1204            Self::WalletMultiSourceRequired(_) => "wallet-multi-source-required",
1205            Self::CardRevoked(_) => "card-revoked",
1206            Self::CardExpired(_) => "card-expired",
1207            Self::InstallRecipientMismatch(_) => "install-recipient-mismatch",
1208            Self::UnknownResourceClass(_) => "unknown-resource-class",
1209            Self::InvalidPattern(_) => "invalid-pattern",
1210            Self::InvalidVerb(_) => "invalid-verb",
1211            Self::InvalidOwner(_) => "invalid-owner",
1212            Self::InvalidRecipient(_) => "invalid-recipient",
1213        }
1214    }
1215}
1216
1217impl From<raw::PermissionError> for PermissionError {
1218    fn from(value: raw::PermissionError) -> Self {
1219        match value {
1220            raw::PermissionError::NotPermitted(message) => Self::NotPermitted(message),
1221            raw::PermissionError::LowerBoundTooBroad(message) => Self::LowerBoundTooBroad(message),
1222            raw::PermissionError::UpperBoundTooBroad(message) => Self::UpperBoundTooBroad(message),
1223            raw::PermissionError::WalletMultiSourceRequired(message) => {
1224                Self::WalletMultiSourceRequired(message)
1225            }
1226            raw::PermissionError::CardRevoked(message) => Self::CardRevoked(message),
1227            raw::PermissionError::CardExpired(message) => Self::CardExpired(message),
1228            raw::PermissionError::InstallRecipientMismatch(message) => {
1229                Self::InstallRecipientMismatch(message)
1230            }
1231            raw::PermissionError::UnknownResourceClass(message) => {
1232                Self::UnknownResourceClass(message)
1233            }
1234            raw::PermissionError::InvalidPattern(message) => Self::InvalidPattern(message),
1235            raw::PermissionError::InvalidVerb(message) => Self::InvalidVerb(message),
1236            raw::PermissionError::InvalidOwner(message) => Self::InvalidOwner(message),
1237            raw::PermissionError::InvalidRecipient(message) => Self::InvalidRecipient(message),
1238        }
1239    }
1240}
1241
1242impl std::fmt::Display for PermissionError {
1243    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1244        write!(f, "{}: {}", self.kind(), self.message())
1245    }
1246}
1247
1248impl std::error::Error for PermissionError {}
1249
1250/// Implements the schema, wire and serde codecs of a card type. The two card
1251/// types share the handle codec and differ only in their declared
1252/// `polymorphic` flag.
1253macro_rules! card_codecs {
1254    ($card:ident, $type_id:literal, polymorphic: $polymorphic:literal) => {
1255        impl WireSchema for $card {
1256            fn wire_type_id() -> String {
1257                $type_id.to_string()
1258            }
1259
1260            fn append_schema(builder: &mut WireSchemaBuilder) -> i32 {
1261                builder.push(wire::SchemaTypeBody::PermissionCardType(
1262                    wire::PermissionCardSpec {
1263                        polymorphic: $polymorphic,
1264                    },
1265                ))
1266            }
1267        }
1268
1269        impl FromWire for $card {
1270            /// Takes the handle out of the decoded node. Decoding the same node
1271            /// again, for example from a second reader over a shared snapshot,
1272            /// fails.
1273            fn read_wire(reader: &mut WireReader, index: i32) -> Result<Self, WireError> {
1274                GuestPermissionCardHandle::read_wire(reader, index)?
1275                    .take()
1276                    .map(Self::from_raw)
1277                    .ok_or(WireError::ConsumedResource("permission-card"))
1278            }
1279        }
1280
1281        impl IntoWire for $card {
1282            fn preflight(&self, resources: &mut WirePreflight) -> Result<(), WireError> {
1283                self.handle.preflight(resources)
1284            }
1285
1286            fn write_wire(&self, writer: &mut WireWriter) -> Result<i32, WireError> {
1287                self.handle.write_wire(writer)
1288            }
1289        }
1290
1291        #[cfg(feature = "export_golem_agentic")]
1292        impl IntoSchema for $card {
1293            fn type_id() -> TypeId {
1294                TypeId::new($type_id)
1295            }
1296
1297            fn register_in(_builder: &mut SchemaBuilder) -> SchemaType {
1298                SchemaType::permission_card(PermissionCardSpec {
1299                    polymorphic: $polymorphic,
1300                })
1301            }
1302
1303            /// Lowers the card into a schema value that shares its handle.
1304            ///
1305            /// The handle moves out of the card only when the schema value is
1306            /// encoded into a WIT `schema-value-tree`.
1307            fn to_value(&self) -> SchemaValue {
1308                SchemaValue::PermissionCard(self.handle.clone())
1309            }
1310        }
1311
1312        #[cfg(feature = "export_golem_agentic")]
1313        impl FromSchema for $card {
1314            /// Takes the handle out of `value`. Decoding the same value again
1315            /// fails.
1316            fn from_value(value: &SchemaValue) -> Result<Self, FromSchemaError> {
1317                match value {
1318                    SchemaValue::PermissionCard(handle) => {
1319                        handle.take().map(Self::from_raw).ok_or_else(|| {
1320                            FromSchemaError::custom(
1321                                "permission card has already been moved out of this value",
1322                            )
1323                        })
1324                    }
1325                    other => Err(FromSchemaError::shape_mismatch(
1326                        "permission-card",
1327                        format!("{other:?}"),
1328                        stringify!($card),
1329                    )),
1330                }
1331            }
1332        }
1333
1334        #[cfg(feature = "export_golem_agentic")]
1335        impl crate::serde::Serialize for $card {
1336            fn serialize<S: crate::serde::Serializer>(
1337                &self,
1338                _serializer: S,
1339            ) -> Result<S::Ok, S::Error> {
1340                Err(crate::serde::ser::Error::custom(
1341                    "permission cards cannot be serialized; pass them as agent method arguments or results",
1342                ))
1343            }
1344        }
1345
1346        #[cfg(feature = "export_golem_agentic")]
1347        impl<'de> crate::serde::Deserialize<'de> for $card {
1348            fn deserialize<D: crate::serde::Deserializer<'de>>(
1349                _deserializer: D,
1350            ) -> Result<Self, D::Error> {
1351                Err(crate::serde::de::Error::custom(
1352                    "permission cards cannot be deserialized or forged from data",
1353                ))
1354            }
1355        }
1356    };
1357}
1358
1359card_codecs!(PermissionCard, "golem.core.PermissionCard", polymorphic: false);
1360card_codecs!(
1361    PolymorphicPermissionCard,
1362    "golem.core.PolymorphicPermissionCard",
1363    polymorphic: true
1364);
1365
1366#[cfg(feature = "export_golem_agentic")]
1367impl crate::serde::Serialize for ScopeCard {
1368    fn serialize<S: crate::serde::Serializer>(&self, _serializer: S) -> Result<S::Ok, S::Error> {
1369        Err(crate::serde::ser::Error::custom(
1370            "scope cards cannot be serialized; they are valid only during the invocation that derived them",
1371        ))
1372    }
1373}
1374
1375#[cfg(test)]
1376mod tests {
1377    use super::*;
1378    use crate::schema::wit::direct;
1379    use test_r::test;
1380
1381    fn fake_card(handle: u32) -> PermissionCard {
1382        PermissionCard::from_raw(unsafe { wire::PermissionCard::from_handle(handle) })
1383    }
1384
1385    /// Releases a fake handle without calling the host's resource drop.
1386    fn forget(card: PermissionCard) -> Option<u32> {
1387        card.handle.take().map(|raw| raw.take_handle())
1388    }
1389
1390    fn fake_template(handle: u32) -> PolymorphicPermissionCard {
1391        PolymorphicPermissionCard::from_raw(unsafe { wire::PermissionCard::from_handle(handle) })
1392    }
1393
1394    fn forget_template(card: PolymorphicPermissionCard) -> Option<u32> {
1395        card.handle.take().map(|raw| raw.take_handle())
1396    }
1397
1398    fn fake_scope(handle: u32) -> ScopeCard {
1399        ScopeCard {
1400            raw: Arc::new(unsafe { wire::PermissionCard::from_handle(handle) }),
1401        }
1402    }
1403
1404    /// Releases the only reference to a fake scope handle without calling the
1405    /// host's resource drop.
1406    fn forget_scope(scope: ScopeCard) -> u32 {
1407        Arc::try_unwrap(scope.raw)
1408            .unwrap_or_else(|_| panic!("scope handle is still shared"))
1409            .take_handle()
1410    }
1411
1412    /// Drops a scope card whose handle is still shared with another scope
1413    /// card, and returns the handle number.
1414    #[cfg(feature = "export_golem_agentic")]
1415    fn forget_scope_shared_handle(scope: ScopeCard) -> u32 {
1416        assert!(Arc::strong_count(&scope.raw) > 1);
1417        scope.raw.handle()
1418    }
1419
1420    fn panic_message(f: impl FnOnce() + std::panic::UnwindSafe) -> String {
1421        let payload = std::panic::catch_unwind(f).expect_err("operation must panic");
1422        payload
1423            .downcast_ref::<String>()
1424            .cloned()
1425            .or_else(|| payload.downcast_ref::<&str>().map(|s| s.to_string()))
1426            .unwrap_or_default()
1427    }
1428
1429    #[test]
1430    fn permission_error_covers_every_wit_case() {
1431        let cases = [
1432            (
1433                raw::PermissionError::NotPermitted("a".into()),
1434                PermissionError::NotPermitted("a".into()),
1435                "not-permitted",
1436            ),
1437            (
1438                raw::PermissionError::LowerBoundTooBroad("b".into()),
1439                PermissionError::LowerBoundTooBroad("b".into()),
1440                "lower-bound-too-broad",
1441            ),
1442            (
1443                raw::PermissionError::UpperBoundTooBroad("c".into()),
1444                PermissionError::UpperBoundTooBroad("c".into()),
1445                "upper-bound-too-broad",
1446            ),
1447            (
1448                raw::PermissionError::WalletMultiSourceRequired("d".into()),
1449                PermissionError::WalletMultiSourceRequired("d".into()),
1450                "wallet-multi-source-required",
1451            ),
1452            (
1453                raw::PermissionError::CardRevoked("e".into()),
1454                PermissionError::CardRevoked("e".into()),
1455                "card-revoked",
1456            ),
1457            (
1458                raw::PermissionError::CardExpired("f".into()),
1459                PermissionError::CardExpired("f".into()),
1460                "card-expired",
1461            ),
1462            (
1463                raw::PermissionError::InstallRecipientMismatch("g".into()),
1464                PermissionError::InstallRecipientMismatch("g".into()),
1465                "install-recipient-mismatch",
1466            ),
1467            (
1468                raw::PermissionError::UnknownResourceClass("h".into()),
1469                PermissionError::UnknownResourceClass("h".into()),
1470                "unknown-resource-class",
1471            ),
1472            (
1473                raw::PermissionError::InvalidPattern("i".into()),
1474                PermissionError::InvalidPattern("i".into()),
1475                "invalid-pattern",
1476            ),
1477            (
1478                raw::PermissionError::InvalidVerb("j".into()),
1479                PermissionError::InvalidVerb("j".into()),
1480                "invalid-verb",
1481            ),
1482            (
1483                raw::PermissionError::InvalidOwner("k".into()),
1484                PermissionError::InvalidOwner("k".into()),
1485                "invalid-owner",
1486            ),
1487            (
1488                raw::PermissionError::InvalidRecipient("l".into()),
1489                PermissionError::InvalidRecipient("l".into()),
1490                "invalid-recipient",
1491            ),
1492        ];
1493        // The `golem:permissions/types.permission-error` variant has exactly
1494        // this many cases; a WIT change must update this test.
1495        assert_eq!(cases.len(), 12);
1496        let mut kinds = std::collections::HashSet::new();
1497        for (raw_error, expected, kind) in cases {
1498            let message = expected.message().to_string();
1499            let converted = PermissionError::from(raw_error);
1500            assert_eq!(converted, expected);
1501            assert_eq!(converted.to_string(), format!("{kind}: {message}"));
1502            assert!(kinds.insert(kind));
1503        }
1504    }
1505
1506    #[test]
1507    fn lifespan_from_iso8601_accepts_only_days_and_time_units() {
1508        for value in ["PT1H", "P1DT2H", "PT30.5S", "P2D"] {
1509            assert_eq!(Lifespan::iso8601(value).unwrap().as_iso8601(), value);
1510        }
1511        for value in ["P1Y", "P1M", "P1W", "PT", "1h", "5s", "", "-PT1S"] {
1512            assert!(
1513                Lifespan::iso8601(value).is_err(),
1514                "{value} must be rejected"
1515            );
1516        }
1517    }
1518
1519    #[test]
1520    fn lifespan_from_duration_is_a_fixed_iso8601_duration() {
1521        let lifespan = |duration| Lifespan::try_from(duration).unwrap();
1522        assert!(Lifespan::try_from(Duration::ZERO).is_err());
1523        assert_eq!(
1524            lifespan(Duration::from_secs(90_061)).as_iso8601(),
1525            "P1DT1H1M1S"
1526        );
1527        assert_eq!(
1528            lifespan(Duration::from_millis(2_500)).as_iso8601(),
1529            "PT2.5S"
1530        );
1531        assert_eq!(
1532            lifespan(Duration::from_nanos(1)).as_iso8601(),
1533            "PT0.000000001S"
1534        );
1535        assert!(Lifespan::try_from(Duration::from_secs(u64::MAX)).is_err());
1536    }
1537
1538    #[test]
1539    fn moved_card_fails_loudly() {
1540        let card = fake_card(7);
1541        let alias = PermissionCard {
1542            handle: card.handle.clone(),
1543        };
1544        assert_eq!(forget(card), Some(7));
1545
1546        assert_eq!(format!("{alias:?}"), "PermissionCard(<moved>)");
1547        let message = panic_message(std::panic::AssertUnwindSafe(|| {
1548            let _ = alias.id();
1549        }));
1550        assert!(message.contains("already been moved"), "{message}");
1551        let message = panic_message(std::panic::AssertUnwindSafe(|| {
1552            let _ = alias.inspect();
1553        }));
1554        assert!(message.contains("already been moved"), "{message}");
1555        let message = panic_message(std::panic::AssertUnwindSafe(|| {
1556            let _ = alias.derive(&Bounds::default(), None);
1557        }));
1558        assert!(message.contains("already been moved"), "{message}");
1559        let message = panic_message(std::panic::AssertUnwindSafe(|| {
1560            let _ = derive_scope(&[(&alias).into()], &Bounds::default());
1561        }));
1562        assert!(message.contains("already been moved"), "{message}");
1563
1564        let revoked = PermissionCard {
1565            handle: alias.handle.clone(),
1566        };
1567        let message = panic_message(std::panic::AssertUnwindSafe(move || {
1568            let _ = revoked.revoke();
1569        }));
1570        assert!(message.contains("already been moved"), "{message}");
1571        let installed = PermissionCard {
1572            handle: alias.handle.clone(),
1573        };
1574        let message = panic_message(std::panic::AssertUnwindSafe(move || {
1575            let _ = installed.install(&Holder::Account(Uuid::nil()));
1576        }));
1577        assert!(message.contains("already been moved"), "{message}");
1578    }
1579
1580    #[test]
1581    fn transferred_card_is_moved_and_cannot_be_encoded_twice() {
1582        let card = fake_card(11);
1583        let tree = direct::encode(&card).unwrap();
1584        assert_eq!(format!("{card:?}"), "PermissionCard(<moved>)");
1585        assert!(matches!(
1586            direct::encode(&card),
1587            Err(WireError::ConsumedResource(_))
1588        ));
1589
1590        let received = direct::decode::<PermissionCard>(tree).unwrap();
1591        assert_eq!(format!("{received:?}"), "PermissionCard(<present>)");
1592        assert_eq!(forget(received), Some(11));
1593    }
1594
1595    #[test]
1596    fn aliased_card_is_rejected_before_any_handle_moves() {
1597        let card = fake_card(13);
1598        let alias = PermissionCard {
1599            handle: card.handle.clone(),
1600        };
1601        let pair = (card, alias);
1602        assert!(matches!(
1603            direct::encode(&pair),
1604            Err(WireError::AliasedResource(_))
1605        ));
1606        assert_eq!(format!("{:?}", pair.1), "PermissionCard(<present>)");
1607        assert_eq!(forget(pair.0), Some(13));
1608    }
1609
1610    #[cfg(feature = "export_golem_agentic")]
1611    #[test]
1612    fn card_cannot_be_serialized_or_deserialized() {
1613        let card = fake_card(17);
1614        let error = crate::serde_json::to_string(&card).unwrap_err().to_string();
1615        assert!(error.contains("cannot be serialized"), "{error}");
1616        let error = crate::serde_json::from_str::<PermissionCard>("17")
1617            .unwrap_err()
1618            .to_string();
1619        assert!(error.contains("cannot be deserialized"), "{error}");
1620        assert_eq!(forget(card), Some(17));
1621    }
1622
1623    #[cfg(feature = "export_golem_agentic")]
1624    #[test]
1625    fn scope_card_cannot_be_serialized() {
1626        let scope = fake_scope(18);
1627        let error = crate::serde_json::to_string(&scope)
1628            .unwrap_err()
1629            .to_string();
1630        assert!(error.contains("cannot be serialized"), "{error}");
1631        assert_eq!(format!("{scope:?}"), "ScopeCard(..)");
1632        assert_eq!(forget_scope(scope), 18);
1633    }
1634
1635    #[cfg(feature = "export_golem_agentic")]
1636    #[test]
1637    fn decoding_a_card_takes_its_handle() {
1638        let card = fake_card(23);
1639        let value = card.to_value();
1640        let decoded = PermissionCard::from_value(&value).unwrap();
1641        assert_eq!(format!("{card:?}"), "PermissionCard(<moved>)");
1642        assert!(PermissionCard::from_value(&value).is_err());
1643        assert!(PermissionCard::from_value(&SchemaValue::Bool(true)).is_err());
1644        assert_eq!(forget(decoded), Some(23));
1645    }
1646
1647    #[cfg(feature = "export_golem_agentic")]
1648    #[test]
1649    fn default_scope_card_is_borrowed_and_per_call_card_wins() {
1650        let default_scope = fake_scope(19);
1651        let per_call = fake_scope(20);
1652        let mut default = __DefaultScopeCard::default();
1653        assert!(default.resolve(None).is_none());
1654        default.set(&default_scope);
1655        let handle = |card: Option<ScopeCard>| card.map(forget_scope_shared_handle);
1656        assert_eq!(handle(default.resolve(None)), Some(19));
1657        assert_eq!(handle(default.resolve(Some(&per_call))), Some(20));
1658
1659        // The default does not keep the scope card alive.
1660        assert_eq!(forget_scope(default_scope), 19);
1661        let message = panic_message(std::panic::AssertUnwindSafe(|| {
1662            let _ = default.resolve(None);
1663        }));
1664        assert!(
1665            message.contains("default scope card was dropped"),
1666            "{message}"
1667        );
1668        assert_eq!(handle(default.resolve(Some(&per_call))), Some(20));
1669
1670        default.clear();
1671        assert!(default.resolve(None).is_none());
1672        assert_eq!(forget_scope(per_call), 20);
1673    }
1674
1675    #[test]
1676    fn scope_parents_are_each_locked_once() {
1677        let first = fake_card(21);
1678        let second = fake_card(22);
1679        let template = fake_template(26);
1680        let scope = fake_scope(24);
1681        let handles = with_raw_parents(
1682            &[
1683                (&first).into(),
1684                (&template).into(),
1685                (&scope).into(),
1686                (&second).into(),
1687                (&template).into(),
1688                (&first).into(),
1689            ],
1690            |raw| raw.iter().map(|card| card.handle()).collect::<Vec<_>>(),
1691        );
1692        assert_eq!(handles, vec![21, 26, 24, 22, 26, 21]);
1693        assert_eq!(forget(first), Some(21));
1694        assert_eq!(forget(second), Some(22));
1695        assert_eq!(forget_template(template), Some(26));
1696        assert_eq!(forget_scope(scope), 24);
1697    }
1698
1699    #[test]
1700    fn moved_template_card_fails_loudly() {
1701        let card = fake_template(31);
1702        let alias = PolymorphicPermissionCard {
1703            handle: card.handle.clone(),
1704        };
1705        assert_eq!(forget_template(card), Some(31));
1706
1707        assert_eq!(format!("{alias:?}"), "PolymorphicPermissionCard(<moved>)");
1708        for operation in [
1709            Box::new(|card: &PolymorphicPermissionCard| {
1710                let _ = card.id();
1711            }) as Box<dyn Fn(&PolymorphicPermissionCard)>,
1712            Box::new(|card| {
1713                let _ = card.parents();
1714            }),
1715            Box::new(|card| {
1716                let _ = card.expires_at();
1717            }),
1718            Box::new(|card| {
1719                let _ = card.inspect();
1720            }),
1721            Box::new(|card| {
1722                let _ = card.derive(&Bounds::default(), None);
1723            }),
1724            Box::new(|card| {
1725                let _ = derive_scope(&[card.into()], &Bounds::default());
1726            }),
1727        ] {
1728            let message = panic_message(std::panic::AssertUnwindSafe(|| operation(&alias)));
1729            assert!(message.contains("already been moved"), "{message}");
1730        }
1731
1732        let revoked = PolymorphicPermissionCard {
1733            handle: alias.handle.clone(),
1734        };
1735        let message = panic_message(std::panic::AssertUnwindSafe(move || {
1736            let _ = revoked.revoke();
1737        }));
1738        assert!(message.contains("already been moved"), "{message}");
1739        let installed = PolymorphicPermissionCard {
1740            handle: alias.handle.clone(),
1741        };
1742        let message = panic_message(std::panic::AssertUnwindSafe(move || {
1743            let _ = installed.install(&Holder::Account(Uuid::nil()));
1744        }));
1745        assert!(message.contains("already been moved"), "{message}");
1746    }
1747
1748    #[test]
1749    fn transferred_template_card_is_moved_and_cannot_be_encoded_twice() {
1750        let card = fake_template(32);
1751        let tree = direct::encode(&card).unwrap();
1752        assert_eq!(format!("{card:?}"), "PolymorphicPermissionCard(<moved>)");
1753        assert!(matches!(
1754            direct::encode(&card),
1755            Err(WireError::ConsumedResource(_))
1756        ));
1757
1758        let received = direct::decode::<PolymorphicPermissionCard>(tree).unwrap();
1759        assert_eq!(
1760            format!("{received:?}"),
1761            "PolymorphicPermissionCard(<present>)"
1762        );
1763        assert_eq!(forget_template(received), Some(32));
1764    }
1765
1766    #[test]
1767    fn aliased_template_card_is_rejected_before_any_handle_moves() {
1768        let card = fake_template(33);
1769        let alias = PolymorphicPermissionCard {
1770            handle: card.handle.clone(),
1771        };
1772        let other = fake_card(34);
1773        let triple = (other, card, alias);
1774        assert!(matches!(
1775            direct::encode(&triple),
1776            Err(WireError::AliasedResource(_))
1777        ));
1778        assert_eq!(format!("{:?}", triple.0), "PermissionCard(<present>)");
1779        assert_eq!(
1780            format!("{:?}", triple.2),
1781            "PolymorphicPermissionCard(<present>)"
1782        );
1783        assert_eq!(forget(triple.0), Some(34));
1784        assert_eq!(forget_template(triple.1), Some(33));
1785    }
1786
1787    #[test]
1788    fn decoding_a_shared_wire_snapshot_twice_does_not_alias_the_template_card() {
1789        let card = fake_template(35);
1790        let tree = direct::encode(&card).unwrap();
1791        let snapshot = std::rc::Rc::new(direct::WireSnapshot::new(tree.value_nodes));
1792        let first =
1793            PolymorphicPermissionCard::read_wire(&mut snapshot.reader(), tree.root).unwrap();
1794        assert!(matches!(
1795            PolymorphicPermissionCard::read_wire(&mut snapshot.reader(), tree.root),
1796            Err(WireError::ConsumedResource(_))
1797        ));
1798        assert_eq!(forget_template(first), Some(35));
1799    }
1800
1801    #[test]
1802    fn card_types_declare_their_polymorphic_flag_on_the_wire() {
1803        let polymorphic =
1804            |graph: wire::SchemaGraph| match &graph.type_nodes[graph.root as usize].body {
1805                wire::SchemaTypeBody::PermissionCardType(spec) => spec.polymorphic,
1806                other => panic!("expected a permission-card type, found {other:?}"),
1807            };
1808        assert!(!polymorphic(direct::schema::<PermissionCard>()));
1809        assert!(polymorphic(direct::schema::<PolymorphicPermissionCard>()));
1810    }
1811
1812    #[cfg(feature = "export_golem_agentic")]
1813    #[test]
1814    fn template_card_cannot_be_serialized_or_deserialized() {
1815        let card = fake_template(36);
1816        let error = crate::serde_json::to_string(&card).unwrap_err().to_string();
1817        assert!(error.contains("cannot be serialized"), "{error}");
1818        let error = crate::serde_json::from_str::<PolymorphicPermissionCard>("36")
1819            .unwrap_err()
1820            .to_string();
1821        assert!(error.contains("cannot be deserialized"), "{error}");
1822        assert_eq!(forget_template(card), Some(36));
1823    }
1824
1825    #[cfg(feature = "export_golem_agentic")]
1826    #[test]
1827    fn decoding_a_template_card_takes_its_handle() {
1828        let card = fake_template(37);
1829        let value = card.to_value();
1830        let decoded = PolymorphicPermissionCard::from_value(&value).unwrap();
1831        assert_eq!(format!("{card:?}"), "PolymorphicPermissionCard(<moved>)");
1832        assert!(PolymorphicPermissionCard::from_value(&value).is_err());
1833        assert!(PolymorphicPermissionCard::from_value(&SchemaValue::Bool(true)).is_err());
1834        assert_eq!(forget_template(decoded), Some(37));
1835    }
1836
1837    #[cfg(feature = "export_golem_agentic")]
1838    #[test]
1839    fn card_types_declare_their_polymorphic_flag_in_the_schema() {
1840        #[derive(IntoSchema)]
1841        struct Cards {
1842            concrete: PermissionCard,
1843            template: PolymorphicPermissionCard,
1844        }
1845
1846        let schema = crate::schema::try_into_schema_graph::<Cards>().unwrap();
1847        let resolve = |ty| schema.resolve_ref(ty).unwrap();
1848        let SchemaType::Record { fields, .. } = resolve(&schema.root) else {
1849            panic!("expected a record");
1850        };
1851        let flags = fields
1852            .iter()
1853            .map(|field| match resolve(&field.body) {
1854                SchemaType::PermissionCard { spec, .. } => spec.polymorphic,
1855                other => panic!("expected a permission-card type, found {other:?}"),
1856            })
1857            .collect::<Vec<_>>();
1858        assert_eq!(flags, vec![false, true]);
1859    }
1860
1861    #[test]
1862    fn wallet_cards_keep_the_kind_the_host_reported() {
1863        let raw_card = |handle| unsafe { wire::PermissionCard::from_handle(handle) };
1864        let cards = [
1865            raw::WalletCard::Concrete(raw_card(41)),
1866            raw::WalletCard::Template(raw_card(42)),
1867            raw::WalletCard::Scope(raw_card(43)),
1868        ]
1869        .into_iter()
1870        .map(WalletCard::from_raw)
1871        .collect::<Vec<_>>();
1872        let handles = cards
1873            .into_iter()
1874            .map(|card| match card {
1875                WalletCard::Concrete(card) => ("concrete", forget(card).unwrap()),
1876                WalletCard::Template(card) => ("template", forget_template(card).unwrap()),
1877                WalletCard::Scope(card) => ("scope", forget_scope(card)),
1878            })
1879            .collect::<Vec<_>>();
1880        assert_eq!(
1881            handles,
1882            vec![("concrete", 41), ("template", 42), ("scope", 43)]
1883        );
1884    }
1885
1886    #[test]
1887    fn card_id_is_opaque_but_comparable() {
1888        let raw_id = || wire::CardId {
1889            uuid: Uuid::from_u128(42).into(),
1890        };
1891        let id = CardId::from_raw(raw_id());
1892        assert_eq!(id, CardId::from_raw(raw_id()));
1893        assert_ne!(
1894            id,
1895            CardId::from_raw(wire::CardId {
1896                uuid: Uuid::from_u128(43).into()
1897            })
1898        );
1899        assert_eq!(format!("{id:?}"), "CardId(<redacted card id>)");
1900        assert_eq!(format!("{id:#?}"), "CardId(<redacted card id>)");
1901        assert_eq!(id.to_string(), "<redacted card id>");
1902        #[cfg(feature = "export_golem_agentic")]
1903        assert_eq!(
1904            crate::serde_json::to_string(&vec![id.clone()]).unwrap(),
1905            r#"["<redacted card id>"]"#
1906        );
1907        assert_eq!(id.uuid, Uuid::from_u128(42));
1908    }
1909
1910    #[test]
1911    fn decoding_a_shared_wire_snapshot_twice_does_not_alias_the_card() {
1912        let card = fake_card(25);
1913        let tree = direct::encode(&card).unwrap();
1914        let snapshot = std::rc::Rc::new(direct::WireSnapshot::new(tree.value_nodes));
1915        let first = PermissionCard::read_wire(&mut snapshot.reader(), tree.root).unwrap();
1916        assert!(matches!(
1917            PermissionCard::read_wire(&mut snapshot.reader(), tree.root),
1918            Err(WireError::ConsumedResource(_))
1919        ));
1920        assert_eq!(forget(first), Some(25));
1921    }
1922}