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