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}