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}