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
4 changes: 2 additions & 2 deletions crates/kindling/src/commands/capsule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ pub fn run_open(args: CapsuleOpenArgs, via_daemon: bool) -> CliResult {
let scope = scope_from(args.session.as_deref(), args.repo.as_deref());

let capsule = if via_daemon {
let client = build_client()?;
let client = build_client(args.common.db.as_deref())?;
runtime()?.block_on(async {
client
.open_capsule(kind, args.intent.clone(), scope, None)
Expand Down Expand Up @@ -78,7 +78,7 @@ pub fn run_open(args: CapsuleOpenArgs, via_daemon: bool) -> CliResult {

pub fn run_close(args: CapsuleCloseArgs, via_daemon: bool) -> CliResult {
let capsule = if via_daemon {
let client = build_client()?;
let client = build_client(args.common.db.as_deref())?;
let body = match &args.summary {
Some(content) => CloseCapsuleBody {
generate_summary: Some(true),
Expand Down
2 changes: 1 addition & 1 deletion crates/kindling/src/commands/forget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use crate::{build_client, open_service, runtime, CliResult};

pub fn run(args: ForgetArgs, via_daemon: bool) -> CliResult {
if via_daemon {
let client = build_client()?;
let client = build_client(args.common.db.as_deref())?;
runtime()?.block_on(async { client.forget(&args.id).await })?;
} else {
let (service, _db) = open_service(args.common.db.as_deref())?;
Expand Down
2 changes: 1 addition & 1 deletion crates/kindling/src/commands/log.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ pub fn run(args: LogArgs, via_daemon: bool) -> CliResult {
};

let observation = if via_daemon {
let client = build_client()?;
let client = build_client(args.common.db.as_deref())?;
runtime()?
.block_on(async {
client
Expand Down
4 changes: 2 additions & 2 deletions crates/kindling/src/commands/pin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ pub fn run_pin(args: PinArgs, via_daemon: bool) -> CliResult {
let target_type = parse_target_type(&args.target_type)?;

let pin = if via_daemon {
let client = build_client()?;
let client = build_client(args.common.db.as_deref())?;
let body = CreatePinBody {
target_type,
target_id: args.id.clone(),
Expand Down Expand Up @@ -75,7 +75,7 @@ pub fn run_pin(args: PinArgs, via_daemon: bool) -> CliResult {

pub fn run_unpin(args: UnpinArgs, via_daemon: bool) -> CliResult {
if via_daemon {
let client = build_client()?;
let client = build_client(args.common.db.as_deref())?;
runtime()?.block_on(async { client.unpin(&args.id).await })?;
} else {
let (service, _db) = open_service(args.common.db.as_deref())?;
Expand Down
2 changes: 1 addition & 1 deletion crates/kindling/src/commands/search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ pub fn run(args: SearchArgs, via_daemon: bool) -> CliResult {
};

let result = if via_daemon {
let client = build_client()?;
let client = build_client(args.common.db.as_deref())?;
runtime()?.block_on(async { client.retrieve(options).await })?
} else {
let (service, _db) = open_service(args.common.db.as_deref())?;
Expand Down
34 changes: 28 additions & 6 deletions crates/kindling/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,13 +181,35 @@ fn dispatch(cli: Cli) -> CliResult {
}
}

/// Build a daemon client honouring `--db` as a socket-routing project hint.
/// Build a daemon client, rejecting an explicit `--db` that cannot be honoured.
///
/// The client routes by project root (hashed into a per-project DB by the
/// daemon), not by an explicit DB file, so `--via-daemon` + `--db` cannot point
/// the daemon at an arbitrary file. We use the default socket + the current
/// working directory as the project root (mirroring `ClientConfig::defaults`).
pub(crate) fn build_client() -> Result<kindling_client::Client, CliError> {
/// The daemon routes by project *root* (a string it hashes into a per-project DB
/// via `kindling_store::project_db_path`), carried in the `X-Kindling-Project`
/// header — it never accepts an arbitrary DB *file* path. `--db` is an explicit
/// DB file path, meaningful only in embedded mode (via [`resolve_db_path`]).
/// There is therefore no faithful way to point the daemon at a `--db` file.
///
/// Rather than silently ignore `--db` and route by the current working directory
/// (which could read/write a *different* project's database than the one named),
/// we reject the combination: when `explicit_db` is `Some`, return a clear
/// [`CliError::Invalid`] telling the user to drop `--db` (use the per-project
/// default) or drop `--via-daemon` (embedded mode honours `--db`). When
/// `explicit_db` is `None`, behave as before — [`kindling_client::Client::new`]
/// routes by the current working directory (mirroring `ClientConfig::defaults`).
///
/// All daemon-backed verbs (log, capsule open/close, search, pin, unpin, forget)
/// build their client through here, so the rejection covers every daemon path.
pub(crate) fn build_client(explicit_db: Option<&str>) -> Result<kindling_client::Client, CliError> {
if explicit_db.is_some() {
return Err(CliError::Invalid(
"--db cannot be combined with --via-daemon: the daemon routes per \
project root (hashed into a per-project database), not by an explicit \
database file path, so --db cannot be honoured over the daemon. \
Either drop --db to use the project default, or drop --via-daemon to \
run embedded (which honours --db)."
.to_string(),
));
}
Ok(kindling_client::Client::new()?)
}

Expand Down
51 changes: 51 additions & 0 deletions crates/kindling/tests/daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,57 @@ async fn via_daemon_log_roundtrip() {
handle.abort();
}

/// `--via-daemon <cmd> --db <path>` is rejected for every daemon-backed verb.
///
/// The daemon routes per project root (hashed into a per-project DB), not by an
/// explicit DB file, so honouring `--db` over the daemon is impossible. Rather
/// than silently mis-route to the cwd's project DB, the CLI rejects the
/// combination with a clear, non-zero-exit error mentioning both flags. This
/// rejection happens in `build_client` before any socket connection, so no
/// running daemon is required.
#[test]
fn via_daemon_with_db_is_rejected() {
let bin = env!("CARGO_BIN_EXE_kindling");
let db = tempfile::tempdir().unwrap().path().join("explicit.db");
let db = db.to_string_lossy().into_owned();

// One representative invocation per daemon-backed verb. Each must fail
// cleanly (non-zero) with an error naming both `--db` and `--via-daemon`,
// and must NOT attempt to spawn/contact a daemon.
let invocations: Vec<Vec<&str>> = vec![
vec!["--via-daemon", "log", "hello", "--db", &db],
vec![
"--via-daemon",
"capsule",
"open",
"--intent",
"x",
"--db",
&db,
],
vec!["--via-daemon", "capsule", "close", "cap_1", "--db", &db],
vec!["--via-daemon", "search", "query", "--db", &db],
vec!["--via-daemon", "pin", "observation", "obs_1", "--db", &db],
vec!["--via-daemon", "unpin", "pin_1", "--db", &db],
vec!["--via-daemon", "forget", "obs_1", "--db", &db],
];

for args in invocations {
let out = Command::new(bin).args(&args).output().expect("run cli");
assert!(
!out.status.success(),
"expected non-zero exit for {args:?}, got success\nstdout: {}\nstderr: {}",
stdout(&out),
stderr(&out)
);
let err = stderr(&out);
assert!(
err.contains("--db") && err.contains("--via-daemon"),
"error for {args:?} should mention both --db and --via-daemon, got: {err}"
);
}
}

/// Wait for a child to exit within `timeout`, returning its status (or `None`
/// on timeout, after killing it).
fn wait_with_timeout(
Expand Down
Loading