Skip to main content

systemprompt_cli/
descriptor.rs

1//! Per-command bootstrap descriptor: which of profile/secrets/paths a command
2//! needs.
3//!
4//! Copyright (c) systemprompt.io — Business Source License 1.1.
5//! See <https://systemprompt.io> for licensing details.
6
7/// Bootstrap flags for one command.
8///
9/// Model discovery (installing the services registry through boot-time
10/// discovery) is set only for the server; every other command loads the YAML
11/// catalog as authored and must not reach for the network.
12///
13/// `is_destructive` is never set by hand: it is projected from the command's
14/// exhaustive [`DataImpact`] classification, so a command cannot be added
15/// without deciding whether it destroys data.
16#[derive(Debug, Clone, Copy, Default)]
17pub struct CommandDescriptor {
18    flags: u16,
19}
20
21impl CommandDescriptor {
22    const FLAG_PROFILE: u16 = 0b0000_0000_0001;
23    const FLAG_SECRETS: u16 = 0b0000_0000_0010;
24    const FLAG_PATHS: u16 = 0b0000_0000_0100;
25    const FLAG_DATABASE: u16 = 0b0000_0000_1000;
26    const FLAG_REMOTE_ELIGIBLE: u16 = 0b0000_0001_0000;
27    const FLAG_SKIP_VALIDATION: u16 = 0b0000_0010_0000;
28    const FLAG_READ_ONLY: u16 = 0b0000_0100_0000;
29    const FLAG_MODEL_DISCOVERY: u16 = 0b0000_1000_0000;
30    const FLAG_DESTRUCTIVE: u16 = 0b0001_0000_0000;
31
32    pub const NONE: Self = Self { flags: 0 };
33
34    pub const PROFILE_ONLY: Self = Self {
35        flags: Self::FLAG_PROFILE,
36    };
37
38    pub const PROFILE_AND_SECRETS: Self = Self {
39        flags: Self::FLAG_PROFILE | Self::FLAG_SECRETS,
40    };
41
42    pub const PROFILE_SECRETS_AND_PATHS: Self = Self {
43        flags: Self::FLAG_PROFILE | Self::FLAG_SECRETS | Self::FLAG_PATHS,
44    };
45
46    pub const FULL: Self = Self {
47        flags: Self::FLAG_PROFILE
48            | Self::FLAG_SECRETS
49            | Self::FLAG_PATHS
50            | Self::FLAG_DATABASE
51            | Self::FLAG_REMOTE_ELIGIBLE,
52    };
53
54    pub const fn profile(&self) -> bool {
55        self.flags & Self::FLAG_PROFILE != 0
56    }
57
58    pub const fn secrets(&self) -> bool {
59        self.flags & Self::FLAG_SECRETS != 0
60    }
61
62    pub const fn paths(&self) -> bool {
63        self.flags & Self::FLAG_PATHS != 0
64    }
65
66    pub const fn database(&self) -> bool {
67        self.flags & Self::FLAG_DATABASE != 0
68    }
69
70    pub const fn routing_class(&self) -> RoutingClass {
71        if self.flags & Self::FLAG_REMOTE_ELIGIBLE == 0 {
72            RoutingClass::LocalOnly
73        } else if self.flags & Self::FLAG_READ_ONLY != 0 {
74            RoutingClass::ReadOnly
75        } else {
76            RoutingClass::Mutating
77        }
78    }
79
80    pub const fn skip_validation(&self) -> bool {
81        self.flags & Self::FLAG_SKIP_VALIDATION != 0
82    }
83
84    pub const fn discovers_models(&self) -> bool {
85        self.flags & Self::FLAG_MODEL_DISCOVERY != 0
86    }
87
88    pub const fn with_remote_eligible(self) -> Self {
89        Self {
90            flags: self.flags | Self::FLAG_REMOTE_ELIGIBLE,
91        }
92    }
93
94    pub const fn with_read_only(self) -> Self {
95        Self {
96            flags: self.flags | Self::FLAG_READ_ONLY,
97        }
98    }
99
100    pub const fn with_skip_validation(self) -> Self {
101        Self {
102            flags: self.flags | Self::FLAG_SKIP_VALIDATION,
103        }
104    }
105
106    pub const fn with_model_discovery(self) -> Self {
107        Self {
108            flags: self.flags | Self::FLAG_MODEL_DISCOVERY,
109        }
110    }
111
112    pub const fn is_destructive(&self) -> bool {
113        self.flags & Self::FLAG_DESTRUCTIVE != 0
114    }
115
116    pub const fn data_impact(&self) -> DataImpact {
117        if self.is_destructive() {
118            DataImpact::Destructive
119        } else {
120            DataImpact::Preserving
121        }
122    }
123
124    pub const fn with_data_impact(self, impact: DataImpact) -> Self {
125        match impact {
126            DataImpact::Destructive => Self {
127                flags: self.flags | Self::FLAG_DESTRUCTIVE,
128            },
129            DataImpact::Preserving => self,
130        }
131    }
132}
133
134/// How a command treats the data behind the profile it resolves.
135///
136/// `Destructive` commands delete rows, rewrite schema, run jobs or change
137/// privilege. They refuse a cloud profile that arrived implicitly (stored
138/// session or directory discovery), and a failed remote route never falls
139/// back to direct database access for them. Every command enum classifies
140/// its variants in an exhaustive `match` with no wildcard arm.
141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
142pub enum DataImpact {
143    Preserving,
144    Destructive,
145}
146
147/// What a command is allowed to do when the active profile is a cloud profile.
148///
149/// `LocalOnly` is never routed remotely and runs against whatever the profile
150/// resolves; `ReadOnly` prefers remote when a session is available and falls
151/// back to local with a warning; `Mutating` must route remotely or fail loudly.
152#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153pub enum RoutingClass {
154    LocalOnly,
155    ReadOnly,
156    Mutating,
157}
158
159pub trait DescribeCommand {
160    fn descriptor(&self) -> CommandDescriptor;
161}