Skip to main content

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