Skip to main content

Kernel

Struct Kernel 

Source
pub struct Kernel { /* private fields */ }
Expand description

The Kernel (核) — executes kaish code.

This is the primary interface for running kaish commands. It owns all the runtime state: variables, tools, VFS, jobs, and persistence.

Implementations§

Source§

impl Kernel

Source

pub fn new(config: KernelConfig) -> Result<Self>

Create a new kernel with the given configuration.

Source

pub fn transient() -> Result<Self>

Create a transient kernel (no persistence).

Source

pub fn with_backend( backend: Arc<dyn KernelBackend>, config: KernelConfig, configure_vfs: impl FnOnce(&mut VfsRouter), configure_tools: impl FnOnce(&mut ToolRegistry), ) -> Result<Self>

Create a kernel with a custom backend and /v/* virtual path support.

This is the constructor for embedding kaish in other systems that provide their own storage backend (e.g., CRDT-backed storage in kaijutsu).

A VirtualOverlayBackend routes paths automatically:

  • /v/* → Internal VFS (JobFs at /v/jobs, MemoryFs at /v/blobs)
  • /dev → DevFs (synthetic /dev/null, /dev/zero, /dev/random, /dev/urandom) — kernel-owned so it works even when your backend is read-only
  • Everything else → Your custom backend

The optional configure_vfs closure lets you add additional virtual mounts (e.g., /v/docs for CRDT blocks) after the built-in mounts are set up.

Note: The config’s vfs_mode is ignored — all non-/v/* path routing is handled by your custom backend. The config is only used for name, cwd, skip_validation, and interactive.

§Example
// Simple: default /v/* mounts only
let kernel = Kernel::with_backend(backend, config, |_| {}, |_| {})?;

// With custom mounts
let kernel = Kernel::with_backend(backend, config, |vfs| {
    vfs.mount_arc("/v/docs", docs_fs);
    vfs.mount_arc("/v/g", git_fs);
}, |_| {})?;

// With custom tools
let kernel = Kernel::with_backend(backend, config, |_| {}, |tools| {
    tools.register(MyCustomTool::new());
})?;
Source

pub fn plan_program( &self, source: &str, ) -> Result<Vec<PlannedStatement>, Vec<ParseError>>

Plan every statement of source without executing anything — plan_program as a method, so an embedder holding a kernel can pair the plans with get_var lookups against this kernel’s live state.

§Errors

Returns the parse errors when source does not parse.

Source

pub fn expand_fragment( &self, source: &str, addr: FragmentAddr, scope: &[(String, Value)], ) -> Result<Expansion, FragmentError>

Expand one heredoc body against a scope the caller supplies — expand_fragment as a method.

The scope is the caller’s, not this kernel’s: pair it with get_var when the session’s values are the ones to judge against, and supply different values when they are not. Nothing executes, and a $(…) in the body comes back as a Hole rather than running here.

§Errors

Returns a FragmentError when the source does not parse, the address names no heredoc, or the body reads something the supplied scope does not carry.

Source

pub fn name(&self) -> &str

Get the kernel name.

Source

pub fn into_arc(self) -> Arc<Self>

Wrap this Kernel in an Arc and initialize its self-reference.

This enables the Kernel to hand out Arc<dyn CommandDispatcher> references to child contexts, allowing builtins like timeout to dispatch inner commands through the full resolution chain (user tools → builtins → .kai scripts → external commands).

Source

pub async fn fork(&self) -> Arc<Self>

Fork a subsidiary kernel for concurrent execution.

The fork is a fully-functional Kernel that:

  • Snapshots per-session state from the parent: scope (COW — cheap), user-defined tools, cwd, aliases, ignore config, etc. Mutations on the fork do NOT propagate back to the parent — matching bash subshell / background-job semantics.
  • Shares read-mostly resources with the parent via Arc: the tool registry, the VFS router, and the job manager. A job registered by the fork is visible to the parent’s jobs builtin, and the fork sees the same VFS mounts.
  • Owns its own stderr_receiver, cancel_token, and execute_lock. It is never the TTY owner, so interactive is false and terminal_state is None.

The returned Arc has its self_weak populated (via into_arc), so nested dispatch through ctx.dispatcher (e.g. the timeout builtin) routes through the fork itself, not the parent — which is essential for concurrency safety.

Use this for detached background concurrency where the fork should survive parent cancellation: the & background-job operator and any other “fire and forget” worker. The fork gets a fresh, independent cancellation token.

For foreground concurrency (scatter workers, concurrent pipeline stages, $(...) cmdsubs) where parent timeout/cancel must cascade into the fork’s external children, use Self::fork_attached.

Source

pub async fn fork_attached(&self) -> Arc<Self>

Fork attached to the parent’s cancellation.

Same as Self::fork but the fork’s cancel_token is a child of the parent’s. When the parent cancels (request timeout, embedder Kernel::cancel, etc.), the fork’s token also cancels, which in turn kills any external children spawned in the fork via the wait_or_kill / SIGTERM-grace-SIGKILL path.

Source

pub async fn fork_for_background( &self, cancel: CancellationToken, job_id: JobId, ) -> Arc<Self>

Fork for a background job, stamping the job id so external commands spawned anywhere beneath it record their process groups on that job (for kill -<sig> %N). The caller owns cancel so it can also drive JobManager::cancel.

Source

pub fn dispatcher(&self) -> Option<Arc<dyn CommandDispatcher>>

Get an Arc<dyn CommandDispatcher> to this Kernel, if wrapped via into_arc().

Returns None if the Kernel was not wrapped, or if all strong references have been dropped (the Weak can no longer upgrade).

Source

pub fn set_trash_backend(&mut self, backend: Option<Arc<dyn TrashBackend>>)

Replace or remove the trash backend used by rm and kaish-trash.

The kernel installs the OS trash (SystemTrash) automatically when built with the os-integration feature. Embedders and tests can swap in a custom crate::trash::TrashBackend, or pass None to remove it — with trash enabled but no backend present, rm fails loud rather than falling through to permanent delete.

Source

pub fn cancel(&self)

Cancel the current execution.

This cancels the current cancellation token, causing any execution loop to exit at the next checkpoint with exit code 130 (SIGINT). A fresh token is installed for the next execute() call.

Source

pub fn is_cancelled(&self) -> bool

Check if the current execution has been cancelled.

Also the polling point for ExecuteOptions::interrupt: when the embedder’s check reports true, the internal token fires here, so every call site of this method is an interrupt checkpoint for free.

Source

pub async fn execute(&self, input: &str) -> Result<ExecResult, KernelError>

Execute kaish source code with default options.

Equivalent to execute_with_options(input, ExecuteOptions::default()). Returns the result of the last statement executed.

§Errors

Returns KernelError when the program was rejected before running (a lex/parse failure or a validator rejection) or faulted while running. See KernelError::is_rejected to route on that distinction. A nonzero exit from the script itself — a failed command, set -e — is not an Err; it comes back as Ok with the exit code folded into the returned ExecResult.

Source

pub async fn execute_argv( &self, name: &str, argv: &[Value], ) -> Result<ExecResult, KernelError>

Argv-native peer of Self::execute — run one command whose arguments are already tokenized.

execute(&str) is string-native: it lexes and parses its input. A caller that already holds OS/structured argv (a busybox-style multicall binary, a structured embedder like kaijutsu) would otherwise have to re-quote argv into a string just to have the lexer split it apart again — a round-trip that is lossy for typed values, since to_argv() stringifies Value::Bytes/Value::Json. execute_argv skips it.

Tokens are literal. No glob expansion, no $VAR interpolation, no command substitution, no word splitting — the “single-quoted word” semantics taken to its end. execute_argv("echo", &[Value::String("*.txt" .into())]) emits *.txt; it does not glob. (One shared-binder expansion does still apply, for consistency with the string door: a leading ~ is expanded against the session HOME — kaish expands ~ uniformly, so the two doors agree. Pass a pre-resolved path if you need it byte-literal.) A non-string Value (Bytes/Json/Int) lands directly in ToolArgs.positional, so typed data survives without a to_argv() round-trip. (Caveat: the two-layer clap arg model means a builtin that re-parses its own to_argv() still sees a stringified value; the typed-passthrough win fully lands only for builtins that read args.positional directly — the documented pattern.)

This is a peer, not a subset: a command string can carry pipelines, &&/||, control flow and $() that have no argv encoding, so the two doors converge late (at the shared dispatch chain) rather than one wrapping the other. From argv classification onward execute_argv reuses the exact path a Stmt::Command takes — command resolution (aliases, user tools, .kai scripts, externals, backend tools), arg binding, and the --json transform — so an ls --json still applies output formatting. The kernel’s pre-execution syntax validator does not run: argv has no shell syntax to validate (a tool’s own validate()/clap parse still runs at dispatch).

Concurrent callers serialize on the same execute lock as Self::execute, and the kernel’s configured request_timeout applies (a hung builtin or external is interrupted at the deadline with exit code 124, the same as the string door). There is no per-call options surface yet — a future execute_argv_with_options would carry per-call timeout/cancel/vars/cwd.

§Errors

Returns KernelError, always KernelError::Execution — argv has no shell syntax to reject, so execute_argv never returns KernelError::Parse or KernelError::Validation (a tool’s own validate()/clap parse at dispatch still surfaces here, as an execution failure).

Source

pub async fn execute_with_options( &self, input: &str, opts: ExecuteOptions, ) -> Result<ExecResult, KernelError>

Execute with per-call options. The primary entry point for embedders that don’t need per-statement output streaming.

opts carries timeout, transient vars overlay, optional cwd override, and optional embedder-owned cancellation token. See ExecuteOptions for semantics. For streaming, use Self::execute_with_options_streaming.

Cancellation: if opts.cancel_token is Some, it is raced against the kernel’s internal token. Either firing cancels and kills external children. The embedder’s token is read-only — kernel timeouts do NOT propagate into it. Distinguish via the returned code: 124 = timeout, 130 = cancellation.

Timeout: opts.timeout overrides KernelConfig::request_timeout. Some(Duration::ZERO) returns 124 immediately without spawning.

Concurrent callers on the same Kernel serialize on the kernel-wide execute lock. For true parallelism, call Kernel::fork (detached) or Kernel::fork_attached (cancellation cascades from this kernel).

§Errors

Returns KernelError when the program was rejected before running or faulted while running — see KernelError::is_rejected.

Source

pub async fn execute_with_options_streaming( &self, input: &str, opts: ExecuteOptions, on_output: &mut (dyn FnMut(&ExecResult) + Send), ) -> Result<ExecResult, KernelError>

Same as Self::execute_with_options but with a per-statement output callback. The callback fires after each top-level statement so the embedder (REPL, MCP streaming) can flush output incrementally.

§Errors

See Self::execute_with_options.

Source

pub async fn execute_with_pipe_stdin( &self, input: &str, opts: ExecuteOptions, pipe_stdin: PipeReader, ) -> Result<ExecResult, KernelError>

Execute with a lazy standard input fed as a PipeReader.

Unlike ExecuteOptions::with_stdin (a pre-read buffer), this never forces the input to be drained before execution: the reader seeds the first top-level command’s pipe_stdin, and a command that does not read stdin (echo) returns without touching it. This is the seam a non-interactive frontend uses to forward an open process stdin without hanging on a pipe that never sends EOF (sleep 10 | kaish -c 'echo hi').

Embedders that already hold a complete buffer (text or binary) should prefer the simpler ExecuteOptions::with_stdin path instead.

§Errors

See Self::execute_with_options.

Source

pub async fn execute_with_pipe_stdin_streaming( &self, input: &str, opts: ExecuteOptions, pipe_stdin: PipeReader, on_output: &mut (dyn FnMut(&ExecResult) + Send), ) -> Result<ExecResult, KernelError>

Streaming counterpart to Self::execute_with_pipe_stdin — the REPL -c/script frontend uses this to print output incrementally while feeding a lazy process-stdin pipe.

§Errors

See Self::execute_with_options.

Source

pub async fn execute_with_vars( &self, input: &str, vars: HashMap<String, Value>, ) -> Result<ExecResult, KernelError>

👎Deprecated:

use Kernel::execute_with_options with ExecuteOptions::with_vars

Execute kaish source code with a transient overlay of exported variables.

Deprecated thin wrapper over Self::execute_with_options. New code should use that method directly: execute_with_options(input, ExecuteOptions::new().with_vars(vars)).

Source

pub async fn execute_streaming( &self, input: &str, on_output: &mut (dyn FnMut(&ExecResult) + Send), ) -> Result<ExecResult, KernelError>

👎Deprecated:

use Kernel::execute_with_options_streaming

Execute kaish source code with a per-statement callback.

Deprecated thin wrapper. New code should use Self::execute_with_options_streaming.

Source

pub async fn get_var(&self, name: &str) -> Option<Value>

Get a variable value.

Source

pub async fn set_var(&self, name: &str, value: Value)

Set a variable value.

Source

pub async fn set_positional( &self, script_name: impl Into<String>, args: Vec<String>, )

Set positional parameters ($0 script name and $1-$9 args).

Source

pub async fn list_vars(&self) -> Vec<(String, Value)>

List all variables.

Source

pub async fn exported_vars(&self) -> Vec<(String, Value)>

List exported variables (name, value), sorted by name. These are the vars a child process would see (see dispatch’s hermetic env build).

Source

pub async fn cwd(&self) -> PathBuf

Get current working directory.

Source

pub async fn set_cwd(&self, path: PathBuf)

Set current working directory.

Source

pub async fn try_set_cwd(&self, path: PathBuf) -> bool

Set the working directory only if path resolves to a directory in the kernel’s backend — the same namespace cd validates against. Unlike a raw host-FS is_dir() check, this correctly accepts virtual mounts (/v/docs, in-memory scratch, …) and rejects real paths that have since disappeared. Returns whether the cwd was changed.

Source

pub async fn last_result(&self) -> ExecResult

Get the last result ($?).

Source

pub async fn has_function(&self, name: &str) -> bool

Check if a user-defined function exists.

Source

pub fn tool_schemas(&self) -> Vec<ToolSchema>

Get available tool schemas.

Source

pub async fn classify_command(&self, name: &str) -> CommandKind

Classify how the kernel will resolve a command name.

This is the supported, single source of truth for command resolution that embedders should call instead of re-deriving the rules. Walk a parsed script (kaish_kernel::parser::parseStmt::Command nodes) and call this per command name to bucket each into builtin / user-function / special-form / dynamic / external — for example a consent gate that blocks a script until external commands are approved.

The classification mirrors the interpreter’s real resolution order (execute_command_depth): special-forms (true/false/source/.) short-circuit first, then aliases are expanded (bounded recursion, re-checking special-forms each step, exactly as execution does), then user functions (which shadow builtins), then builtins, then a PATH lookup. A name that is a variable or command-substitution expansion ($cmd, $(pick), ${x}) classifies as CommandKind::Dynamic because it can’t be resolved statically.

Aliases are resolved against the kernel’s current alias table, so an alias cat=/bin/something makes cat classify as External — the same thing it would actually run. The safe direction of any residual imprecision is External/Dynamic, never a false “internal”: the /v/bin/ prefix and .kai/backend-tool resolution are reported External even though some of those resolve in-process, so a consent gate over-gates rather than letting a PATH escape slip through.

Source

pub fn jobs(&self) -> Arc<JobManager>

Get job manager.

Source

pub fn vfs(&self) -> Arc<VfsRouter>

Get VFS router.

Source

pub async fn reset(&self) -> Result<()>

Reset kernel to initial state.

Clears in-memory variables and resets cwd to root. History is not cleared (it persists across resets). The kernel’s $$ identity, the trash-on-delete configuration, the current errexit state, and any frontend-seeded initial_vars (HOME/PATH/etc, from KernelConfig) are re-applied to the fresh scope rather than silently reverting to defaults — an embedder that opted into trash or errexit must not find either quietly disabled after a reset() between requests.

Background jobs are untouched (GH #245) — reset() is a scope/cwd reset, not a session boundary for &. A job started before reset() keeps running, stays in jobs, and the job ID counter keeps counting up. An embedder treating reset() as “new session” (a fresh MCP conversation reusing one kernel, say) inherits every job the previous conversation backgrounded — call Self::cancel_all_jobs first if that inheritance is not wanted.

Source

pub async fn cancel_all_jobs(&self) -> usize

Trip the cancellation token of every tracked background job (&) — whether or not shutdown follows.

This is the same lever kill %N uses: a running job’s in-process future exits at its next checkpoint, and any external children it spawned get the SIGTERM→SIGKILL cascade; it then stays tracked with status Killed once it unwinds. For an already-finished job the token trip is a no-op — its future has already resolved and the job keeps reporting its terminal status. This only starts cancellation, it does not wait (pair with JobManager::wait/wait_all if the caller needs to block on the unwind, bounded as Self::shutdown does).

A job registered by an embedder via JobManager::register with no cancel token attached has no lever to cancel — silently skipped here, same as kill %N’s own “no cancellation token” case.

Returns how many jobs a token was actually tripped for.

Source

pub async fn shutdown(&self) -> Result<()>

Shut down the kernel.

Cancels every tracked background job (Self::cancel_all_jobs), then waits up to kill_grace + 3s per job — the same bound kill %N gives a single target (GH #244) — for it to actually unwind. The waits are sequential, so the worst case is additive: N jobs that all ignore cancellation block shutdown for N × (kill_grace + 3s). Jobs that unwind promptly (the normal case) cost only their own unwind time. Before this fix shutdown called wait_all() with no timeout at all: sleep 3600 & then shutdown() blocked for an hour (GH #245).

A job that has not unwound by its deadline is abandoned: logged via tracing::warn! and left running detached until the tokio runtime itself goes away. There is no further lever once shutdown() has returned — this method does not hang, but it also does not guarantee every job actually stopped.

Takes &self, not owned self — an embedder holding Arc<Kernel> (e.g. kaish-client’s EmbeddedClient) can call this without Arc::try_unwrap, since the work here only touches the shared Arc<JobManager>, never kernel state that would need exclusive ownership.

Trait Implementations§

Source§

impl CommandDispatcher for Kernel

Source§

fn dispatch<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, cmd: &'life1 Command, ctx: &'life2 mut ExecContext, ) -> Pin<Box<dyn Future<Output = Result<ExecResult>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait,

Dispatch a command through the Kernel’s full resolution chain.

This is the single path for all command execution when called from the pipeline runner. It provides the full dispatch chain: user tools → builtins → .kai scripts → external commands → backend tools.

Source§

fn dispatch_stmt<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, stmt: &'life1 Stmt, ctx: &'life2 mut ExecContext, ) -> Pin<Box<dyn Future<Output = Result<ExecResult>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait,

Run a compound pipeline stage through the kernel’s statement executor.

Source§

fn eval_expr<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, expr: &'life1 Expr, _ctx: &'life2 ExecContext, ) -> Pin<Box<dyn Future<Output = Result<Value>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait,

Evaluate an expression through the kernel’s async chain, including command substitution. Delegates to eval_expr_async, which snapshots the kernel’s scope/cwd and restores them after any $(...) runs, so only command output escapes. The ctx is unused here because the kernel evaluates against its own session state (a fork carries the pipeline stage’s snapshot); var refs resolve against that scope.

Source§

fn fork<'life0, 'async_trait>( &'life0 self, ) -> Pin<Box<dyn Future<Output = Arc<dyn CommandDispatcher>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait,

Produce a forked dispatcher with independent mutable state (detached).

Calls the inherent Kernel::fork method (note the UFCS to avoid recursing into the trait method we’re defining) and coerces the returned Arc<Kernel> to Arc<dyn CommandDispatcher>.

Source§

fn fork_attached<'life0, 'async_trait>( &'life0 self, ) -> Pin<Box<dyn Future<Output = Arc<dyn CommandDispatcher>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait,

Produce a forked dispatcher with cancellation cascading from this kernel.

Auto Trait Implementations§

§

impl !Freeze for Kernel

§

impl !RefUnwindSafe for Kernel

§

impl !UnwindSafe for Kernel

§

impl Send for Kernel

§

impl Sync for Kernel

§

impl Unpin for Kernel

§

impl UnsafeUnpin for Kernel

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> FutureExt for T

Source§

fn with_context(self, otel_cx: Context) -> WithContext<Self>

Attaches the provided Context to this type, returning a WithContext wrapper. Read more
Source§

fn with_current_context(self) -> WithContext<Self>

Attaches the current Context to this type, returning a WithContext wrapper. Read more
Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<'src, T> IntoMaybe<'src, T> for T
where T: 'src,

Source§

type Proj<U: 'src> = U

Source§

fn map_maybe<R>( self, _f: impl FnOnce(&'src T) -> &'src R, g: impl FnOnce(T) -> R, ) -> <T as IntoMaybe<'src, T>>::Proj<R>
where R: 'src,

Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, S> SpanWrap<S> for T
where S: WrappingSpan<T>,

Source§

fn with_span(self, span: S) -> <S as WrappingSpan<Self>>::Spanned

Invokes WrappingSpan::make_wrapped to wrap an AST node in a span.
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more