Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,39 @@ breaking entries are marked **BREAKING**.

## [Unreleased]

### Fixed

- `grep` now reserves exit 1 for "no lines matched". An invalid pattern, an
unreadable file, or a missing pattern argument exits 2, so a caller cannot
read a broken search as a negative answer. `diff` argument errors exit 2 to
match its neighboring operand checks.
- A program kaish refuses — lex, parse, or validation — now exits 2 from
`kaish -c` and from a script file, matching what `kaish --plan` already
documented for the same source.
- `kaish --plan` now runs the validator. A program that parses but the kernel
would reject reports `{"errors": [...]}` and exits 2 instead of printing a
clean plan the caller cannot run.
- An invalid regex names the escape that fixes it (`\[` for a literal `[`,
`[(]` and `[{]` where a backslash would be a BRE operator) instead of
linking kaish's regex crate. A pattern with two faults gets no hint rather
than one that still does not compile.
- A pattern that arrives through a variable (`p='[cast:'; grep "$p" f`) now
exits 2 like a literal one. The validator skips a computed pattern, so the
failure surfaced from the regex builders inside `grep` instead.
- `grep` reading from a pipe no longer discards a read error and reports it as
"no lines matched". A read failure exits 2; a downstream close still keeps
the match-based code.

### Changed

- **BREAKING** (`kaish-tool-api`): `ToolCtx` is sealed. Tool authors receive a
`ToolCtx` and never implement one, so this changes no supported use, but an
out-of-tree implementation no longer compiles.
- A kernel builtin dispatched with a context that is not the kernel's now
panics instead of returning exit 1 with an internal message. Sealing
`ToolCtx` is what makes that branch unreachable: `ToolRegistry::get` and
`Tool::execute` are public, so type privacy alone left it open.

## [0.17.2] - 2026-09-09

### Fixed
Expand Down
10 changes: 3 additions & 7 deletions crates/kaish-kernel/src/tools/builtin/alias.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use clap::{CommandFactory, Parser};

use crate::ast::Value;
use crate::interpreter::{ExecResult, OutputData, OutputNode};
use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema};
use crate::tools::{exec_context, schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema};

/// Alias tool: define, list, or show command aliases.
///
Expand Down Expand Up @@ -45,9 +45,7 @@ impl Tool for Alias {
}

async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult {
let Some(ctx) = ctx.as_any_mut().downcast_mut::<ExecContext>() else {
return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext");
};
let ctx = exec_context(ctx);
// For alias, args.named carries user-defined name=value pairs that
// clap can't know about. Synthesise an argv with just flags so clap
// sees only the global --json; we read name=value off args.named below.
Expand Down Expand Up @@ -172,9 +170,7 @@ impl Tool for Unalias {
}

async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult {
let Some(ctx) = ctx.as_any_mut().downcast_mut::<ExecContext>() else {
return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext");
};
let ctx = exec_context(ctx);
let argv = match args.to_argv() {
Ok(v) => v,
Err(e) => return ExecResult::failure(2, format!("unalias: {e}")),
Expand Down
7 changes: 3 additions & 4 deletions crates/kaish-kernel/src/tools/builtin/assert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use clap::{CommandFactory, Parser};

use crate::ast::Value;
use crate::interpreter::ExecResult;
use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema};
use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema};

/// Assert tool: verify conditions in tests.
pub struct Assert;
Expand Down Expand Up @@ -40,9 +40,7 @@ impl Tool for Assert {
}

async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult {
let Some(ctx) = ctx.as_any_mut().downcast_mut::<ExecContext>() else {
return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext");
};
let ctx = exec_context(ctx);
let argv = match args.to_argv() {
Ok(v) => v,
Err(e) => return ExecResult::failure(2, format!("assert: {e}")),
Expand Down Expand Up @@ -95,6 +93,7 @@ fn is_truthy(value: &Value) -> bool {
#[cfg(test)]
mod tests {
use super::*;
use crate::tools::ExecContext;
use crate::vfs::{MemoryFs, VfsRouter};
use std::sync::Arc;

Expand Down
7 changes: 3 additions & 4 deletions crates/kaish-kernel/src/tools/builtin/awk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use crate::interpreter::{ExecResult, OutputData};
use crate::tools::builtin::get_path_string;
use crate::tools::builtin::read_repeatable_strings;
use crate::tools::builtin::regex_dialect::{append_dialect_hint, bre_metas_to_ere};
use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema};
use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema};

/// Compile an awk ERE pattern, first rewriting the GNU BRE backslash-metas to
/// ERE so `\|`/`\(…\)`/`\{N\}` behave as operators (issue #60). awk is ERE-only
Expand Down Expand Up @@ -83,9 +83,7 @@ impl Tool for Awk {
}

async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult {
let Some(ctx) = ctx.as_any_mut().downcast_mut::<ExecContext>() else {
return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext");
};
let ctx = exec_context(ctx);
let argv = match args.to_argv() {
Ok(v) => v,
Err(e) => return ExecResult::failure(2, format!("awk: {e}")),
Expand Down Expand Up @@ -2856,6 +2854,7 @@ impl AwkRuntime {
#[cfg(test)]
mod tests {
use super::*;
use crate::tools::ExecContext;
use crate::vfs::{Filesystem, MemoryFs, VfsRouter};
use std::sync::Arc;

Expand Down
7 changes: 3 additions & 4 deletions crates/kaish-kernel/src/tools/builtin/base64_tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use base64::Engine;

use crate::ast::Value;
use crate::interpreter::{ExecResult, OutputData};
use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema};
use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema};

/// Base64 tool: encode or decode base64 data.
pub struct Base64Tool;
Expand Down Expand Up @@ -53,9 +53,7 @@ impl Tool for Base64Tool {
}

async fn execute(&self, mut args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult {
let Some(ctx) = ctx.as_any_mut().downcast_mut::<ExecContext>() else {
return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext");
};
let ctx = exec_context(ctx);
// Tests poke `args.named.insert("decode", Value::Bool(true))` directly;
// to_argv would render that as `--decode=true` which clap won't accept
// for a bool field. Promote any Bool-typed named entries to flags so
Expand Down Expand Up @@ -150,6 +148,7 @@ fn wrap_lines(s: &str, width: usize) -> String {
#[cfg(test)]
mod tests {
use super::*;
use crate::tools::ExecContext;
use crate::vfs::{Filesystem, MemoryFs, VfsRouter};
use std::sync::Arc;

Expand Down
7 changes: 3 additions & 4 deletions crates/kaish-kernel/src/tools/builtin/basename.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use std::path::Path;
use crate::ast::Value;
use crate::interpreter::{ExecResult, OutputData};
use crate::tools::builtin::get_path_string;
use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema};
use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema};

/// Basename tool: extract filename from path.
pub struct Basename;
Expand Down Expand Up @@ -43,9 +43,7 @@ impl Tool for Basename {
}

async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult {
let Some(ctx) = ctx.as_any_mut().downcast_mut::<ExecContext>() else {
return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext");
};
let ctx = exec_context(ctx);
let argv = match args.to_argv() {
Ok(v) => v,
Err(e) => return ExecResult::failure(2, format!("basename: {e}")),
Expand Down Expand Up @@ -91,6 +89,7 @@ impl Tool for Basename {
#[cfg(test)]
mod tests {
use super::*;
use crate::tools::ExecContext;
use crate::vfs::{MemoryFs, VfsRouter};
use std::sync::Arc;

Expand Down
7 changes: 3 additions & 4 deletions crates/kaish-kernel/src/tools/builtin/bg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use crate::interpreter::ExecResult;
use crate::interpreter::OutputData;
#[cfg(unix)]
use crate::scheduler::JobId;
use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema};
use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema};

/// Bg tool: resume a stopped job in the background.
pub struct Bg;
Expand Down Expand Up @@ -55,9 +55,7 @@ impl Tool for Bg {
}

async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult {
let Some(ctx) = ctx.as_any_mut().downcast_mut::<ExecContext>() else {
return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext");
};
let ctx = exec_context(ctx);
let argv = match args.to_argv() {
Ok(v) => v,
Err(e) => return ExecResult::failure(2, format!("bg: {e}")),
Expand Down Expand Up @@ -189,6 +187,7 @@ impl Tool for Bg {
#[cfg(test)]
mod tests {
use super::*;
use crate::tools::ExecContext;
use crate::scheduler::JobManager;
use crate::vfs::{MemoryFs, VfsRouter};
use std::os::unix::process::CommandExt;
Expand Down
6 changes: 2 additions & 4 deletions crates/kaish-kernel/src/tools/builtin/cat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::ast::Value;
use crate::backend::ReadRange;
use crate::interpreter::{ExecResult, OutputData};
use crate::scheduler::PipeWriter;
use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema};
use crate::tools::{exec_context, schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema};

/// Cat tool: read and output file contents.
pub struct Cat;
Expand Down Expand Up @@ -49,9 +49,7 @@ impl Tool for Cat {
}

async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult {
let Some(ctx) = ctx.as_any_mut().downcast_mut::<ExecContext>() else {
return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext");
};
let ctx = exec_context(ctx);
let argv = match args.to_argv() {
Ok(v) => v,
Err(e) => return ExecResult::failure(2, format!("cat: {e}")),
Expand Down
7 changes: 3 additions & 4 deletions crates/kaish-kernel/src/tools/builtin/cd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use std::path::{Path, PathBuf};
use crate::ast::Value;
use crate::interpreter::{ExecResult, OutputData};
use crate::tools::builtin::get_path_string;
use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema};
use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema};

/// Cd tool: change current working directory.
pub struct Cd;
Expand Down Expand Up @@ -44,9 +44,7 @@ impl Tool for Cd {
}

async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult {
let Some(ctx) = ctx.as_any_mut().downcast_mut::<ExecContext>() else {
return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext");
};
let ctx = exec_context(ctx);
let argv = match args.to_argv() {
Ok(v) => v,
Err(e) => return ExecResult::failure(2, format!("cd: {e}")),
Expand Down Expand Up @@ -113,6 +111,7 @@ impl Tool for Cd {
#[cfg(test)]
mod tests {
use super::*;
use crate::tools::ExecContext;
use crate::vfs::{Filesystem, MemoryFs, VfsRouter};
use std::path::PathBuf;
use std::sync::Arc;
Expand Down
6 changes: 2 additions & 4 deletions crates/kaish-kernel/src/tools/builtin/checksum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use digest::Digest;

use crate::interpreter::{ExecResult, OutputData, OutputNode};
use crate::tools::builtin::get_path_string;
use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema};
use crate::tools::{exec_context, schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema};

/// Checksum tool: compute or verify file hashes.
pub struct Checksum;
Expand Down Expand Up @@ -65,9 +65,7 @@ impl Tool for Checksum {
}

async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult {
let Some(ctx) = ctx.as_any_mut().downcast_mut::<ExecContext>() else {
return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext");
};
let ctx = exec_context(ctx);
let argv = match args.to_argv() {
Ok(v) => v,
Err(e) => return ExecResult::failure(2, format!("checksum: {e}")),
Expand Down
6 changes: 2 additions & 4 deletions crates/kaish-kernel/src/tools/builtin/cmp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use std::path::Path;

use crate::backend::ReadRange;
use crate::interpreter::ExecResult;
use crate::tools::{schema_from_clap, ExecContext, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema};
use crate::tools::{exec_context, schema_from_clap, ExecContext, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema};

/// cmp tool.
pub struct Cmp;
Expand Down Expand Up @@ -51,9 +51,7 @@ impl Tool for Cmp {
}

async fn execute(&self, mut args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult {
let Some(ctx) = ctx.as_any_mut().downcast_mut::<ExecContext>() else {
return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext");
};
let ctx = exec_context(ctx);
args.flagify_bool_named(&self.schema());
let argv = match args.to_argv() {
Ok(v) => v,
Expand Down
7 changes: 3 additions & 4 deletions crates/kaish-kernel/src/tools/builtin/cp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use std::path::{Path, PathBuf};
use crate::backend::{BackendError, KernelBackend, WriteMode};
use crate::interpreter::ExecResult;
use crate::operation::KernelOperation;
use crate::tools::{cas_overwrite, schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema};
use crate::tools::{exec_context, cas_overwrite, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema};

/// Cp tool: copy files and directories.
pub struct Cp;
Expand Down Expand Up @@ -62,9 +62,7 @@ impl Tool for Cp {
}

async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult {
let Some(ctx) = ctx.as_any_mut().downcast_mut::<ExecContext>() else {
return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext");
};
let ctx = exec_context(ctx);
let argv = match args.to_argv() {
Ok(v) => v,
Err(e) => return ExecResult::failure(2, format!("cp: {e}")),
Expand Down Expand Up @@ -389,6 +387,7 @@ fn copy_dir_recursive<'a>(
#[cfg(test)]
mod tests {
use super::*;
use crate::tools::ExecContext;
use crate::ast::Value;
use crate::vfs::{Filesystem, MemoryFs, VfsRouter};
use std::sync::Arc;
Expand Down
7 changes: 3 additions & 4 deletions crates/kaish-kernel/src/tools/builtin/cut.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::path::Path;

use crate::ast::Value;
use crate::interpreter::ExecResult;
use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema};
use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema};

/// Cut tool: select portions of each line.
pub struct Cut;
Expand Down Expand Up @@ -60,9 +60,7 @@ impl Tool for Cut {
}

async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult {
let Some(ctx) = ctx.as_any_mut().downcast_mut::<ExecContext>() else {
return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext");
};
let ctx = exec_context(ctx);
let argv = match args.to_argv() {
Ok(v) => v,
Err(e) => return ExecResult::failure(2, format!("cut: {e}")),
Expand Down Expand Up @@ -228,6 +226,7 @@ fn select_indices(spec: &str, max_len: usize) -> Vec<usize> {
#[cfg(test)]
mod tests {
use super::*;
use crate::tools::ExecContext;
use crate::vfs::{Filesystem, MemoryFs, VfsRouter};
use std::sync::Arc;

Expand Down
6 changes: 2 additions & 4 deletions crates/kaish-kernel/src/tools/builtin/date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ use chrono_tz::Tz;
use clap::{CommandFactory, Parser};

use crate::interpreter::{value_to_string, ExecResult, OutputData};
use crate::tools::{
use crate::tools::{exec_context,
schema_from_clap, ExecContext, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema,
};

Expand Down Expand Up @@ -185,9 +185,7 @@ impl Tool for Date {
}

async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult {
let Some(ctx) = ctx.as_any_mut().downcast_mut::<ExecContext>() else {
return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext");
};
let ctx = exec_context(ctx);
let argv = match args.to_argv() {
Ok(v) => v,
Err(e) => return ExecResult::failure(2, format!("date: {e}")),
Expand Down
Loading