Skip to main content

netget/client/vault/
actions.rs

1use crate::protocol::log_template::LogTemplate;
2use crate::{
3    llm::actions::{
4        client_trait::{Client, ClientActionResult},
5        protocol_trait::Protocol,
6        ActionDefinition, Parameter, ParameterDefinition,
7    },
8    protocol::{ConnectContext, EventType},
9    state::AppState,
10};
11use anyhow::{bail, Result};
12use serde_json::{json, Value};
13use std::sync::LazyLock;
14#[derive(Default)]
15pub struct VaultClientProtocol;
16impl VaultClientProtocol {
17    pub fn new() -> Self {
18        Self
19    }
20}
21fn p(name: &str, kind: &str, description: &str, required: bool) -> Parameter {
22    Parameter {
23        name: name.into(),
24        type_hint: kind.into(),
25        description: description.into(),
26        required,
27    }
28}
29fn request() -> ActionDefinition {
30    ActionDefinition{name:"vault_request".into(),description:"Selected Vault system reads and KV v2 read/write/list/metadata operations. No raw HTTP routes, dynamic-secret engines or administration.".into(),parameters:vec![p("operation","string","seal_status, health, leader, read, write, list, or metadata",true),p("mount","string","KV v2 mount override; default startup kv_mount",false),p("path","string","KV relative ASCII name segments; list accepts a folder slash; empty only for mount-root list",false),p("version","integer","read: nonnegative version; omitted/0 reads latest",false),p("data","object","write: secret object, encoded into native KV v2 data envelope",false),p("cas","integer","write: expected current version;0 creates only when no versions exist",false)],example:json!({"type":"vault_request","operation":"read","path":"fixture/app","version":1}),log_template:Some(LogTemplate::new().with_info("Vault {operation} request queued"))}
31}
32fn login() -> ActionDefinition {
33    ActionDefinition{name:"vault_userpass_login".into(),description:"Authenticate selected userpass credentials. A new attempt discards the previous session token; only a complete validated success installs one token. Login password and returned token are omitted from events and incidental diagnostics.".into(),parameters:vec![p("username","string","Bounded ASCII userpass username",true),p("password","string","Userpass password; not included in result metadata",true),p("auth_mount","string","Userpass mount override; default startup auth_mount",false)],example:json!({"type":"vault_userpass_login","username":"fixture-reader","password":"fixture-password"}),log_template:Some(LogTemplate::new().with_info("Vault userpass authentication queued"))}
34}
35fn clear() -> ActionDefinition {
36    ActionDefinition{name:"vault_clear_token".into(),description:"Forget this client's credential locally. Does not revoke a Vault token or end any backend lease.".into(),parameters:vec![],example:json!({"type":"vault_clear_token"}),log_template:Some(LogTemplate::new().with_info("Vault local credential cleared"))}
37}
38fn disconnect() -> ActionDefinition {
39    ActionDefinition {
40        name: "disconnect".into(),
41        description: "Cancel pending I/O and stop this logical client".into(),
42        parameters: vec![],
43        example: json!({"type":"disconnect"}),
44        log_template: Some(LogTemplate::new().with_info("Vault client disconnected")),
45    }
46}
47fn actions() -> Vec<ActionDefinition> {
48    vec![request(), login(), clear(), disconnect()]
49}
50fn event(id: &str, description: &str, params: Vec<Parameter>) -> EventType {
51    EventType::new(id, description, request().example)
52        .with_parameters(params)
53        .with_actions(actions())
54}
55pub static CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
56    event(
57        "vault_connected",
58        "Public seal status succeeded; configured token has not been verified",
59        vec![
60            p(
61                "origin",
62                "string",
63                "Vault service HTTP or HTTPS origin",
64                true,
65            ),
66            p("kv_mount", "string", "Default KV v2 mount", true),
67            p("auth_mount", "string", "Default userpass mount", true),
68            p(
69                "token_present",
70                "boolean",
71                "Credential configured, not proof that Vault accepts it",
72                true,
73            ),
74            p(
75                "authentication_verified",
76                "boolean",
77                "False: startup seal probe is public",
78                true,
79            ),
80            p(
81                "seal",
82                "object",
83                "Typed seal status, including initialized/sealed/threshold/progress",
84                true,
85            ),
86        ],
87    )
88});
89pub static RESPONSE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
90    event("vault_response","Complete selected Vault response; legitimate health status codes preserve their native meaning",vec![p("request","object","Originating typed action with credentials omitted",true),p("operation","string","Selected operation",true),p("status","integer","HTTP status; health may return200/429/472/473/474/501/503/530",true),p("data","object","Typed system body or KV v2 envelope. Reads keep data and version metadata distinct; writes return positive version acknowledgement; lists return keys with folder suffixes; metadata retains versions, deletion/destruction state and RFC3339 times. No secret store is created.",true)])
91});
92pub static AUTH_EVENT: LazyLock<EventType> = LazyLock::new(|| {
93    event("vault_authentication","Validated userpass success or explicit local credential clear",vec![p("request","object","Login receipt excludes password; clear receipt has no credential",true),p("operation","string","login or clear_token",true),p("status","integer|null","HTTP200 for login;null for local clear",true),p("token_present","boolean","One ephemeral credential currently retained",true),p("authentication_verified","boolean","True only after complete validated login auth; false for clear",true),p("auth","object|null","Token accessor, policies, metadata, lease_duration seconds, renewable, entity_id, service/batch token_type and optional orphan/num_uses. client_token and MFA challenge are never exposed; MFA continuation is refused.",true),p("request_id","string","Native login request identifier; absent for local clear",false),p("warnings","array|null","Credential-redacted login warnings; absent for local clear",false)])
94});
95pub static ERROR_EVENT: LazyLock<EventType> = LazyLock::new(|| {
96    event(
97        "vault_request_error",
98        "Request refused or failed; no partial success, session remains available",
99        vec![
100            p(
101                "request",
102                "object",
103                "Originating action with password omitted",
104                true,
105            ),
106            p(
107                "status",
108                "integer|null",
109                "Native HTTP status when received",
110                true,
111            ),
112            p("category", "string", "http, schema, or transport", true),
113            p(
114                "errors",
115                "array",
116                "Credential-redacted native/validation/deadline diagnostics",
117                true,
118            ),
119            p(
120                "token_present",
121                "boolean",
122                "Current credential presence; failed login leaves it false",
123                true,
124            ),
125        ],
126    )
127});
128impl Protocol for VaultClientProtocol {
129    fn protocol_name(&self) -> &'static str {
130        "Vault"
131    }
132    fn stack_name(&self) -> &'static str {
133        "ETH>IP>TCP>HTTP>Vault"
134    }
135    fn keywords(&self) -> Vec<&'static str> {
136        vec!["vault", "hashicorp", "kv v2"]
137    }
138    fn description(&self) -> &'static str {
139        "Selected typed Vault authentication and KV v2 operations"
140    }
141    fn group_name(&self) -> &'static str {
142        "AI & API"
143    }
144    fn example_prompt(&self) -> &'static str {
145        "Read version1 of fixture/app from Vault at localhost:8200"
146    }
147    fn get_async_actions(&self, _: &AppState) -> Vec<ActionDefinition> {
148        actions()
149    }
150    fn get_sync_actions(&self) -> Vec<ActionDefinition> {
151        vec![]
152    }
153    fn get_event_types(&self) -> Vec<EventType> {
154        vec![
155            CONNECTED_EVENT.clone(),
156            RESPONSE_EVENT.clone(),
157            AUTH_EVENT.clone(),
158            ERROR_EVENT.clone(),
159        ]
160    }
161    fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
162        vec![
163        ParameterDefinition{name:"token".into(),type_hint:"string".into(),description:"Optional bounded Vault token; syntax checked, never authenticated by the public startup probe. Ephemeral header only, no credential file.".into(),required:false,example:json!("hvs.fixture-token"),default:None},
164        ParameterDefinition{name:"kv_mount".into(),type_hint:"string".into(),description:"Default KV v2 mount of bounded ASCII segments; sys is refused".into(),required:false,example:json!("secret"),default:Some(json!(super::DEFAULT_KV_MOUNT))},
165        ParameterDefinition{name:"auth_mount".into(),type_hint:"string".into(),description:"Default selected userpass mount, excluding sys".into(),required:false,example:json!("userpass"),default:Some(json!(super::DEFAULT_AUTH_MOUNT))},
166        ParameterDefinition{name:"request_timeout_secs".into(),type_hint:"integer".into(),description:"Whole request connect/head/body deadline1..30 seconds".into(),required:false,example:json!(10),default:Some(json!(super::DEFAULT_TIMEOUT_SECS))},
167    ]
168    }
169    fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
170        use crate::protocol::metadata::*;
171        ProtocolMetadataV2::builder().state(DevelopmentState::Experimental).privilege_requirement(PrivilegeRequirement::None).well_known_port(8200).implementation("Bounded HTTP(S), typed public system probes, KV v2 version/CAS/metadata and selected userpass login").llm_control("Selected native authentication and secret operations, redacted auth metadata, event routing/injection and shared memory").e2e_testing("tests/client/vault: native isolated Vault daemon and CLI, programmable NetGet pair, handler paths, schema/bounds/deadlines/cancellation; existing server native CLI tests").notes("One ephemeral token and one latest bounded password for reflection redaction, no protocol secret database. Body1MiB, arrays10000, object fields256, strings16KiB, depth32/nodes65536; in-memory actions preflight retained content8MiB before copying; one request, queues8,4 followups. HTTP(S) native with certificate verification; browser HTTP only. No custom trust, client certificates, namespaces, redirects, proxies, cookie state, token helpers/files, automatic auth/renewal/retry, response wrapping, MFA, OAuth/OIDC/AppRole, dynamic-secret engines, deletion/patch/undelete/destroy or administration. Clear is local, never backend revocation. Public seal probe does not verify startup tokens. Existing programmable KV v2 server auth/login refusal remains.").max_inbound_bytes(super::api::MAX_BODY).build()
172    }
173    fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
174        let llm = json!({"type":"open_client","base_stack":"vault","protocol":"vault","remote_addr":"127.0.0.1:8200","instruction":"Read Vault health, then explain initialized, sealed and standby status"});
175        let mut fixed = llm.clone();
176        fixed["event_handlers"] = json!([{"event_pattern":"vault_connected","handler":{"type":"static","actions":[{"type":"vault_request","operation":"health"}]}},{"event_pattern":"*","handler":{"type":"static","actions":[]}}]);
177        let mut script = fixed.clone();
178        script["event_handlers"][0]["handler"] = json!({"type":"script","language":"python","code":"import json,sys\njson.dump({'actions':[{'type':'vault_request','operation':'health'}]},sys.stdout)"});
179        crate::llm::actions::StartupExamples::new(llm, script, fixed)
180    }
181}
182impl Client for VaultClientProtocol {
183    fn connect(
184        &self,
185        ctx: ConnectContext,
186    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<std::net::SocketAddr>> + Send>>
187    {
188        Box::pin(super::connect(ctx))
189    }
190    fn execute_action(&self, value: Value) -> Result<ClientActionResult> {
191        if !super::api::action_within_budget(&value) {
192            crate::utils::json_budget::drop_iteratively(value);
193            bail!("Vault action depth/node/retained-content limit");
194        }
195        match value["type"].as_str() {
196            Some("vault_request" | "vault_userpass_login" | "vault_clear_token") => {
197                super::api::request(&value, super::DEFAULT_KV_MOUNT, super::DEFAULT_AUTH_MOUNT)?;
198                Ok(ClientActionResult::Custom {
199                    name: "vault_request".into(),
200                    data: value,
201                })
202            }
203            Some("disconnect") => Ok(ClientActionResult::Disconnect),
204            _ => bail!("unknown Vault client action"),
205        }
206    }
207}