kaish_tool_api/ctx.rs
1//! The trimmed execution context exposed to tools.
2
3use std::any::Any;
4use std::path::{Path, PathBuf};
5use std::sync::Arc;
6use std::time::Duration;
7
8use async_trait::async_trait;
9use kaish_types::{OutputFormat, Value};
10
11use crate::backend::KernelBackend;
12
13/// RAII guard returned by [`ToolCtx::patient`].
14///
15/// While held, the kernel's script-level timeout watchdog is suspended for
16/// this execution: the script clock freezes and the guard's own budget
17/// governs instead. Dropping the guard resumes the script clock with the
18/// remaining time it had at acquire.
19///
20/// The inner box is the kernel's hold object; its `Drop` does the restore.
21/// An *inert* guard (no watchdog running — e.g. the kernel has no script
22/// timeout, or a non-kernel test context) holds nothing and drops as a no-op.
23pub struct PatientGuard {
24 hold: Option<Box<dyn Any + Send>>,
25}
26
27impl PatientGuard {
28 /// A guard that does nothing — for contexts without a watchdog.
29 pub fn inert() -> Self {
30 Self { hold: None }
31 }
32
33 /// Wrap a kernel hold object whose `Drop` restores the watchdog.
34 pub fn held(hold: Box<dyn Any + Send>) -> Self {
35 Self { hold: Some(hold) }
36 }
37
38 /// Whether this guard actually suspended a watchdog.
39 pub fn is_active(&self) -> bool {
40 self.hold.is_some()
41 }
42}
43
44/// The portable execution context a tool sees.
45///
46/// This is deliberately small: it carries only what a well-behaved,
47/// out-of-tree tool needs. The kernel's full `ExecContext` implements this
48/// trait; trusted in-tree builtins that need deeper state (job control,
49/// streaming pipes, the dispatcher) downcast through [`ToolCtx::as_any_mut`].
50///
51/// `Send + Sync` are supertraits because tool execution is async: a `&dyn
52/// ToolCtx` shared with an async helper is held across await points, and for
53/// the resulting future to be `Send` the referent must be `Sync`. The kernel's
54/// `ExecContext` already satisfies both.
55///
56/// `#[async_trait]` desugars the `async fn`s below into boxed futures; every
57/// already-synchronous method is untouched.
58#[async_trait]
59pub trait ToolCtx: Send + Sync {
60 /// The backend for file I/O and tool dispatch.
61 ///
62 /// Tools reach the VFS (and re-dispatch other tools) through this handle.
63 fn backend(&self) -> &Arc<dyn KernelBackend>;
64
65 /// The current working directory, as a VFS path.
66 fn cwd(&self) -> &Path;
67
68 /// Resolve a (possibly relative) path against the cwd, normalizing `.`
69 /// and `..` lexically. Never touches the real filesystem.
70 fn resolve_path(&self, path: &str) -> PathBuf;
71
72 /// Read a variable from the current scope, cloned.
73 ///
74 /// Returns `None` if the name is unset. Tools use this for configuration
75 /// supplied by the frontend (e.g. `HOSTNAME`).
76 fn var(&self, name: &str) -> Option<Value>;
77
78 /// Set a variable in the current scope.
79 fn set_var(&mut self, name: &str, value: Value);
80
81 /// Set the per-execution output format override (e.g. from `--json`).
82 ///
83 /// The dispatcher reads this after `execute()` returns and applies the
84 /// format to the result.
85 fn set_output_format(&mut self, format: OutputFormat);
86
87 /// Suspend the script-level timeout watchdog while the returned guard is
88 /// held, bounding the patient operation by `budget` instead.
89 ///
90 /// For tools that legitimately outlive a script timeout (model/provider
91 /// calls that run minutes): while the guard is held the script clock
92 /// freezes and the watchdog fires only if the hold outlives `budget`.
93 /// On drop the script clock resumes with the remaining time it had at
94 /// acquire. Only Rust tool code can obtain the guard — script code has no
95 /// path to it, so the script-level budget keeps its teeth.
96 ///
97 /// Cancellation stays live while suspended: `Kernel::cancel()` and the
98 /// embedder token fire immediately — only the *timer* pauses. A patient
99 /// tool must still `select!` its wait against the cancellation token.
100 ///
101 /// The explicit `timeout` builtin is **not** suspended: a user-requested
102 /// bound on a command keeps its teeth regardless of patient holds.
103 ///
104 /// The default implementation returns an inert guard (no watchdog to
105 /// suspend); the kernel's context overrides it.
106 fn patient(&self, budget: Duration) -> PatientGuard {
107 let _ = budget;
108 PatientGuard::inert()
109 }
110
111 /// Escape hatch for trusted in-tree tools: recover the concrete context.
112 ///
113 /// Out-of-tree tools must not rely on this — downcasting to a kernel type
114 /// is exactly the coupling this trait exists to avoid. It is here so
115 /// in-tree builtins needing job control / pipes / the dispatcher can keep
116 /// full access without those internals leaking into the public surface.
117 ///
118 /// `#[doc(hidden)]`: present for in-tree use but deliberately kept off the
119 /// documented public surface so it doesn't advertise itself as a supported
120 /// downcast hatch.
121 #[doc(hidden)]
122 fn as_any(&self) -> &dyn Any;
123
124 /// Mutable counterpart to [`ToolCtx::as_any`].
125 #[doc(hidden)]
126 fn as_any_mut(&mut self) -> &mut dyn Any;
127}