systemprompt_cli/commands/admin/config/
governance.rs1use anyhow::{Result, bail};
8use clap::{Args, Subcommand};
9use systemprompt_config::ProfileBootstrap;
10use systemprompt_models::profile::{
11 AuthzConfig, AuthzHookConfig, AuthzMode, GovernanceConfig, UNRESTRICTED_ACKNOWLEDGEMENT,
12};
13
14use super::profile_io::{load_profile, save_profile};
15use super::types::ConfigMutationOutput;
16use crate::CliConfig;
17use crate::shared::{CommandOutput, render_result};
18
19#[derive(Debug, Subcommand)]
20pub enum GovernanceCommands {
21 #[command(about = "Show governance configuration")]
22 Show,
23
24 #[command(about = "Set the authorization hook")]
25 Set(SetArgs),
26}
27
28#[derive(Debug, Clone, Args)]
29pub struct SetArgs {
30 #[arg(
31 long,
32 help = "Authz mode: webhook | extension | disabled | unrestricted"
33 )]
34 pub mode: String,
35
36 #[arg(long, help = "Webhook URL (required for mode=webhook)")]
37 pub url: Option<String>,
38
39 #[arg(long, help = "Webhook timeout in milliseconds")]
40 pub timeout_ms: Option<u64>,
41
42 #[arg(
43 long,
44 help = "Acknowledgement sentence (required for mode=unrestricted)"
45 )]
46 pub acknowledgement: Option<String>,
47}
48
49pub fn execute(command: &GovernanceCommands, _config: &CliConfig) -> Result<()> {
50 match command {
51 GovernanceCommands::Show => execute_show(),
52 GovernanceCommands::Set(args) => execute_set(args),
53 }
54}
55
56fn parse_mode(raw: &str) -> Result<AuthzMode> {
57 match raw.to_lowercase().as_str() {
58 "webhook" => Ok(AuthzMode::Webhook),
59 "extension" => Ok(AuthzMode::Extension),
60 "disabled" => Ok(AuthzMode::Disabled),
61 "unrestricted" => Ok(AuthzMode::Unrestricted),
62 other => bail!("unknown authz mode '{other}' (webhook|extension|disabled|unrestricted)"),
63 }
64}
65
66fn execute_set(args: &SetArgs) -> Result<()> {
67 let mode = parse_mode(&args.mode)?;
68
69 if matches!(mode, AuthzMode::Webhook) && args.url.is_none() {
70 bail!("mode=webhook requires --url");
71 }
72 if matches!(mode, AuthzMode::Unrestricted)
73 && args.acknowledgement.as_deref() != Some(UNRESTRICTED_ACKNOWLEDGEMENT)
74 {
75 bail!(
76 "mode=unrestricted requires --acknowledgement \"{}\"",
77 UNRESTRICTED_ACKNOWLEDGEMENT
78 );
79 }
80
81 let profile_path = ProfileBootstrap::get_path()?;
82 let mut profile = load_profile(profile_path)?;
83
84 profile.governance = Some(GovernanceConfig {
85 authz: Some(AuthzConfig {
86 hook: AuthzHookConfig {
87 mode,
88 url: args.url.clone(),
89 timeout_ms: args.timeout_ms.unwrap_or(500),
90 acknowledgement: args.acknowledgement.clone(),
91 },
92 }),
93 });
94
95 save_profile(&profile, profile_path)?;
96
97 render_result(&CommandOutput::card_value(
98 "Governance Updated",
99 &ConfigMutationOutput {
100 field: "governance.authz".to_owned(),
101 message: format!("Authz mode set to {}", args.mode.to_lowercase()),
102 },
103 ));
104 Ok(())
105}
106
107fn execute_show() -> Result<()> {
108 let profile = ProfileBootstrap::get()?;
109 let summary = profile
110 .governance
111 .as_ref()
112 .and_then(|g| g.authz.as_ref())
113 .map_or_else(
114 || "authz: none (fail-closed deny-all)".to_owned(),
115 |authz| {
116 authz.hook.url.as_deref().map_or_else(
117 || "authz mode set".to_owned(),
118 |url| format!("authz mode set, url={url}"),
119 )
120 },
121 );
122 render_result(&CommandOutput::text_titled(
123 "Governance Configuration",
124 summary,
125 ));
126 Ok(())
127}