1use super::{Capability, CapabilityLocalization, CapabilityStatus};
11use crate::tool_types::ToolHints;
12use crate::tools::{Tool, ToolExecutionResult};
13use crate::traits::ToolContext;
14use async_trait::async_trait;
15use serde_json::{Value, json};
16
17const INTERNAL_KV_PREFIXES: &[&str] = &[
23 super::AGENT_RUN_KEY_PREFIX,
24 crate::ard_attachment::ARD_ATTACHMENT_KV_PREFIX,
25 crate::ard_attachment::ARD_DISCOVERY_KV_PREFIX,
26];
27const INTERNAL_SECRET_PREFIXES: &[&str] = &["browserless_internal:"];
28
29pub fn is_internal_session_kv_key(key: &str) -> bool {
30 INTERNAL_KV_PREFIXES
31 .iter()
32 .any(|prefix| key.starts_with(prefix))
33}
34
35fn reserved_kv_key_error() -> ToolExecutionResult {
36 ToolExecutionResult::tool_error("Key is reserved for internal system use")
37}
38
39fn is_internal_secret_name(name: &str) -> bool {
40 INTERNAL_SECRET_PREFIXES
41 .iter()
42 .any(|prefix| name.starts_with(prefix))
43}
44
45pub const SESSION_STORAGE_CAPABILITY_ID: &str = "session_storage";
46
47pub struct SessionStorageCapability;
49
50impl Capability for SessionStorageCapability {
51 fn id(&self) -> &str {
52 SESSION_STORAGE_CAPABILITY_ID
53 }
54
55 fn name(&self) -> &str {
56 "Storage"
57 }
58
59 fn description(&self) -> &str {
60 r#"Tools to store and retrieve key/value pairs and encrypted secrets within a session.
61
62> [!NOTE]
63> Data persists for the session duration. Secrets are encrypted at rest.
64
65> [!TIP]
66> Use key/value storage for general data. Use secrets for sensitive information like API keys or tokens."#
67 }
68
69 fn localizations(&self) -> Vec<CapabilityLocalization> {
70 vec![CapabilityLocalization::text(
71 "uk",
72 "Сховище",
73 r#"Інструменти для збереження та отримання пар ключ-значення і зашифрованих секретів у межах сесії.
74
75> [!NOTE]
76> Дані зберігаються протягом усієї сесії. Секрети шифруються при зберіганні.
77
78> [!TIP]
79> Використовуйте сховище ключ-значення для загальних даних. Використовуйте секрети для чутливої інформації, як-от API-ключі чи токени."#,
80 )]
81 }
82
83 fn status(&self) -> CapabilityStatus {
84 CapabilityStatus::Available
85 }
86
87 fn icon(&self) -> Option<&str> {
88 Some("database")
89 }
90
91 fn category(&self) -> Option<&str> {
92 Some("Storage")
93 }
94
95 fn system_prompt_addition(&self) -> Option<&str> {
96 Some(
97 "Use `kv_store` for general data. Use `secret_store` for sensitive data (API keys, tokens, credentials) — secrets are encrypted at rest. Keys are unique per session; storing with the same key overwrites.",
98 )
99 }
100
101 fn tools(&self) -> Vec<Box<dyn Tool>> {
102 vec![Box::new(KvStoreTool), Box::new(SecretStoreTool)]
103 }
104
105 fn features(&self) -> Vec<&'static str> {
106 vec!["secrets", "key_value"]
107 }
108}
109
110pub struct KvStoreTool;
116
117#[async_trait]
118impl Tool for KvStoreTool {
119 fn narrate(
120 &self,
121 tool_call: &crate::tool_types::ToolCall,
122 phase: crate::tool_narration::ToolNarrationPhase,
123 locale: Option<&str>,
124 _ctx: crate::tool_narration::ToolNarrationContext<'_>,
125 ) -> Option<String> {
126 let fallback = self.display_name().unwrap_or("Key-Value Store");
127 Some(crate::tool_narration::narrate_secret_store(
128 &tool_call.arguments,
129 fallback,
130 phase,
131 locale,
132 ))
133 }
134
135 fn name(&self) -> &str {
136 "kv_store"
137 }
138
139 fn display_name(&self) -> Option<&str> {
140 Some("Key-Value Store")
141 }
142
143 fn description(&self) -> &str {
144 "Key/value storage operations: set, get, delete, or list keys."
145 }
146
147 fn parameters_schema(&self) -> Value {
148 json!({
149 "type": "object",
150 "properties": {
151 "operation": {
152 "type": "string",
153 "enum": ["set", "get", "delete", "list"],
154 "description": "The operation to perform"
155 },
156 "key": {
157 "type": "string",
158 "description": "The key (required for set, get, delete; max 255 chars)"
159 },
160 "value": {
161 "type": "string",
162 "description": "The value to store (required for set; can be JSON-encoded)"
163 }
164 },
165 "required": ["operation"],
166 "additionalProperties": false
167 })
168 }
169
170 fn hints(&self) -> ToolHints {
171 ToolHints::default()
174 .with_idempotent(true)
175 .with_concurrency_class("session_storage")
176 }
177
178 async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
179 ToolExecutionResult::tool_error(
180 "kv_store requires context. This tool must be executed with session context.",
181 )
182 }
183
184 async fn execute_with_context(
185 &self,
186 arguments: Value,
187 context: &ToolContext,
188 ) -> ToolExecutionResult {
189 let operation = match arguments.get("operation").and_then(|v| v.as_str()) {
190 Some(op) => op,
191 None => {
192 return ToolExecutionResult::tool_error("Missing required parameter: operation");
193 }
194 };
195
196 let storage_store = match &context.storage_store {
197 Some(store) => store,
198 None => {
199 return ToolExecutionResult::tool_error("Storage not available in this context");
200 }
201 };
202
203 match operation {
204 "set" => {
205 let key = match arguments.get("key").and_then(|v| v.as_str()) {
206 Some(k) => k,
207 None => {
208 return ToolExecutionResult::tool_error(
209 "Missing required parameter: key (for set operation)",
210 );
211 }
212 };
213 let value = match arguments.get("value").and_then(|v| v.as_str()) {
214 Some(v) => v,
215 None => {
216 return ToolExecutionResult::tool_error(
217 "Missing required parameter: value (for set operation)",
218 );
219 }
220 };
221 if key.len() > 255 {
222 return ToolExecutionResult::tool_error("Key must be 255 characters or less");
223 }
224 if is_internal_session_kv_key(key) {
225 return reserved_kv_key_error();
226 }
227 match storage_store
228 .set_value(context.session_id, key, value)
229 .await
230 {
231 Ok(()) => ToolExecutionResult::success(json!({
232 "operation": "set",
233 "key": key,
234 "success": true
235 })),
236 Err(e) => ToolExecutionResult::internal_error(e),
237 }
238 }
239 "get" => {
240 let key = match arguments.get("key").and_then(|v| v.as_str()) {
241 Some(k) => k,
242 None => {
243 return ToolExecutionResult::tool_error(
244 "Missing required parameter: key (for get operation)",
245 );
246 }
247 };
248 if is_internal_session_kv_key(key) {
249 return reserved_kv_key_error();
250 }
251 match storage_store.get_value(context.session_id, key).await {
252 Ok(Some(value)) => ToolExecutionResult::success(json!({
253 "operation": "get",
254 "key": key,
255 "value": value,
256 "found": true
257 })),
258 Ok(None) => ToolExecutionResult::success(json!({
259 "operation": "get",
260 "key": key,
261 "value": null,
262 "found": false
263 })),
264 Err(e) => ToolExecutionResult::internal_error(e),
265 }
266 }
267 "delete" => {
268 let key = match arguments.get("key").and_then(|v| v.as_str()) {
269 Some(k) => k,
270 None => {
271 return ToolExecutionResult::tool_error(
272 "Missing required parameter: key (for delete operation)",
273 );
274 }
275 };
276 if is_internal_session_kv_key(key) {
277 return reserved_kv_key_error();
278 }
279 match storage_store.delete_value(context.session_id, key).await {
280 Ok(deleted) => ToolExecutionResult::success(json!({
281 "operation": "delete",
282 "key": key,
283 "deleted": deleted
284 })),
285 Err(e) => ToolExecutionResult::internal_error(e),
286 }
287 }
288 "list" => match storage_store.list_keys(context.session_id).await {
289 Ok(keys) => {
290 let key_list: Vec<Value> = keys
291 .iter()
292 .filter(|k| !is_internal_session_kv_key(&k.key))
293 .map(|k| {
294 json!({
295 "key": k.key,
296 "created_at": k.created_at.to_rfc3339(),
297 "updated_at": k.updated_at.to_rfc3339()
298 })
299 })
300 .collect();
301 ToolExecutionResult::success(json!({
302 "operation": "list",
303 "keys": key_list,
304 "count": key_list.len()
305 }))
306 }
307 Err(e) => ToolExecutionResult::internal_error(e),
308 },
309 _ => ToolExecutionResult::tool_error(format!(
310 "Invalid operation: {}. Must be one of: set, get, delete, list",
311 operation
312 )),
313 }
314 }
315
316 fn requires_context(&self) -> bool {
317 true
318 }
319}
320
321pub struct SecretStoreTool;
327
328#[async_trait]
329impl Tool for SecretStoreTool {
330 fn narrate(
331 &self,
332 tool_call: &crate::tool_types::ToolCall,
333 phase: crate::tool_narration::ToolNarrationPhase,
334 locale: Option<&str>,
335 _ctx: crate::tool_narration::ToolNarrationContext<'_>,
336 ) -> Option<String> {
337 let fallback = self.display_name().unwrap_or("Secret Store");
338 Some(crate::tool_narration::narrate_secret_store(
339 &tool_call.arguments,
340 fallback,
341 phase,
342 locale,
343 ))
344 }
345
346 fn name(&self) -> &str {
347 "secret_store"
348 }
349
350 fn display_name(&self) -> Option<&str> {
351 Some("Secret Store")
352 }
353
354 fn description(&self) -> &str {
355 "Encrypted secret storage operations: set, get, delete, or list secrets."
356 }
357
358 fn parameters_schema(&self) -> Value {
359 json!({
360 "type": "object",
361 "properties": {
362 "operation": {
363 "type": "string",
364 "enum": ["set", "get", "delete", "list"],
365 "description": "The operation to perform"
366 },
367 "name": {
368 "type": "string",
369 "description": "The secret name (required for set, get, delete; max 255 chars)"
370 },
371 "value": {
372 "type": "string",
373 "description": "The secret value to store (required for set; will be encrypted)"
374 }
375 },
376 "required": ["operation"],
377 "additionalProperties": false
378 })
379 }
380
381 fn hints(&self) -> ToolHints {
382 ToolHints::default()
385 .with_idempotent(true)
386 .with_requires_secrets(true)
387 .with_concurrency_class("session_storage")
388 }
389
390 async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
391 ToolExecutionResult::tool_error(
392 "secret_store requires context. This tool must be executed with session context.",
393 )
394 }
395
396 async fn execute_with_context(
397 &self,
398 arguments: Value,
399 context: &ToolContext,
400 ) -> ToolExecutionResult {
401 let operation = match arguments.get("operation").and_then(|v| v.as_str()) {
402 Some(op) => op,
403 None => {
404 return ToolExecutionResult::tool_error("Missing required parameter: operation");
405 }
406 };
407
408 let storage_store = match &context.storage_store {
409 Some(store) => store,
410 None => {
411 return ToolExecutionResult::tool_error("Storage not available in this context");
412 }
413 };
414
415 match operation {
416 "set" => {
417 let name = match arguments.get("name").and_then(|v| v.as_str()) {
418 Some(n) => n,
419 None => {
420 return ToolExecutionResult::tool_error(
421 "Missing required parameter: name (for set operation)",
422 );
423 }
424 };
425 let value = match arguments.get("value").and_then(|v| v.as_str()) {
426 Some(v) => v,
427 None => {
428 return ToolExecutionResult::tool_error(
429 "Missing required parameter: value (for set operation)",
430 );
431 }
432 };
433 if name.len() > 255 {
434 return ToolExecutionResult::tool_error(
435 "Secret name must be 255 characters or less",
436 );
437 }
438 if is_internal_secret_name(name) {
439 return ToolExecutionResult::tool_error(
440 "Secret name is reserved for internal system use",
441 );
442 }
443 match storage_store
444 .set_secret(context.session_id, name, value)
445 .await
446 {
447 Ok(()) => ToolExecutionResult::success(json!({
448 "operation": "set",
449 "name": name,
450 "success": true
451 })),
452 Err(e) => {
453 let msg = e.to_string();
454 if msg.contains("Encryption not configured") {
455 ToolExecutionResult::tool_error(
456 "Secret storage not available. Encryption is not configured.",
457 )
458 } else {
459 ToolExecutionResult::internal_error(e)
460 }
461 }
462 }
463 }
464 "get" => {
465 let name = match arguments.get("name").and_then(|v| v.as_str()) {
466 Some(n) => n,
467 None => {
468 return ToolExecutionResult::tool_error(
469 "Missing required parameter: name (for get operation)",
470 );
471 }
472 };
473 if is_internal_secret_name(name) {
474 return ToolExecutionResult::tool_error("Secret not found");
475 }
476 match storage_store.get_secret(context.session_id, name).await {
477 Ok(Some(value)) => ToolExecutionResult::success(json!({
478 "operation": "get",
479 "name": name,
480 "value": value,
481 "found": true
482 })),
483 Ok(None) => ToolExecutionResult::success(json!({
484 "operation": "get",
485 "name": name,
486 "value": null,
487 "found": false
488 })),
489 Err(e) => {
490 let msg = e.to_string();
491 if msg.contains("Encryption not configured") {
492 ToolExecutionResult::tool_error(
493 "Secret storage not available. Encryption is not configured.",
494 )
495 } else {
496 ToolExecutionResult::internal_error(e)
497 }
498 }
499 }
500 }
501 "delete" => {
502 let name = match arguments.get("name").and_then(|v| v.as_str()) {
503 Some(n) => n,
504 None => {
505 return ToolExecutionResult::tool_error(
506 "Missing required parameter: name (for delete operation)",
507 );
508 }
509 };
510 if is_internal_secret_name(name) {
511 return ToolExecutionResult::tool_error(
512 "Secret name is reserved for internal system use",
513 );
514 }
515 match storage_store.delete_secret(context.session_id, name).await {
516 Ok(deleted) => ToolExecutionResult::success(json!({
517 "operation": "delete",
518 "name": name,
519 "deleted": deleted
520 })),
521 Err(e) => ToolExecutionResult::internal_error(e),
522 }
523 }
524 "list" => match storage_store.list_secrets(context.session_id).await {
525 Ok(secrets) => {
526 let secret_list: Vec<Value> = secrets
527 .iter()
528 .filter(|s| !is_internal_secret_name(&s.name))
529 .map(|s| {
530 json!({
531 "name": s.name,
532 "created_at": s.created_at.to_rfc3339(),
533 "updated_at": s.updated_at.to_rfc3339()
534 })
535 })
536 .collect();
537 ToolExecutionResult::success(json!({
538 "operation": "list",
539 "secrets": secret_list,
540 "count": secret_list.len()
541 }))
542 }
543 Err(e) => {
544 let msg = e.to_string();
545 if msg.contains("Encryption not configured") {
546 ToolExecutionResult::tool_error(
547 "Secret storage not available. Encryption is not configured.",
548 )
549 } else {
550 ToolExecutionResult::internal_error(e)
551 }
552 }
553 },
554 _ => ToolExecutionResult::tool_error(format!(
555 "Invalid operation: {}. Must be one of: set, get, delete, list",
556 operation
557 )),
558 }
559 }
560
561 fn requires_context(&self) -> bool {
562 true
563 }
564}
565
566#[cfg(test)]
567mod tests {
568 use super::*;
569 use crate::traits::SessionStorageStore;
570 use crate::typed_id::SessionId;
571 use crate::{KeyInfo, Result};
572 use std::collections::HashMap;
573 use std::sync::{Arc, Mutex};
574
575 #[derive(Default)]
576 struct TestStorageStore {
577 values: Mutex<HashMap<String, String>>,
578 }
579
580 #[async_trait]
581 impl crate::traits::SessionStorageStore for TestStorageStore {
582 async fn set_value(&self, _session_id: SessionId, key: &str, value: &str) -> Result<()> {
583 self.values
584 .lock()
585 .unwrap()
586 .insert(key.to_string(), value.to_string());
587 Ok(())
588 }
589
590 async fn get_value(&self, _session_id: SessionId, key: &str) -> Result<Option<String>> {
591 Ok(self.values.lock().unwrap().get(key).cloned())
592 }
593
594 async fn delete_value(&self, _session_id: SessionId, key: &str) -> Result<bool> {
595 Ok(self.values.lock().unwrap().remove(key).is_some())
596 }
597
598 async fn list_keys(&self, _session_id: SessionId) -> Result<Vec<KeyInfo>> {
599 let now = chrono::Utc::now();
600 Ok(self
601 .values
602 .lock()
603 .unwrap()
604 .keys()
605 .map(|key| KeyInfo {
606 key: key.clone(),
607 created_at: now,
608 updated_at: now,
609 })
610 .collect())
611 }
612
613 async fn set_secret(
614 &self,
615 _session_id: SessionId,
616 _name: &str,
617 _value: &str,
618 ) -> Result<()> {
619 Ok(())
620 }
621
622 async fn get_secret(&self, _session_id: SessionId, _name: &str) -> Result<Option<String>> {
623 Ok(None)
624 }
625
626 async fn delete_secret(&self, _session_id: SessionId, _name: &str) -> Result<bool> {
627 Ok(false)
628 }
629
630 async fn list_secrets(&self, _session_id: SessionId) -> Result<Vec<crate::SecretInfo>> {
631 Ok(Vec::new())
632 }
633 }
634
635 #[test]
636 fn test_internal_kv_key_filtering() {
637 assert!(is_internal_session_kv_key("agent_run:abc"));
638 assert!(!is_internal_session_kv_key("user:agent_run:abc"));
639 }
640
641 #[test]
642 fn test_internal_secret_name_filtering() {
643 assert!(is_internal_secret_name("browserless_internal:cookies"));
644 assert!(!is_internal_secret_name("api_key"));
645 }
646
647 #[test]
650 fn test_capability_has_system_prompt() {
651 let cap = SessionStorageCapability;
652 let prompt = cap.system_prompt_addition().unwrap();
653 assert!(prompt.contains("kv_store"));
654 assert!(prompt.contains("secret_store"));
655 assert!(prompt.contains("encrypted"));
656 }
657
658 #[tokio::test]
659 async fn test_kv_store_without_context() {
660 let tool = KvStoreTool;
661 let result = tool
662 .execute(json!({"operation": "set", "key": "test", "value": "data"}))
663 .await;
664
665 if let ToolExecutionResult::ToolError(msg) = result {
666 assert!(msg.contains("requires context"));
667 } else {
668 panic!("Expected tool error");
669 }
670 }
671
672 #[tokio::test]
673 async fn test_kv_store_missing_operation() {
674 let tool = KvStoreTool;
675 let context = ToolContext::new(SessionId::new());
676
677 let result = tool
678 .execute_with_context(json!({"key": "test"}), &context)
679 .await;
680
681 if let ToolExecutionResult::ToolError(msg) = result {
682 assert!(msg.contains("operation"));
683 } else {
684 panic!("Expected tool error for missing operation");
685 }
686 }
687
688 #[tokio::test]
689 async fn test_kv_store_no_storage_store() {
690 let tool = KvStoreTool;
691 let context = ToolContext::new(SessionId::new());
692
693 let result = tool
694 .execute_with_context(
695 json!({"operation": "set", "key": "test", "value": "data"}),
696 &context,
697 )
698 .await;
699
700 if let ToolExecutionResult::ToolError(msg) = result {
701 assert!(msg.contains("not available"));
702 } else {
703 panic!("Expected tool error for missing storage store");
704 }
705 }
706
707 #[tokio::test]
708 async fn test_kv_store_rejects_reserved_internal_keys() {
709 let tool = KvStoreTool;
710 let session_id = SessionId::new();
711 let storage = Arc::new(TestStorageStore::default());
712 storage
713 .set_value(session_id, "agent_run:trusted", "trusted-record")
714 .await
715 .unwrap();
716 storage
717 .set_value(session_id, "public", "public-record")
718 .await
719 .unwrap();
720 let context = ToolContext::with_storage_store(session_id, storage.clone());
721
722 for arguments in [
723 json!({"operation": "set", "key": "agent_run:trusted", "value": "forged"}),
724 json!({"operation": "get", "key": "agent_run:trusted"}),
725 json!({"operation": "delete", "key": "agent_run:trusted"}),
726 ] {
727 let result = tool.execute_with_context(arguments, &context).await;
728 assert!(
729 matches!(result, ToolExecutionResult::ToolError(ref msg) if msg.contains("reserved")),
730 "expected reserved-key error, got {result:?}"
731 );
732 }
733
734 assert_eq!(
735 storage
736 .get_value(session_id, "agent_run:trusted")
737 .await
738 .unwrap()
739 .as_deref(),
740 Some("trusted-record")
741 );
742
743 let result = tool
744 .execute_with_context(json!({"operation": "list"}), &context)
745 .await;
746 let ToolExecutionResult::Success(value) = result else {
747 panic!("expected successful list");
748 };
749 assert_eq!(value["count"], 1);
750 assert_eq!(value["keys"][0]["key"], "public");
751 }
752
753 #[tokio::test]
754 async fn test_secret_store_without_context() {
755 let tool = SecretStoreTool;
756 let result = tool
757 .execute(json!({"operation": "set", "name": "api_key", "value": "secret123"}))
758 .await;
759
760 if let ToolExecutionResult::ToolError(msg) = result {
761 assert!(msg.contains("requires context"));
762 } else {
763 panic!("Expected tool error");
764 }
765 }
766}