#[cfg(not(target_family = "wasm"))]
mod clipboard;
#[cfg(feature = "quic")]
mod quic;
mod repl;
#[cfg(feature = "web")]
mod route_load;
#[cfg(not(target_family = "wasm"))]
mod tui;
const USAGE: &str = "\
ikigai — resource-resolution REPL
usage:
ikigai start the interactive REPL (full-screen on a terminal)
ikigai --plain force the line REPL (also used automatically when piped)
ikigai --demo mount the interactive runbook (urn:runbook:*); off by default
ikigai --connect [<target>] attach the REPL to a kernel server (a Unix path, or quic://host:port)
ikigai --mount <pfx>=<tgt> compose a remote kernel at prefix <pfx> (<tgt> = Unix path or quic://host:port)
ikigai serve [<target>] run a kernel server (a Unix socket path, or quic://addr to bind)
ikigai serve <q> --cap <s> serve under a fixed capability ceiling <s> every client is clamped to
(with clients.json below, the OUTER bound each grant narrows within)
ikigai serve <q> per-identity authority when ~/.config/ikigai/clients.json enrols
certificates — it maps a client's SHA-256 fingerprint to a grant
named in grants.json, so each gets its own scopes, an unenrolled
one is REFUSED, and editing the file revokes on the next
connection. `cert add-client` prints the fingerprint to enrol.
ikigai serve <q> --announce also advertise this kernel on the local network (mDNS), so clients
can mount it by name; `source urn:peer:list` shows who is out there
ikigai serve <s> --prefer … a served host may take --mount/--override/--prefer too, so IT owns
the topology: a local client reaches a peer through this socket
without holding its certs or finding it itself
ikigai serve … --code-signer <k> accept SIGNED programs (urn:lisp:run) vouched for by key
resource <k> (repeatable; --code-signers-dir sets where
urn:codekey:{file} reads from). Without it the door is unbound.
ikigai serve … --eval-timeout <s> wall-clock ceiling for a served eval (default 10s)
ikigai serve --http <port> serve the inbound HTTP face (loopback; front with TLS at your proxy)
[--trust-proxy: honor X-Forwarded-*; --cors-origin <o>: allow a CORS origin;
--routes <iri>: load routes from an RDF or plain-JSON resource
(a urn:file: route hot-reloads); --routes-only: un-routed → 404]
ikigai --daemon headless: timers, the watcher, and the standing sync — for launchd
ikigai --name <instance> name this instance (scopes <name>.* config properties; defaults
repl / daemon / serve by mode)
ikigai --mount <p>=<t> graft a remote namespace at prefix <p> (ALIAS: <p>rest → urn:rest
on the remote, tried after local; --cert-dir after it is ITS cert set)
ikigai --override <p>=<t> the SAME namespace, served remotely: IRIs forward unchanged and win
over local. <p> may be a whole IRI, so a single resource can be
rerouted; the most specific override wins
ikigai --prefer <p>=<t> like --override, but falls back to the LOCAL binding when the peer
is unreachable (transient failures only; denials still propagate)
<t> = peer:<name> find that peer on the local network (mDNS) instead of naming an
address; needs a pinned cert at <config>/ikigai/quic-<name>/
ikigai --react run the space reactor in this session — claims and executes tuples
dropped in the workspace. OFF by default; the daemon is the worker
ikigai cert generate create the pinned QUIC certificates (--dir <d> for a dedicated set)
ikigai cert add-client <n> mint an extra client identity into clients/<n>.{crt,key}
ikigai -c '<command>' ... run command(s) non-interactively, then exit
ikigai -e '<sexpr>' ... evaluate a Lisp s-expression (urn:lisp:eval), then exit
ikigai --load <uri> [--cap <s>] read a script resource and evaluate it as Lisp (--cap narrows first)
ikigai -h | --help show this help
QUIC: --server-cert/--server-key name the server's identity, --client-cert/--client-key the client's
inside the REPL: source, describe, help, quit (type `help` for details)";
use ikigai_engine::Engine;
#[derive(Default, Clone)]
struct Certs {
cert_dir: Option<String>,
server_cert: Option<String>,
server_key: Option<String>,
client_cert: Option<String>,
client_key: Option<String>,
}
enum Mode {
Repl(ReplArgs),
Daemon {
mounts: Vec<Mount>,
},
Serve {
target: Option<String>,
certs: Certs,
caps: Vec<String>,
http: Option<String>,
trust_proxy: bool,
cors_origins: Vec<String>,
routes: Option<String>,
routes_only: bool,
announce: bool,
mounts: Vec<Mount>,
},
Mcp {
grants: Vec<String>,
scopes: Vec<String>,
mounts: Vec<Mount>,
},
CertGenerate {
force: bool,
dir: Option<String>,
},
CertAddClient {
name: String,
cert_dir: Option<String>,
force: bool,
},
}
#[derive(Default, Clone)]
struct ReplArgs {
plain: bool,
demo: bool,
commands: Vec<String>,
connect: Option<Option<String>>,
mounts: Vec<Mount>,
certs: Certs,
react: bool,
}
#[derive(Clone)]
struct Mount {
prefix: String,
target: String,
certs: Certs,
kind: ikigai_embedded::MountKind,
}
fn is_quic(target: &str) -> bool {
target.starts_with("quic://")
}
fn cert_flag(
arg: &str,
argv: &mut impl Iterator<Item = String>,
certs: &mut Certs,
) -> Result<bool, String> {
let slot = match arg {
"--cert-dir" => &mut certs.cert_dir,
"--server-cert" => &mut certs.server_cert,
"--server-key" => &mut certs.server_key,
"--client-cert" => &mut certs.client_cert,
"--client-key" => &mut certs.client_key,
_ => return Ok(false),
};
*slot = Some(
argv.next()
.ok_or_else(|| format!("{arg} requires a path"))?,
);
Ok(true)
}
fn parse_args() -> Result<Option<Mode>, String> {
parse_argv(std::env::args().skip(1))
}
fn parse_argv(args: impl Iterator<Item = String>) -> Result<Option<Mode>, String> {
let mut argv = args.peekable();
if argv.peek().map(String::as_str) == Some("cert") {
argv.next();
return match argv.next().as_deref() {
Some("generate") => {
let mut force = false;
let mut dir = None;
while let Some(arg) = argv.next() {
match arg.as_str() {
"--force" => force = true,
"--dir" => {
dir = Some(
argv.next()
.ok_or_else(|| "--dir needs a path".to_string())?,
)
}
other => {
return Err(format!("unknown argument after `cert generate`: {other}"))
}
}
}
Ok(Some(Mode::CertGenerate { force, dir }))
}
Some("add-client") => {
let mut name = None;
let mut cert_dir = None;
let mut force = false;
while let Some(arg) = argv.next() {
match arg.as_str() {
"--force" => force = true,
"--cert-dir" => {
cert_dir = Some(
argv.next()
.ok_or_else(|| "--cert-dir needs a path".to_string())?,
)
}
other if other.starts_with('-') => {
return Err(format!(
"unknown argument after `cert add-client`: {other}"
))
}
_ if name.is_none() => name = Some(arg),
other => {
return Err(format!(
"unexpected argument after `cert add-client`: {other}"
))
}
}
}
let name =
name.ok_or_else(|| "usage: `ikigai cert add-client <name>`".to_string())?;
Ok(Some(Mode::CertAddClient {
name,
cert_dir,
force,
}))
}
Some(other) => Err(format!("unknown `cert` subcommand: {other}")),
None => {
Err("usage: `ikigai cert generate` | `ikigai cert add-client <name>`".to_string())
}
};
}
if argv.peek().map(String::as_str) == Some("serve") {
argv.next();
let mut target = None;
let mut certs = Certs::default();
let mut caps = Vec::new();
let mut code_signers: Vec<String> = Vec::new();
let mut http = None;
let mut trust_proxy = false;
let mut cors_origins = Vec::new();
let mut routes = None;
let mut routes_only = false;
let mut announce = false;
let mut mounts: Vec<Mount> = Vec::new();
while let Some(arg) = argv.next() {
if cert_flag(&arg, &mut argv, &mut certs)? {
if let Some(mount) = mounts.last_mut() {
mount.certs = certs.clone();
}
continue;
}
if arg == "--announce" {
announce = true;
continue;
}
if let Some(kind) = match arg.as_str() {
"--mount" => Some(ikigai_embedded::MountKind::Alias),
"--override" => Some(ikigai_embedded::MountKind::Override),
"--prefer" => Some(ikigai_embedded::MountKind::Prefer),
_ => None,
} {
let spec = argv
.next()
.ok_or_else(|| format!("{arg} needs <prefix>=<target>"))?;
let (prefix, target) = spec
.split_once('=')
.ok_or_else(|| format!("{arg} expects <prefix>=<target>, got `{spec}`"))?;
mounts.push(Mount {
prefix: prefix.to_string(),
target: target.to_string(),
certs: certs.clone(),
kind,
});
continue;
}
if arg == "--name" {
let name = argv
.next()
.ok_or_else(|| "--name needs a value".to_string())?;
#[cfg(feature = "embedded")]
ikigai_embedded::set_instance_name(name);
continue;
}
if arg == "--cap" {
caps.push(
argv.next()
.ok_or_else(|| "--cap needs a capability IRI".to_string())?,
);
continue;
}
if arg == "--code-signer" {
let signer = argv
.next()
.ok_or_else(|| "--code-signer needs a key resource IRI".to_string())?;
code_signers.push(signer);
continue;
}
if arg == "--code-signers-dir" {
let dir = argv
.next()
.ok_or_else(|| "--code-signers-dir needs a path".to_string())?;
#[cfg(feature = "embedded")]
ikigai_embedded::set_code_signers_dir(std::path::PathBuf::from(dir));
#[cfg(not(feature = "embedded"))]
let _ = dir;
continue;
}
if arg == "--eval-timeout" {
let secs = argv
.next()
.ok_or_else(|| "--eval-timeout needs seconds".to_string())?
.parse::<u64>()
.map_err(|_| "--eval-timeout needs a whole number of seconds".to_string())?;
#[cfg(feature = "embedded")]
ikigai_embedded::set_eval_timeout_secs(secs);
#[cfg(not(feature = "embedded"))]
let _ = secs;
continue;
}
if arg == "--http" {
http = Some(
argv.next()
.ok_or_else(|| "--http needs a port or host:port".to_string())?,
);
continue;
}
if arg == "--trust-proxy" {
trust_proxy = true;
continue;
}
if arg == "--cors-origin" {
cors_origins.push(
argv.next()
.ok_or_else(|| "--cors-origin needs an origin (or `*`)".to_string())?,
);
continue;
}
if arg == "--routes" {
routes = Some(
argv.next()
.ok_or_else(|| "--routes needs a resource IRI".to_string())?,
);
continue;
}
if arg == "--routes-only" {
routes_only = true;
continue;
}
if arg.starts_with('-') {
return Err(format!("unknown argument: {arg}"));
} else if target.is_none() {
target = Some(arg);
} else {
return Err(format!("unexpected argument after `serve`: {arg}"));
}
}
#[cfg(feature = "embedded")]
ikigai_embedded::set_code_signers(code_signers);
#[cfg(not(feature = "embedded"))]
let _ = code_signers;
return Ok(Some(Mode::Serve {
target,
certs,
caps,
http,
trust_proxy,
cors_origins,
routes,
routes_only,
announce,
mounts,
}));
}
if argv.peek().map(String::as_str) == Some("mcp") {
argv.next();
let mut grants = Vec::new();
let mut scopes = Vec::new();
let mut certs = Certs::default();
let mut mounts: Vec<Mount> = Vec::new();
while let Some(arg) = argv.next() {
if cert_flag(&arg, &mut argv, &mut certs)? {
if let Some(mount) = mounts.last_mut() {
mount.certs = certs.clone();
}
continue;
}
if let Some(kind) = match arg.as_str() {
"--mount" => Some(ikigai_embedded::MountKind::Alias),
"--override" => Some(ikigai_embedded::MountKind::Override),
"--prefer" => Some(ikigai_embedded::MountKind::Prefer),
_ => None,
} {
let spec = argv
.next()
.ok_or_else(|| format!("{arg} needs <prefix>=<target>"))?;
let (prefix, target) = spec
.split_once('=')
.ok_or_else(|| format!("{arg} expects <prefix>=<target>, got `{spec}`"))?;
mounts.push(Mount {
prefix: prefix.to_string(),
target: target.to_string(),
certs: certs.clone(),
kind,
});
continue;
}
match arg.as_str() {
"--grant" => grants.push(
argv.next()
.ok_or_else(|| "--grant needs a name".to_string())?,
),
"--scope" => scopes.push(
argv.next()
.ok_or_else(|| "--scope needs a capability IRI".to_string())?,
),
other => return Err(format!("unknown argument after `mcp`: {other}")),
}
}
return Ok(Some(Mode::Mcp {
grants,
scopes,
mounts,
}));
}
let mut repl = ReplArgs::default();
let mut daemon = false;
while let Some(arg) = argv.next() {
let cert_target = match repl.mounts.last_mut() {
Some(mount) => &mut mount.certs,
None => &mut repl.certs,
};
if cert_flag(&arg, &mut argv, cert_target)? {
continue;
}
match arg.as_str() {
"-h" | "--help" => return Ok(None),
"--plain" => repl.plain = true,
"--demo" => repl.demo = true,
"--daemon" => daemon = true,
"--name" => {
let name = argv
.next()
.ok_or_else(|| "--name needs a value".to_string())?;
ikigai_embedded::set_instance_name(name);
}
"--connect" => {
let target = match argv.peek() {
Some(next) if !next.starts_with('-') => argv.next(),
_ => None,
};
repl.connect = Some(target);
}
"--mount" => {
let spec = argv
.next()
.ok_or_else(|| "--mount needs <prefix>=<socket>".to_string())?;
let (prefix, socket) = spec
.split_once('=')
.ok_or_else(|| format!("--mount expects <prefix>=<socket>, got `{spec}`"))?;
repl.mounts.push(Mount {
prefix: prefix.to_string(),
target: socket.to_string(),
certs: repl.certs.clone(),
kind: ikigai_embedded::MountKind::Alias,
});
}
"--override" => {
let spec = argv
.next()
.ok_or_else(|| "--override needs <prefix>=<target>".to_string())?;
let (prefix, target) = spec
.split_once('=')
.ok_or_else(|| format!("--override expects <prefix>=<target>, got `{spec}`"))?;
repl.mounts.push(Mount {
prefix: prefix.to_string(),
target: target.to_string(),
certs: repl.certs.clone(),
kind: ikigai_embedded::MountKind::Override,
});
}
"--react" => {
repl.react = true;
}
"--prefer" => {
let spec = argv
.next()
.ok_or_else(|| "--prefer needs <prefix>=<target>".to_string())?;
let (prefix, target) = spec
.split_once('=')
.ok_or_else(|| format!("--prefer expects <prefix>=<target>, got `{spec}`"))?;
repl.mounts.push(Mount {
prefix: prefix.to_string(),
target: target.to_string(),
certs: repl.certs.clone(),
kind: ikigai_embedded::MountKind::Prefer,
});
}
"-c" | "--command" => {
let command = argv
.next()
.ok_or_else(|| format!("{arg} requires a command argument"))?;
repl.commands.push(command);
}
"-e" | "--eval" => {
let sexpr = argv
.next()
.ok_or_else(|| format!("{arg} requires an s-expression argument"))?;
repl.commands.push(sexpr);
}
"--load" => {
let uri = argv
.next()
.ok_or_else(|| "--load requires a <uri> argument".to_string())?;
let mut command = format!(":load {uri}");
if argv.peek().map(String::as_str) == Some("--cap") {
argv.next();
let scope = argv
.next()
.ok_or_else(|| "--cap requires a capability scope".to_string())?;
command.push_str(&format!(" cap={scope}"));
}
repl.commands.push(command);
}
other => return Err(format!("unknown argument: {other}")),
}
}
if daemon {
return Ok(Some(Mode::Daemon {
mounts: repl.mounts,
}));
}
Ok(Some(Mode::Repl(repl)))
}
#[cfg(feature = "embedded")]
fn main() {
let mode = match parse_args() {
Ok(Some(mode)) => mode,
Ok(None) => {
println!("{USAGE}");
return;
}
Err(e) => {
eprintln!("ikigai: {e}\n\n{USAGE}");
std::process::exit(2);
}
};
#[cfg(feature = "embedded")]
ikigai_embedded::set_instance_name(match &mode {
Mode::Daemon { .. } => "daemon",
Mode::Serve { .. } => "serve",
Mode::Mcp { .. } => "mcp",
_ => "repl",
});
match mode {
Mode::Daemon { mounts } => daemon(mounts),
Mode::Mcp {
grants,
scopes,
mounts,
} => mcp(grants, scopes, mounts),
Mode::CertGenerate { force, dir } => cert_generate(force, dir),
Mode::CertAddClient {
name,
cert_dir,
force,
} => cert_add_client(&name, cert_dir, force),
Mode::Serve {
target,
certs,
caps,
announce,
mounts,
http,
trust_proxy,
cors_origins,
routes,
routes_only,
} => match (http, target.as_deref()) {
(Some(bind), _) => serve_http(
&bind,
&caps,
trust_proxy,
&cors_origins,
routes.as_deref(),
routes_only,
),
(None, Some(t)) if is_quic(t) => serve_quic(t, &certs, &caps, announce, mounts),
(None, _) if !caps.is_empty() => {
eprintln!("ikigai: --cap sets a per-connection ceiling and needs a quic:// target");
std::process::exit(2);
}
(None, _) => serve_ipc(target, mounts),
},
Mode::Repl(args) => {
if args.demo {
ikigai_embedded::demo_flag().store(true, std::sync::atomic::Ordering::SeqCst);
}
let engine = build_engine(args.connect, args.mounts, &args.certs, args.react)
.unwrap_or_else(|e| {
eprintln!("ikigai: {e}");
std::process::exit(1);
});
run_repl(engine, args.plain, &args.commands);
}
}
}
#[cfg(feature = "embedded")]
fn daemon(mounts: Vec<Mount>) {
let mounts = match mounts_or_config(mounts) {
Ok(mounts) => mounts,
Err(e) => {
eprintln!("ikigai: {e}");
std::process::exit(2);
}
};
let kernel = if mounts.is_empty() {
ikigai_embedded::reactive_kernel_with_mounts(Vec::new())
} else {
let mut resolved = Vec::new();
for mount in mounts {
match resolve_mount(mount) {
Ok(spec) => resolved.push(spec),
Err(e) => {
eprintln!("ikigai: {e}");
std::process::exit(2);
}
}
}
ikigai_embedded::reactive_kernel_with_mounts(resolved)
};
let name = ikigai_embedded::instance_name();
match ikigai_embedded::standing_sync_interval() {
Some(every) => eprintln!(
"ikigai: daemon up — instance \"{name}\": standing sync every {}s + watchers (Ctrl-C to stop)",
every.as_secs()
),
None => eprintln!(
"ikigai: daemon up — instance \"{name}\": no \"{name}.derive_every\" in calendar.json — IDLE (Ctrl-C to stop)"
),
}
ikigai_embedded::startup_derive(&kernel);
loop {
std::thread::sleep(std::time::Duration::from_secs(3600));
}
}
#[cfg(not(feature = "embedded"))]
fn daemon(_mounts: Vec<Mount>) {
eprintln!("ikigai: --daemon requires the embedded feature");
std::process::exit(2);
}
#[cfg(feature = "embedded")]
fn mcp_capability(grants: &[String], scopes: &[String]) -> ikigai_core::Capability {
let mut union: Vec<String> = scopes.to_vec();
for name in grants {
union.extend(ikigai_embedded::grant_scopes(name));
}
union.sort();
union.dedup();
if union.is_empty() {
ikigai_core::Capability::root()
} else {
ikigai_core::Capability::scoped(union)
}
}
#[cfg(feature = "embedded")]
fn mcp_filter(grants: &[String]) -> ikigai_mcp::ToolFilter {
let mut filter = ikigai_mcp::ToolFilter::default();
for name in grants {
let (show, hide) = ikigai_embedded::grant_visibility(name);
filter.show.extend(show);
filter.hide.extend(hide);
}
filter
}
#[cfg(feature = "embedded")]
fn mcp(grants: Vec<String>, scopes: Vec<String>, mounts: Vec<Mount>) {
use ikigai_mcp::server::handle;
use std::io::{BufRead, Write};
use std::sync::{Arc, Mutex, RwLock};
let capability = Arc::new(RwLock::new(mcp_capability(&grants, &scopes)));
let filter = Arc::new(RwLock::new(mcp_filter(&grants)));
match capability.read().expect("cap lock").scopes() {
None => eprintln!("ikigai mcp: no --grant/--scope — running UNRESTRICTED (root)"),
Some(s) => eprintln!(
"ikigai mcp: serving the manifold under {} scope(s)",
s.len()
),
}
{
let f = filter.read().expect("filter lock");
if !f.show.is_empty() || !f.hide.is_empty() {
eprintln!(
"ikigai mcp: tool visibility — {} shown, {} hidden pattern(s)",
f.show.len(),
f.hide.len()
);
}
}
let mounts = match mounts_or_config(mounts) {
Ok(mounts) => mounts,
Err(e) => {
eprintln!("ikigai: {e}");
std::process::exit(2);
}
};
let kernel = if mounts.is_empty() {
ikigai_embedded::watched_kernel()
} else {
let mut resolved = Vec::new();
for mount in mounts {
let (target, prefix) = (mount.target.clone(), mount.prefix.clone());
match resolve_mount(mount) {
Ok(spec) => {
eprintln!("ikigai mcp: composing {prefix} via {target}");
resolved.push(spec);
}
Err(e) => {
eprintln!("ikigai: {e}");
std::process::exit(2);
}
}
}
for spec in resolved
.iter()
.filter(|s| s.kind == ikigai_embedded::MountKind::Prefer)
{
let _ = spec.resolver.issue(ikigai_core::Request::new(
ikigai_core::Verb::Meta,
ikigai_core::Iri::parse("urn:kernel:catalog").expect("static IRI"),
));
let up = spec.resolver.entries().is_some();
eprintln!(
"ikigai mcp: {} peer is {}",
spec.prefix,
if up {
"up — its tools are projected"
} else {
"absent — its tools are omitted (relaunch when it is up)"
}
);
}
ikigai_embedded::watched_kernel_with_mounts(resolved)
};
let stdout = Arc::new(Mutex::new(std::io::stdout()));
if !grants.is_empty() {
if let Some(path) = ikigai_embedded::grants_path() {
let capability = Arc::clone(&capability);
let filter = Arc::clone(&filter);
let stdout = Arc::clone(&stdout);
std::thread::spawn(move || {
let mtime = || std::fs::metadata(&path).and_then(|m| m.modified()).ok();
let mut last = mtime();
loop {
std::thread::sleep(std::time::Duration::from_secs(1));
let now = mtime();
if now == last {
continue;
}
last = now;
let fresh_cap = mcp_capability(&grants, &scopes);
let fresh_filter = mcp_filter(&grants);
let cap_changed =
fresh_cap.scopes() != capability.read().expect("cap lock").scopes();
let filter_changed = fresh_filter != *filter.read().expect("filter lock");
if cap_changed {
*capability.write().expect("cap lock") = fresh_cap;
}
if filter_changed {
*filter.write().expect("filter lock") = fresh_filter;
}
if cap_changed || filter_changed {
let note =
"{\"jsonrpc\":\"2.0\",\"method\":\"notifications/tools/list_changed\"}";
let mut out = stdout.lock().expect("stdout lock");
let _ = writeln!(out, "{note}");
let _ = out.flush();
eprintln!("ikigai mcp: grant changed — tool list re-emitted");
}
}
});
}
}
let stdin = std::io::stdin();
for line in stdin.lock().lines() {
let Ok(line) = line else { break };
if line.trim().is_empty() {
continue;
}
let Ok(msg) = serde_json::from_str::<serde_json::Value>(&line) else {
continue;
};
let response = {
let cap = capability.read().expect("cap lock");
let filt = filter.read().expect("filter lock");
handle(&kernel, &cap, &filt, &msg)
};
if let Some(response) = response {
let mut out = stdout.lock().expect("stdout lock");
if writeln!(
out,
"{}",
serde_json::to_string(&response).unwrap_or_default()
)
.is_err()
{
break;
}
let _ = out.flush();
}
}
}
#[cfg(not(feature = "embedded"))]
fn mcp(_grants: Vec<String>, _scopes: Vec<String>, _mounts: Vec<Mount>) {
eprintln!("ikigai: mcp requires the embedded feature");
std::process::exit(2);
}
#[cfg(feature = "embedded")]
fn with_profiles(engine: Engine) -> Engine {
engine.define_cap_profile("freebusy", ["urn:cap:personal:calendar:read:freebusy"]);
let root = ikigai_embedded::file_root();
let root = root.display();
let read = format!("urn:cap:fs:read:{root}");
let write = format!("urn:cap:fs:write:{root}");
let delete = format!("urn:cap:fs:delete:{root}");
engine.define_cap_profile("read-only", [read.clone()]);
engine.define_cap_profile("read", [read.clone()]);
engine.define_cap_profile("write", [read.clone(), write.clone()]);
engine.define_cap_profile("delete", [read.clone(), write, delete]);
engine.define_cap_profile(
"agent",
["urn:cap:personal:calendar:read:freebusy".to_string(), read],
);
engine.define_cap_profile("lisp", ["urn:cap:lisp"]);
engine
}
#[cfg(feature = "embedded")]
fn build_engine(
connect: Option<Option<String>>,
mounts: Vec<Mount>,
certs: &Certs,
react: bool,
) -> Result<Engine, String> {
match connect {
None => {
let mounts = mounts_or_config(mounts)?;
let kernel = if mounts.is_empty() {
if react {
ikigai_embedded::reactive_kernel_with_mounts(Vec::new())
} else {
ikigai_embedded::watched_kernel()
}
} else {
let mut resolved = Vec::new();
for mount in mounts {
resolved.push(resolve_mount(mount)?);
}
if react {
ikigai_embedded::reactive_kernel_with_mounts(resolved)
} else {
ikigai_embedded::watched_kernel_with_mounts(resolved)
}
};
Ok(with_profiles(Engine::new(kernel).with_spawner(
std::sync::Arc::new(ikigai_embedded::scheduler()),
)))
}
Some(target) => {
if !mounts.is_empty() {
return Err("--mount composes into the embedded kernel; drop --connect".to_string());
}
match target.as_deref() {
Some(t) if is_quic(t) => connect_quic(t, certs),
_ => connect_ipc(target),
}
}
}
}
#[cfg(feature = "embedded")]
fn mounts_or_config(mounts: Vec<Mount>) -> Result<Vec<Mount>, String> {
if mounts.is_empty() {
config_mounts()
} else {
Ok(mounts)
}
}
#[cfg(feature = "embedded")]
fn config_mounts() -> Result<Vec<Mount>, String> {
ikigai_embedded::config::all("mount")
.into_iter()
.map(|line| {
let mut parts = line.split_whitespace();
let mode = parts
.next()
.ok_or_else(|| format!("mount `{line}`: expected <mode> <prefix>=<target>"))?;
let kind = match mode {
"alias" | "mount" => ikigai_embedded::MountKind::Alias,
"override" => ikigai_embedded::MountKind::Override,
"prefer" => ikigai_embedded::MountKind::Prefer,
other => {
return Err(format!(
"mount `{line}`: unknown mode `{other}` (alias | override | prefer)"
))
}
};
let spec = parts
.next()
.ok_or_else(|| format!("mount `{line}`: expected <prefix>=<target>"))?;
let (prefix, target) = spec.split_once('=').ok_or_else(|| {
format!("mount `{line}`: expected <prefix>=<target>, got `{spec}`")
})?;
let mut certs = Certs::default();
if let Some(dir) = parts.next() {
certs.cert_dir = Some(shellexpand_home(dir));
}
Ok(Mount {
prefix: prefix.to_string(),
target: target.to_string(),
certs,
kind,
})
})
.collect()
}
fn shellexpand_home(path: &str) -> String {
match path.strip_prefix("~/") {
Some(rest) => std::path::PathBuf::from(std::env::var("HOME").unwrap_or_default())
.join(rest)
.display()
.to_string(),
None => path.to_string(),
}
}
fn resolve_mount(mount: Mount) -> Result<ikigai_embedded::MountSpec, String> {
let Mount {
prefix,
target,
certs,
kind,
} = mount;
let resolver: std::sync::Arc<dyn ikigai_resolve::Resolver> =
if kind == ikigai_embedded::MountKind::Prefer {
std::sync::Arc::new(LazyResolver {
target: target.clone(),
certs,
inner: std::sync::Mutex::new(None),
entries_failed: std::sync::Mutex::new(None),
})
} else {
connect_mount(&target, &certs, kind)?
};
Ok(ikigai_embedded::MountSpec {
prefix,
origin: target,
resolver,
kind,
})
}
struct LazyResolver {
target: String,
certs: Certs,
inner: std::sync::Mutex<Option<std::sync::Arc<dyn ikigai_resolve::Resolver>>>,
entries_failed: std::sync::Mutex<Option<std::time::Instant>>,
}
const ENTRIES_REDIAL_AFTER: std::time::Duration = std::time::Duration::from_secs(30);
impl LazyResolver {
fn get(&self) -> Result<std::sync::Arc<dyn ikigai_resolve::Resolver>, ikigai_core::Error> {
if let Some(resolver) = self.inner.lock().unwrap().clone() {
return Ok(resolver);
}
let resolver = connect_mount(
&self.target,
&self.certs,
ikigai_embedded::MountKind::Prefer,
)
.map_err(|e| ikigai_core::Error::Unavailable(format!("{}: {e}", self.target)))?;
*self.inner.lock().unwrap() = Some(std::sync::Arc::clone(&resolver));
Ok(resolver)
}
}
#[async_trait::async_trait]
impl ikigai_resolve::Resolver for LazyResolver {
fn issue(
&self,
request: ikigai_core::Request,
) -> Result<(ikigai_core::Representation, ikigai_resolve::CacheStatus), ikigai_core::Error>
{
self.get()?.issue(request)
}
fn issue_as(
&self,
request: ikigai_core::Request,
capability: &ikigai_core::Capability,
) -> Result<(ikigai_core::Representation, ikigai_resolve::CacheStatus), ikigai_core::Error>
{
self.get()?.issue_as(request, capability)
}
async fn issue_as_async(
&self,
request: ikigai_core::Request,
capability: &ikigai_core::Capability,
) -> Result<(ikigai_core::Representation, ikigai_resolve::CacheStatus), ikigai_core::Error>
{
let resolver = self.get()?;
resolver.issue_as_async(request, capability).await
}
fn is_cached(
&self,
request: &ikigai_core::Request,
capability: &ikigai_core::Capability,
) -> bool {
match self.inner.lock().unwrap().clone() {
Some(resolver) => resolver.is_cached(request, capability),
None => false,
}
}
fn entries(&self) -> Option<Vec<ikigai_core::SpaceEntry>> {
if let Some(resolver) = self.inner.lock().unwrap().clone() {
return resolver.entries();
}
{
let failed = self.entries_failed.lock().unwrap();
if let Some(at) = *failed {
if at.elapsed() < ENTRIES_REDIAL_AFTER {
return None; }
}
}
match self.get() {
Ok(resolver) => {
*self.entries_failed.lock().unwrap() = None;
resolver.entries()
}
Err(_) => {
*self.entries_failed.lock().unwrap() = Some(std::time::Instant::now());
None
}
}
}
fn transport(&self) -> String {
match self.inner.lock().unwrap().clone() {
Some(resolver) => resolver.transport(),
None => format!("{} · not connected", self.target),
}
}
}
fn connect_mount(
target: &str,
certs: &Certs,
kind: ikigai_embedded::MountKind,
) -> Result<std::sync::Arc<dyn ikigai_resolve::Resolver>, String> {
let flag = mount_flag(kind);
if let Some(name) = target.strip_prefix("peer:") {
let (resolved, certs) = resolve_peer(name, certs)?;
return connect_mount_quic(&resolved, &certs, flag);
}
if is_quic(target) {
connect_mount_quic(target, certs, flag)
} else {
connect_mount_ipc(target, kind, flag)
}
}
#[cfg(feature = "embedded")]
fn mount_flag(kind: ikigai_embedded::MountKind) -> &'static str {
match kind {
ikigai_embedded::MountKind::Alias => "--mount",
ikigai_embedded::MountKind::Override => "--override",
ikigai_embedded::MountKind::Prefer => "--prefer",
}
}
#[cfg(all(feature = "embedded", feature = "quic"))]
const PEER_DISCOVERY_WAIT: std::time::Duration = std::time::Duration::from_millis(1500);
#[cfg(all(feature = "embedded", feature = "quic"))]
fn resolve_peer(name: &str, certs: &Certs) -> Result<(String, Certs), String> {
let browser = ikigai_discovery::Browser::start()
.map_err(|e| format!("peer:{name}: could not browse this network: {e}"))?;
std::thread::sleep(PEER_DISCOVERY_WAIT);
let peer = browser.peer(name).ok_or_else(|| {
format!(
"peer:{name}: no peer is announcing under that name \
(`source urn:peer:list` shows who is). The peer must serve with `--announce`."
)
})?;
let addr = peer
.socket_addr()
.ok_or_else(|| format!("peer:{name}: announced no usable address"))?;
let mut certs = certs.clone();
if certs.cert_dir.is_none() && certs.server_cert.is_none() {
let dir = peer_cert_dir(name);
if !dir.join("server.crt").exists() {
return Err(format!(
"peer:{name}: found it at {addr}, but this machine holds no pinned \
certificate for it ({}/server.crt). Discovery supplies an address, never \
trust — enrol the peer first (on {name}: `ikigai cert add-client <this \
host>`, then copy its server.crt here), or pass --cert-dir.",
dir.display()
));
}
certs.cert_dir = Some(dir.display().to_string());
}
Ok((format!("quic://{addr}"), certs))
}
#[cfg(all(feature = "embedded", feature = "quic"))]
fn peer_cert_dir(name: &str) -> std::path::PathBuf {
ikigai_embedded::config::config_home()
.unwrap_or_default()
.join(format!("quic-{name}"))
}
#[cfg(all(feature = "embedded", not(feature = "quic")))]
fn resolve_peer(name: &str, _certs: &Certs) -> Result<(String, Certs), String> {
Err(format!(
"peer:{name}: mounting a discovered peer needs the `quic` feature"
))
}
#[cfg(all(feature = "embedded", feature = "quic"))]
fn quic_idle_timeout() -> std::time::Duration {
ikigai_embedded::config::get("quic.timeout")
.and_then(|s| s.parse::<u64>().ok())
.map(std::time::Duration::from_secs)
.unwrap_or(ikigai_quic::DEFAULT_IDLE_TIMEOUT)
}
#[cfg(all(feature = "embedded", feature = "quic"))]
fn connect_mount_quic(
target: &str,
certs: &Certs,
flag: &'static str,
) -> Result<std::sync::Arc<dyn ikigai_resolve::Resolver>, String> {
let addr = quic::parse_addr(target)?;
let identity = quic::client_identity(certs)?;
let trusted = quic::trusted_server_cert(certs)?;
let resolver = ikigai_quic::connect_with(addr, &identity, &trusted, quic_idle_timeout())
.map_err(|e| format!("{flag}: connect {target}: {e}"))?;
Ok(std::sync::Arc::new(resolver))
}
#[cfg(all(feature = "embedded", not(feature = "quic")))]
fn connect_mount_quic(
_target: &str,
_certs: &Certs,
flag: &'static str,
) -> Result<std::sync::Arc<dyn ikigai_resolve::Resolver>, String> {
Err(format!(
"{flag} of a quic:// target needs the `quic` feature"
))
}
#[cfg(all(feature = "embedded", feature = "ipc"))]
fn connect_mount_ipc(
socket: &str,
kind: ikigai_embedded::MountKind,
flag: &'static str,
) -> Result<std::sync::Arc<dyn ikigai_resolve::Resolver>, String> {
let mode = match kind {
ikigai_embedded::MountKind::Alias => ikigai_ipc::HelloMode::Alias,
_ => ikigai_ipc::HelloMode::Verbatim,
};
let resolver = ikigai_ipc::connect_as(std::path::Path::new(socket), mode)
.map_err(|e| format!("{flag}: connect {socket}: {e}"))?;
Ok(std::sync::Arc::new(resolver))
}
#[cfg(all(feature = "embedded", not(feature = "ipc")))]
fn connect_mount_ipc(
_socket: &str,
_kind: ikigai_embedded::MountKind,
flag: &'static str,
) -> Result<std::sync::Arc<dyn ikigai_resolve::Resolver>, String> {
Err(format!(
"{flag} of a Unix socket needs the `ipc` feature (Unix only)"
))
}
#[cfg(feature = "embedded")]
fn run_repl(engine: Engine, plain: bool, commands: &[String]) {
if !commands.is_empty() {
#[cfg(not(target_family = "wasm"))]
{
use std::io::{IsTerminal, Read};
if !std::io::stdin().is_terminal() {
engine.set_piped_input_with(|| {
let mut buf = Vec::new();
let _ = std::io::stdin().read_to_end(&mut buf);
buf
});
}
}
std::process::exit(repl::run_commands(engine, commands));
}
#[cfg(not(target_family = "wasm"))]
{
use std::io::IsTerminal;
if !plain && std::io::stdin().is_terminal() && std::io::stdout().is_terminal() {
if let Err(e) = tui::run(engine, ikigai_engine::config::keybindings()) {
eprintln!("ikigai: tui error: {e}");
std::process::exit(1);
}
return;
}
repl::run(engine);
}
#[cfg(target_family = "wasm")]
repl::run(engine);
}
#[cfg(all(feature = "embedded", feature = "quic"))]
fn cert_generate(force: bool, dir: Option<String>) -> ! {
match quic::generate(force, dir.map(std::path::PathBuf::from)) {
Ok(dir) => {
println!(
"wrote server.{{crt,key}} and client.{{crt,key}} to {}",
dir.display()
);
println!(
"to attach a client on another machine, copy client.crt, client.key, and \
server.crt there."
);
std::process::exit(0);
}
Err(e) => {
eprintln!("ikigai: {e}");
std::process::exit(1);
}
}
}
#[cfg(all(feature = "embedded", feature = "quic"))]
fn cert_add_client(name: &str, cert_dir: Option<String>, force: bool) -> ! {
let certs = Certs {
cert_dir,
..Default::default()
};
match quic::add_client(name, &certs, force) {
Ok(path) => {
println!("wrote a new client identity to {}", path.display());
println!(
"the server trusts it on next start (it reads clients/*.crt); to use it, copy \
{name}.crt, {name}.key, and server.crt to the client machine."
);
if let Ok(pem) = std::fs::read_to_string(&path) {
if let Ok(fingerprint) = ikigai_quic::fingerprint_of_pem(&pem) {
println!(
"fingerprint: {fingerprint}\n \
to give it its own authority, add it to the `clients` map in \
{}: \"{fingerprint}\": {{\"grant\": \"<grant>\", \"label\": \"{name}\"}}",
ikigai_embedded::clients::clients_path()
.map_or_else(|| "clients.json".into(), |p| p.display().to_string())
);
}
}
std::process::exit(0);
}
Err(e) => {
eprintln!("ikigai: {e}");
std::process::exit(1);
}
}
}
#[cfg(all(feature = "embedded", not(feature = "quic")))]
fn cert_generate(_force: bool, _dir: Option<String>) -> ! {
eprintln!("ikigai: `cert generate` needs the `quic` feature");
std::process::exit(1);
}
#[cfg(all(feature = "embedded", not(feature = "quic")))]
fn cert_add_client(_name: &str, _cert_dir: Option<String>, _force: bool) -> ! {
eprintln!("ikigai: `cert add-client` needs the `quic` feature");
std::process::exit(1);
}
#[cfg(all(feature = "embedded", feature = "quic"))]
fn serve_quic(
target: &str,
certs: &Certs,
caps: &[String],
announce: bool,
mounts: Vec<Mount>,
) -> ! {
let caps = caps.to_vec();
let result = (|| -> Result<(), String> {
let addr = quic::parse_addr(target)?;
let identity = quic::server_identity(certs)?;
let trusted = quic::trusted_client_certs(certs)?;
let mounts = mounts_or_config(mounts)?;
let announced = announce.then(ikigai_embedded::instance_name);
let mounts: Vec<Mount> = mounts
.into_iter()
.filter(|mount| {
let own = is_own_quic_addr(&mount.target, addr, announced);
if own {
eprintln!(
"ikigai: mount `{}={}` targets this process's own serve address — \
skipped (this instance IS the server; that line is for the other \
processes on this machine)",
mount.prefix, mount.target
);
}
!own
})
.collect();
let mut resolved = Vec::new();
for mount in mounts {
resolved.push(resolve_mount(mount)?);
}
let enrolment = ikigai_embedded::clients::enrolment()?;
let file_root = ikigai_embedded::file_root();
let declared: Vec<String> = caps
.iter()
.cloned()
.chain(
enrolment
.iter()
.flat_map(|e| e.grant_names())
.flat_map(|name| ikigai_embedded::grant_scopes(&name)),
)
.collect();
let unaddressable = ikigai_embedded::tenant::unaddressable_fs_scopes(&declared, &file_root);
if !unaddressable.is_empty() {
return Err(format!(
"these file scopes name paths no client of this server can address:\n \
{}\n \
a served connection reaches only {}/<its segment>/… — the file module is \
jailed there and refuses everything outside it, capability or not.\n \
write the path RELATIVE to the client's own workspace (`urn:cap:fs:read:notes` \
grants the `notes` it addresses as `urn:file:notes/…`), or point IKIGAI_FILES \
at the tree you meant to serve.",
unaddressable.join("\n "),
file_root.display()
));
}
let minter: ikigai_quic::Minter = if enrolment.is_some() {
let ceiling = if caps.is_empty() {
ikigai_core::Capability::root()
} else {
ikigai_core::Capability::scoped(caps.clone())
};
let path = ikigai_embedded::clients::clients_path()
.map_or_else(|| "clients.json".into(), |p| p.display().to_string());
let root = file_root.clone();
std::sync::Arc::new(move |peer: &ikigai_quic::PeerIdentity| {
match ikigai_embedded::clients::authority(&peer.fingerprint, &ceiling) {
Ok((grant, capability)) => {
let capability = ikigai_embedded::tenant::root_fs_scopes(
&capability,
&ikigai_embedded::tenant::tenant_root(&root, &peer.segment_id),
);
eprintln!(
"ikigai: client {} → grant \"{grant}\" ({})",
&peer.fingerprint[..peer.fingerprint.len().min(16)],
match capability.scopes() {
None => "unrestricted".to_string(),
Some(s) => format!("{} scope(s)", s.len()),
}
);
Some(ikigai_quic::Session {
capability,
file_segment: peer.segment_id.clone(),
})
}
Err(why) => {
eprintln!(
"ikigai: REFUSED a trusted client certificate — {why}\n \
fingerprint: {}\n \
to enrol it, add it to the `clients` map in {path}",
peer.fingerprint
);
None
}
}
})
} else if caps.is_empty() {
let root = file_root.clone();
std::sync::Arc::new(move |peer: &ikigai_quic::PeerIdentity| {
let segment = ikigai_embedded::tenant::tenant_root(&root, &peer.segment_id);
let _ = std::fs::create_dir_all(&segment); let seg = segment.display();
Some(ikigai_quic::Session {
capability: ikigai_core::Capability::root().attenuate([
format!("urn:cap:fs:read:{seg}"),
format!("urn:cap:fs:write:{seg}"),
format!("urn:cap:fs:delete:{seg}"),
]),
file_segment: peer.segment_id.clone(),
})
})
} else {
let ceiling = ikigai_core::Capability::scoped(caps.clone());
let root = file_root.clone();
std::sync::Arc::new(move |peer: &ikigai_quic::PeerIdentity| {
Some(ikigai_quic::Session {
capability: ikigai_embedded::tenant::root_fs_scopes(
&ceiling,
&ikigai_embedded::tenant::tenant_root(&root, &peer.segment_id),
),
file_segment: peer.segment_id.clone(),
})
})
};
let posture = match (&enrolment, caps.is_empty()) {
(Some(e), true) => format!("per-identity grants: {} enrolled", e.len()),
(Some(e), false) => format!(
"per-identity grants: {} enrolled, under ceiling: {}",
e.len(),
caps.join(", ")
),
(None, true) => "per-client workspaces".to_string(),
(None, false) => format!("fixed ceiling: {}", caps.join(", ")),
};
if let Some(default_grant) = enrolment.as_ref().and_then(|e| e.default_grant()) {
eprintln!(
"ikigai: warning: clients.json sets an explicit shared default grant \
\"{default_grant}\" — every trusted certificate that is not enrolled \
individually gets it"
);
}
let surface_caps: Vec<String> = {
let mut union = caps.clone();
for grant in enrolment.iter().flat_map(|e| e.grant_names()) {
union.extend(ikigai_embedded::grant_scopes(&grant));
}
union.sort();
union.dedup();
union
};
let surface = ikigai_embedded::ServedSurface {
personal: surface_caps
.iter()
.any(|c| c.starts_with("urn:cap:personal:")),
wire_eval: surface_caps
.iter()
.any(|c| c == "urn:cap:lisp" || c == "urn:cap:lisp:run"),
llm: surface_caps.iter().any(|c| c.starts_with("urn:cap:net:")),
};
let mounted = if resolved.is_empty() {
String::new()
} else {
format!("; {} mount(s)", resolved.len())
};
let kernel = ikigai_embedded::served_kernel_with_mounts("Remote (QUIC)", surface, resolved);
let signed_door = ikigai_embedded::code_signers_configured();
let mut faces = vec![if surface.personal {
"calendar-only"
} else {
"host + fs"
}];
if surface.llm {
faces.push("llm");
}
if surface.wire_eval {
faces.push("governed eval");
}
if surface.wire_eval && signed_door {
faces.push("signed-run");
}
let surface = faces.join(" + ");
eprintln!(
"ikigai: serving on {target} ({posture}; surface: {surface}; {} trusted client cert(s){mounted}) (Ctrl-C to stop)",
trusted.len()
);
let _announcement = if announce {
let name = ikigai_embedded::instance_name();
let wire_version = ikigai_wire::PROTOCOL_VERSION.to_string();
match ikigai_discovery::announce(
name,
addr.port(),
&[
(ikigai_discovery::TXT_SURFACE, surface.as_str()),
(ikigai_discovery::TXT_CEILING, posture.as_str()),
(ikigai_discovery::TXT_VERSION, wire_version.as_str()),
],
) {
Ok(handle) => {
eprintln!(
"ikigai: announcing as \"{name}\" on {}",
ikigai_discovery::SERVICE_TYPE
);
Some(handle)
}
Err(e) => {
eprintln!("ikigai: warning: could not announce on this network: {e}");
None
}
}
} else {
None
};
ikigai_quic::serve_with(
kernel,
addr,
&identity,
&trusted,
minter,
quic_idle_timeout(),
)
.map_err(|e| e.to_string())
})();
match result {
Ok(()) => std::process::exit(0),
Err(e) => {
eprintln!("ikigai: {e}");
std::process::exit(1);
}
}
}
#[cfg(all(feature = "embedded", feature = "quic"))]
fn connect_quic(target: &str, certs: &Certs) -> Result<Engine, String> {
let addr = quic::parse_addr(target)?;
let identity = quic::client_identity(certs)?;
let trusted = quic::trusted_server_cert(certs)?;
let resolver = ikigai_quic::connect_with(addr, &identity, &trusted, quic_idle_timeout())
.map_err(|e| format!("connect {target}: {e}"))?;
Ok(with_profiles(Engine::new(resolver)))
}
#[cfg(all(feature = "embedded", not(feature = "quic")))]
fn serve_quic(
_target: &str,
_certs: &Certs,
_caps: &[String],
_announce: bool,
_mounts: Vec<Mount>,
) -> ! {
eprintln!("ikigai: `quic://` needs the `quic` feature");
std::process::exit(1);
}
#[cfg(all(feature = "embedded", not(feature = "quic")))]
fn connect_quic(_target: &str, _certs: &Certs) -> Result<Engine, String> {
Err("`quic://` needs the `quic` feature".to_string())
}
#[cfg(all(feature = "embedded", feature = "ipc", unix))]
fn serve_ipc(path: Option<String>, mounts: Vec<Mount>) -> ! {
let socket = ipc_socket(path);
if let Some(e) = socket_path_error(&socket) {
eprintln!("ikigai: {e}");
std::process::exit(2);
}
let mounts = match mounts_or_config(mounts) {
Ok(mounts) => mounts,
Err(e) => {
eprintln!("ikigai: {e}");
std::process::exit(2);
}
};
let mounts: Vec<Mount> = mounts
.into_iter()
.filter(|mount| {
let own = is_own_socket(&mount.target, &socket);
if own {
eprintln!(
"ikigai: mount `{}={}` targets this process's own serve socket — \
skipped (this instance IS the server; that line is for the other \
processes on this machine)",
mount.prefix, mount.target
);
}
!own
})
.collect();
let mut resolved = Vec::new();
for mount in mounts {
match resolve_mount(mount) {
Ok(spec) => resolved.push(spec),
Err(e) => {
eprintln!("ikigai: {e}");
std::process::exit(2);
}
}
}
let mounted = if resolved.is_empty() {
String::new()
} else {
format!("; {} mount(s)", resolved.len())
};
eprintln!(
"ikigai: serving on {}{mounted} (Ctrl-C to stop)",
socket.display()
);
let kernel = ikigai_embedded::trusted_kernel_with_mounts("Remote (IPC)", resolved);
match ikigai_ipc::serve(kernel, &socket) {
Ok(()) => std::process::exit(0),
Err(e) => {
eprintln!("ikigai: serve error: {e}");
std::process::exit(1);
}
}
}
#[cfg(all(feature = "embedded", feature = "ipc", unix))]
fn connect_ipc(path: Option<String>) -> Result<Engine, String> {
let socket = ipc_socket(path);
let timeout = ikigai_embedded::config::get("ipc.timeout")
.and_then(|v| v.trim().parse::<u64>().ok())
.map(std::time::Duration::from_secs)
.unwrap_or(ikigai_ipc::DEFAULT_TIMEOUT);
let resolver = ikigai_ipc::connect_with_timeout(&socket, Some(timeout))
.map_err(|e| format!("connect {}: {e}", socket.display()))?;
Ok(with_profiles(Engine::new(resolver)))
}
#[cfg(all(feature = "embedded", feature = "quic"))]
fn is_own_quic_addr(target: &str, bind: std::net::SocketAddr, announced: Option<&str>) -> bool {
if let Some(name) = target.strip_prefix("peer:") {
return announced == Some(name);
}
if !is_quic(target) {
return false; }
let Ok(addr) = quic::parse_addr(target) else {
return false;
};
if addr.port() != bind.port() {
return false;
}
addr.ip() == bind.ip()
|| (bind.ip().is_unspecified()
&& (addr.ip().is_loopback() || std::net::UdpSocket::bind((addr.ip(), 0)).is_ok()))
}
#[cfg(all(feature = "embedded", feature = "ipc", unix))]
fn is_own_socket(target: &str, socket: &std::path::Path) -> bool {
if is_quic(target) || target.starts_with("peer:") {
return false;
}
let absolute = |p: std::path::PathBuf| std::path::absolute(&p).unwrap_or(p);
absolute(std::path::PathBuf::from(shellexpand_home(target))) == absolute(socket.to_path_buf())
}
#[cfg(all(feature = "embedded", feature = "ipc", unix))]
const SUN_PATH_CAPACITY: usize = if cfg!(target_os = "linux") { 108 } else { 104 };
#[cfg(all(feature = "embedded", feature = "ipc", unix))]
fn socket_path_error(socket: &std::path::Path) -> Option<String> {
use std::os::unix::ffi::OsStrExt;
let len = socket.as_os_str().as_bytes().len();
(len >= SUN_PATH_CAPACITY).then(|| {
format!(
"socket path is {len} bytes, but a Unix socket path fits {} on this \
platform — serve at a shorter path: {}",
SUN_PATH_CAPACITY - 1,
socket.display()
)
})
}
#[cfg(all(feature = "embedded", feature = "ipc", unix))]
fn ipc_socket(path: Option<String>) -> std::path::PathBuf {
path.map(std::path::PathBuf::from)
.or_else(ikigai_ipc::default_socket_path)
.unwrap_or_else(|| {
eprintln!("ikigai: no socket path given and no runtime directory to default to");
std::process::exit(2);
})
}
#[cfg(all(feature = "embedded", not(all(feature = "ipc", unix))))]
fn serve_ipc(_path: Option<String>, _mounts: Vec<Mount>) -> ! {
eprintln!("ikigai: a Unix-socket server needs the `ipc` feature on a Unix platform");
std::process::exit(1);
}
#[cfg(all(feature = "embedded", feature = "web"))]
fn serve_http(
bind: &str,
caps: &[String],
trust_proxy: bool,
cors_origins: &[String],
routes: Option<&str>,
routes_only: bool,
) -> ! {
use std::net::SocketAddr;
let addr: SocketAddr = if let Ok(port) = bind.parse::<u16>() {
SocketAddr::from(([127, 0, 0, 1], port))
} else {
match bind.parse() {
Ok(a) => a,
Err(e) => {
eprintln!("ikigai: --http wants a port or host:port ({bind}: {e})");
std::process::exit(2);
}
}
};
let kernel = std::sync::Arc::new(ikigai_embedded::kernel_for("Remote (HTTP)"));
let (cap_fn, posture) = if caps.is_empty() {
(ikigai_web::public_cap(), "public cap".to_string())
} else {
(
ikigai_web::fixed_cap(caps.to_vec()),
format!("ceiling: {}", caps.join(", ")),
)
};
let mut config = ikigai_web::EdgeConfig {
trust_proxy,
routes_only,
..Default::default()
};
config.cors.allowed_origins = cors_origins.to_vec();
let runtime = match tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
{
Ok(r) => r,
Err(e) => {
eprintln!("ikigai: could not start the async runtime: {e}");
std::process::exit(1);
}
};
let route_note = match routes {
Some(iri) => {
let loader = ikigai_embedded::kernel();
let table = match runtime.block_on(route_load::load_route_table(
&loader,
iri,
&ikigai_core::Capability::root(),
)) {
Ok(t) => t,
Err(e) => {
eprintln!("ikigai: route load failed ({e})");
std::process::exit(1);
}
};
let n = table.routes.len();
let live = ikigai_web::live_routes(table);
config.live_routes = Some(live.clone());
if let Some(path) = route_load::watch_path(iri, &ikigai_embedded::file_root()) {
let iri_owned = iri.to_string();
runtime.spawn(async move {
let mtime =
|p: &std::path::Path| std::fs::metadata(p).and_then(|m| m.modified()).ok();
let mut last = mtime(&path);
loop {
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
let now = mtime(&path);
if now == last {
continue;
}
last = now;
let loader = ikigai_embedded::kernel();
match route_load::load_route_table(
&loader,
&iri_owned,
&ikigai_core::Capability::root(),
)
.await
{
Ok(t) => {
let m = t.routes.len();
ikigai_web::swap_routes(&live, t);
eprintln!("ikigai: reloaded {m} route(s) from {iri_owned}");
}
Err(e) => eprintln!(
"ikigai: route reload failed ({e}) — keeping the current table"
),
}
}
});
format!("{n} route(s) from {iri}, watching")
} else {
format!("{n} route(s) from {iri}")
}
}
None => "mechanical routing".to_string(),
};
let route_note = if routes_only {
format!("{route_note}, routes-only (un-routed → 404)")
} else {
route_note
};
let cors_note = if cors_origins.is_empty() {
"CORS closed".to_string()
} else {
format!("CORS: {}", cors_origins.join(", "))
};
let proxy_note = if trust_proxy {
"trusting X-Forwarded-*"
} else {
"no proxy trust"
};
eprintln!(
"ikigai: serving HTTP on {addr} ({posture}; {route_note}; {cors_note}; {proxy_note}; terminate TLS at your proxy) (Ctrl-C to stop)"
);
match runtime.block_on(ikigai_web::serve_with(kernel, cap_fn, addr, config)) {
Ok(()) => std::process::exit(0),
Err(e) => {
eprintln!("ikigai: HTTP serve error: {e}");
std::process::exit(1);
}
}
}
#[cfg(not(all(feature = "embedded", feature = "web")))]
fn serve_http(
_bind: &str,
_caps: &[String],
_trust_proxy: bool,
_cors_origins: &[String],
_routes: Option<&str>,
_routes_only: bool,
) -> ! {
eprintln!("ikigai: the inbound HTTP face needs the `web` feature (build with --features web)");
std::process::exit(1);
}
#[cfg(all(feature = "embedded", not(all(feature = "ipc", unix))))]
fn connect_ipc(_path: Option<String>) -> Result<Engine, String> {
Err("attaching to a Unix socket needs the `ipc` feature on a Unix platform".to_string())
}
#[cfg(not(feature = "embedded"))]
fn main() {
eprintln!(
"ikigai {}: built without a transport. Rebuild with a transport feature, e.g. `--features embedded`.",
env!("CARGO_PKG_VERSION")
);
std::process::exit(1);
}
#[cfg(test)]
mod mount_cert_tests {
use super::*;
fn argv(args: &[&str]) -> Vec<String> {
args.iter().map(|s| s.to_string()).collect()
}
fn mounts_of(args: &[&str]) -> Vec<Mount> {
match parse_argv(argv(args).into_iter()) {
Ok(Some(Mode::Repl(repl))) => repl.mounts,
Ok(Some(_)) => panic!("expected a repl mode, got another mode"),
Ok(None) => panic!("expected a repl mode, got no mode"),
Err(e) => panic!("parse failed: {e}"),
}
}
#[test]
fn each_mount_keeps_its_own_certificates() {
let mounts = mounts_of(&[
"--mount",
"urn:personal:=quic://localhost:4433",
"--cert-dir",
"/certs/peer",
"--mount",
"urn:edge:=quic://edge.example:4433",
"--cert-dir",
"/certs/edge",
]);
assert_eq!(mounts.len(), 2);
assert_eq!(mounts[0].prefix, "urn:personal:");
assert_eq!(mounts[0].certs.cert_dir.as_deref(), Some("/certs/peer"));
assert_eq!(mounts[1].prefix, "urn:edge:");
assert_eq!(
mounts[1].certs.cert_dir.as_deref(),
Some("/certs/edge"),
"the second mount must NOT inherit the first mount's cert dir"
);
}
#[test]
fn certs_before_a_mount_are_the_default_inherited_by_mounts() {
let mounts = mounts_of(&[
"--cert-dir",
"/certs/default",
"--mount",
"urn:a:=quic://a.example:4433",
"--mount",
"urn:b:=quic://b.example:4433",
"--cert-dir",
"/certs/b",
]);
assert_eq!(mounts[0].certs.cert_dir.as_deref(), Some("/certs/default"));
assert_eq!(
mounts[1].certs.cert_dir.as_deref(),
Some("/certs/b"),
"a mount's own cert flag overrides the inherited default"
);
}
#[test]
fn mount_override_and_prefer_are_distinct_kinds() {
let mounts = mounts_of(&[
"--mount",
"urn:cal:=quic://a.example:4433",
"--override",
"urn:personal:=quic://b.example:4433",
"--prefer",
"urn:llm:=quic://plasma.local:4433",
"--cert-dir",
"/certs/plasma",
]);
assert_eq!(mounts[0].kind, ikigai_embedded::MountKind::Alias);
assert_eq!(mounts[1].kind, ikigai_embedded::MountKind::Override);
assert_eq!(mounts[2].kind, ikigai_embedded::MountKind::Prefer);
assert_eq!(mounts[2].prefix, "urn:llm:");
assert_eq!(mounts[2].target, "quic://plasma.local:4433");
assert_eq!(mounts[2].certs.cert_dir.as_deref(), Some("/certs/plasma"));
}
#[test]
fn a_prefer_mounts_entries_dial_a_live_peer() {
use ikigai_core::{builtins, EndpointSpace, Exact, Kernel};
let path =
std::env::temp_dir().join(format!("ikigai-prefer-list-{}.sock", std::process::id()));
let _ = std::fs::remove_file(&path);
let kernel = Kernel::new(std::sync::Arc::new(
EndpointSpace::new().bind(Exact::new("urn:fn:toUpper"), builtins::to_upper()),
));
let served = path.clone();
std::thread::spawn(move || {
let _ = ikigai_ipc::serve(kernel, &served);
});
for _ in 0..50 {
if path.exists() {
break;
}
std::thread::sleep(std::time::Duration::from_millis(20));
}
let mount = Mount {
prefix: "urn:fn:".to_string(),
target: path.display().to_string(),
certs: Certs::default(),
kind: ikigai_embedded::MountKind::Prefer,
};
let spec = resolve_mount(mount).expect("prefer resolves lazily");
let entries = spec
.resolver
.entries()
.expect("entries() dials the live peer");
assert!(
entries.iter().any(|e| e.endpoint == "toUpper"),
"the peer's catalog is visible without any prior resolution: {entries:?}"
);
let _ = std::fs::remove_file(&path);
}
#[test]
fn mcp_takes_mounts_with_their_own_certificates() {
let parsed = parse_argv(
argv(&[
"mcp",
"--grant",
"cal",
"--prefer",
"urn:llm:=peer:plasma",
"--mount",
"urn:cal:=quic://bug.local:4433",
"--cert-dir",
"/certs/bug",
])
.into_iter(),
);
let Ok(Some(Mode::Mcp { grants, mounts, .. })) = parsed else {
panic!("expected mcp mode");
};
assert_eq!(grants, vec!["cal".to_string()]);
assert_eq!(mounts.len(), 2);
assert_eq!(mounts[0].kind, ikigai_embedded::MountKind::Prefer);
assert_eq!(mounts[0].target, "peer:plasma");
assert!(
mounts[0].certs.cert_dir.is_none(),
"a cert flag after the SECOND mount must not leak onto the first"
);
assert_eq!(mounts[1].kind, ikigai_embedded::MountKind::Alias);
assert_eq!(mounts[1].certs.cert_dir.as_deref(), Some("/certs/bug"));
}
#[test]
fn a_prefer_mount_does_not_connect_at_startup() {
let mount = Mount {
prefix: "urn:llm:".to_string(),
target: "quic://127.0.0.1:1".to_string(),
certs: Certs::default(),
kind: ikigai_embedded::MountKind::Prefer,
};
let spec = resolve_mount(mount).expect("a prefer mount must not fail at startup");
assert!(
spec.resolver.transport().contains("not connected"),
"transport should say the peer is not connected, got: {}",
spec.resolver.transport()
);
let start = std::time::Instant::now();
assert!(
spec.resolver.entries().is_none(),
"a dead peer yields no catalog"
);
assert!(
spec.resolver.entries().is_none() && start.elapsed() < ENTRIES_REDIAL_AFTER,
"the second probe rides the negative cache"
);
}
#[test]
fn per_file_cert_overrides_are_also_per_mount() {
let mounts = mounts_of(&[
"--mount",
"urn:a:=quic://a.example:4433",
"--server-cert",
"/certs/a-server.crt",
"--mount",
"urn:b:=quic://b.example:4433",
"--server-cert",
"/certs/b-server.crt",
]);
assert_eq!(
mounts[0].certs.server_cert.as_deref(),
Some("/certs/a-server.crt")
);
assert_eq!(
mounts[1].certs.server_cert.as_deref(),
Some("/certs/b-server.crt")
);
}
}
#[cfg(all(test, feature = "embedded", feature = "ipc", unix))]
mod own_socket_tests {
use super::is_own_socket;
use std::path::Path;
#[test]
fn own_socket_is_detected_across_spellings() {
let socket = Path::new("/tmp/ikigai-test/serve.sock");
assert!(is_own_socket("/tmp/ikigai-test/serve.sock", socket));
assert!(is_own_socket("/tmp/ikigai-test/./serve.sock", socket));
assert!(!is_own_socket("/tmp/ikigai-test/other.sock", socket));
assert!(!is_own_socket("quic://plasma.local:4433", socket));
assert!(!is_own_socket("peer:plasma", socket));
}
#[test]
fn tilde_spelling_matches_the_expanded_socket() {
let home = std::env::var("HOME").expect("HOME set in test env");
let socket = std::path::PathBuf::from(home).join(".ikigai-test.sock");
assert!(is_own_socket("~/.ikigai-test.sock", &socket));
}
}
#[cfg(all(test, feature = "embedded", feature = "quic"))]
mod own_quic_addr_tests {
use super::is_own_quic_addr;
#[test]
fn own_quic_address_is_detected() {
let wildcard: std::net::SocketAddr = "0.0.0.0:4433".parse().unwrap();
assert!(is_own_quic_addr("quic://127.0.0.1:4433", wildcard, None));
assert!(!is_own_quic_addr("quic://127.0.0.1:4434", wildcard, None));
assert!(!is_own_quic_addr("/tmp/ikigai.sock", wildcard, None));
let specific: std::net::SocketAddr = "127.0.0.1:4433".parse().unwrap();
assert!(is_own_quic_addr("quic://127.0.0.1:4433", specific, None));
}
#[test]
fn a_peer_target_is_own_only_under_our_announced_name() {
let bind: std::net::SocketAddr = "0.0.0.0:4433".parse().unwrap();
assert!(!is_own_quic_addr("peer:plasma", bind, None));
assert!(is_own_quic_addr("peer:plasma", bind, Some("plasma")));
assert!(!is_own_quic_addr("peer:bug", bind, Some("plasma")));
}
}
#[cfg(all(test, feature = "embedded", feature = "ipc", unix))]
mod socket_preflight_tests {
use super::{socket_path_error, SUN_PATH_CAPACITY};
#[test]
fn the_preflight_mirrors_the_binds_length_boundary() {
let of_len = |n: usize| std::path::PathBuf::from(format!("/{}", "x".repeat(n - 1)));
assert!(socket_path_error(&of_len(SUN_PATH_CAPACITY - 1)).is_none());
let e = socket_path_error(&of_len(SUN_PATH_CAPACITY)).expect("over the sun_path capacity");
assert!(e.contains("shorter path"), "{e}");
assert!(socket_path_error(std::path::Path::new("/tmp/ikigai.sock")).is_none());
}
}
#[cfg(all(test, feature = "embedded", feature = "ipc", unix))]
mod mount_flag_tests {
use super::*;
#[test]
fn a_down_prefer_peer_says_prefer_not_mount() {
let absent = "/tmp/ikigai-test-absent-peer.sock";
let spec = resolve_mount(Mount {
prefix: "urn:repo:".to_string(),
target: absent.to_string(),
certs: Certs::default(),
kind: ikigai_embedded::MountKind::Prefer,
})
.expect("a prefer mount resolves lazily");
let err = spec
.resolver
.issue(ikigai_core::Request::new(
ikigai_core::Verb::Source,
ikigai_core::Iri::parse("urn:repo:x").expect("static IRI"),
))
.expect_err("nothing listens at the absent socket")
.to_string();
assert!(err.contains("--prefer: connect"), "{err}");
assert!(!err.contains("--mount:"), "{err}");
let Err(err) = connect_mount(absent, &Certs::default(), ikigai_embedded::MountKind::Alias)
else {
panic!("nothing listens at the absent socket");
};
assert!(err.contains("--mount: connect"), "{err}");
}
}