Files
cubelinux-2/cubesys/src/commands.rs
T
CUBELinux-2 06ea3252eb cubesys: add cube systemd daemon (cube-server) + socket client (cubec)
Implements the requested cube service: a long-lived daemon that holds ONE
CubeStore for its whole lifetime and serves the cube command language over a
Unix-domain socket, plus cubec to talk to it.

- cubesys::commands: factored the single command interpreter (Session::exec)
  so cube REPL, cubec client, and the daemon run identical logic
- cubesys::net: dependency-free length-framed AF_UNIX transport
- cubesys::persist: dependency-free JSON snapshot (atomic tmp+rename) so the
  store -- including sealed/encrypted records -- survives daemon restarts
- cube-server: listens on $XDG_RUNTIME_DIR/cube/cube.sock, snapshots to
  $XDG_STATE_HOME/cube/cube-store.json, replays on startup
- cubec: one-shot + REPL client over the socket
- cube.rs trimmed to a thin REPL/script/demo driver (help text updated)
- /etc/systemd/system/cube.service: runs as luulu, ProtectSystem=strict,
  RestrictAddressFamilies=AF_UNIX, Restart=on-failure; enabled + active
- integration.md documents the daemon + caveat (open rewrites plaintext)

Verified: ./check (fmt+tests+clippy -D warnings) green; ./check mount (27
FUSE e2e) green; socket CLI round-trips; sealed record survived a full
service restart and reopened+r with original value.
2026-08-11 00:10:45 -04:00

267 lines
11 KiB
Rust

//! Shared CUBELinux-2 system command interpreter.
//!
//! This module holds the *single* implementation of the cube command language
//! (`prog`, `write`, `run`, `ls`, `stat`, `seal`, `open`). It is used by every
//! front-end — the local `cube` REPL, the `cubec` socket client, and the
//! `cube-server` daemon — so the behaviour can never drift between them.
//!
//! A [`Session`] wraps one [`CubeStore`] backend (currently the in-memory
//! [`HashBackend`]) and executes one command line at a time against it. The
//! daemon holds a single long-lived `Session`; the REPL holds a transient one.
use cubecode::{CodeCell, Kind, Op, Vm};
use cubecoords::CubeHeader;
use cubecrypt::{CubeEnv, KeySlot, Selector, TransformId};
use cubestore::{CubeStore, HashBackend};
/// One cube command session: a store plus the command interpreter.
pub struct Session {
store: CubeStore<HashBackend>,
}
impl Default for Session {
fn default() -> Self {
Self::new()
}
}
impl Session {
/// A fresh, empty session over an in-memory store.
pub fn new() -> Self {
Session {
store: CubeStore::new(HashBackend::new()),
}
}
/// Access the underlying store (used by persistence to snapshot/restore).
pub fn store(&self) -> &CubeStore<HashBackend> {
&self.store
}
/// Mutable access to the underlying store (used by persistence to load).
pub fn store_mut(&mut self) -> &mut CubeStore<HashBackend> {
&mut self.store
}
/// Execute one command line. `Ok(out)` is a (possibly multi-line) result to
/// print; `Err(e)` is a human-readable error.
pub fn exec(&mut self, line: &str) -> Result<String, String> {
let mut it = line.split_whitespace();
let cmd = it.next().ok_or_else(|| "empty line".to_string())?;
match cmd {
"prog" => {
let path = it.next().ok_or_else(|| "prog needs <path>".to_string())?;
let mut ops: Vec<Op> = Vec::new();
while let Some(tok) = it.next() {
let arg = if takes_arg(tok) {
it.next()
.and_then(|a| a.parse::<u8>().ok())
.ok_or_else(|| format!("prog: {tok} needs a u8 argument"))?
} else {
0
};
ops.push(make_op(tok, arg)?);
}
if ops.is_empty() {
return Err("prog: no ops given".to_string());
}
let name = path.rsplit('/').next().unwrap_or(path);
let coord =
crate::store_code_cell(&mut self.store, path, Kind::Fn, name, &[], &ops)
.map_err(|e| e.to_string())?;
Ok(format!(
"wrote program {path} -> coord {} ({} ops)",
coord.pack_u32(),
ops.len()
))
}
"write" => {
let path = it.next().ok_or_else(|| "write needs <path>".to_string())?;
let bytes: Vec<u8> = it
.map(parse_byte)
.collect::<Option<_>>()
.ok_or_else(|| "write: every byte must be hex/dec 0..255".to_string())?;
let code = cubecode::decode(&bytes)
.map_err(|e| format!("bytecode decode error: {e:?}"))?;
let name = path.rsplit('/').next().unwrap_or(path);
let coord =
crate::store_code_cell(&mut self.store, path, Kind::Fn, name, &[], &code)
.map_err(|e| e.to_string())?;
Ok(format!("wrote {path} -> coord {}", coord.pack_u32()))
}
"run" => {
let path = it.next().ok_or_else(|| "run needs <path>".to_string())?;
let cell = crate::load_code_cell(&self.store, path).map_err(|e| e.to_string())?;
let mut vm = Vm::new(self.store.clone());
let res = vm.run(cell.label);
let mut out = format!("run {path} => {res:?}");
if !vm.output().is_empty() {
out.push_str(&format!(
"\n trace: {}",
String::from_utf8_lossy(vm.output()).trim_end()
));
}
Ok(out)
}
"ls" => {
let dir = it.next().ok_or_else(|| "ls needs <dir>".to_string())?;
let fs = cubefs::CubeFs::new(self.store.clone());
let entries = fs.readdir(dir).map_err(|e| format!("ls {dir}: {e:?}"))?;
if entries.is_empty() {
Ok(format!("ls {dir} -> (empty)"))
} else {
let names: Vec<String> = entries.into_iter().map(|(n, _)| n).collect();
Ok(format!("ls {dir} -> {}", names.join(" ")))
}
}
"stat" => {
let path = it.next().ok_or_else(|| "stat needs <path>".to_string())?;
let fs = cubefs::CubeFs::new(self.store.clone());
let a = fs
.getattr(path)
.map_err(|e| format!("stat {path}: {e:?}"))?;
Ok(format!(
"stat {path} -> ino={} kind={:?} size={} mode={:o}",
a.ino, a.kind, a.size, a.mode
))
}
"seal" | "open" => {
let path = it.next().ok_or_else(|| format!("{cmd} needs <path>"))?;
let keyc = it
.next()
.ok_or_else(|| format!("{cmd} needs <K.Z.Y.X> key cell"))?;
let tf = it
.next()
.ok_or_else(|| format!("{cmd} needs <transform>"))?;
let coord = crate::path_to_czyx(path).map_err(|e| e.to_string())?;
let kc = parse_coord(keyc)
.ok_or_else(|| "bad key-cell coord (use C.Z.Y.X)".to_string())?;
let transform = parse_transform(tf)
.ok_or_else(|| "unknown transform (none|gcm|chacha|xts)".to_string())?;
// Ensure key material exists at the Null-cube key cell.
if self.store.get_record(&kc).is_none() {
self.store.put_record(
kc,
&CubeHeader::new(),
b"demo-key-material-32-bytes-long!!",
);
}
let env = CubeEnv::new(
vec![KeySlot {
key_cell: kc,
transform,
salt: vec![],
}],
vec![],
);
if cmd == "seal" {
let (h, body) = self
.store
.get_record(&coord)
.ok_or_else(|| format!("seal: no record at {path}"))?;
env.put_encrypted(&mut self.store, coord, Selector::Slot(0), &body, h)
.map_err(|e| format!("seal: {e:?}"))?;
Ok(format!("sealed {path} under key {} ({tf})", kc.pack_u32()))
} else {
let (_, envelope) = self
.store
.get_record(&coord)
.ok_or_else(|| format!("open: no record at {path}"))?;
let pt = env
.open(&self.store, Selector::Slot(0), &envelope)
.map_err(|e| format!("open: {e:?}"))?;
let cell = CodeCell::from_record(coord, &CubeHeader::new(), &pt)
.ok_or_else(|| "open: decrypted body is not valid bytecode".to_string())?;
// The VM runs code located by coordinate, so to execute a
// sealed record we decrypt it back into a plaintext record,
// then run.
self.store.put_record(coord, &CubeHeader::new(), &pt);
let mut vm = Vm::new(self.store.clone());
let res = vm.run(cell.label);
Ok(format!(
"open+run {path} (key {}) => {res:?}",
kc.pack_u32()
))
}
}
other => Err(format!("unknown command: {other}")),
}
}
}
/// Parse a byte token: decimal (`42`) or hex (`0x2a`).
fn parse_byte(t: &str) -> Option<u8> {
if let Ok(v) = t.parse::<u8>() {
return Some(v);
}
u8::from_str_radix(t.trim_start_matches("0x"), 16).ok()
}
/// Parse a coordinate `C.Z.Y.X` (decimal, allows 0 for Null space).
pub fn parse_coord(s: &str) -> Option<cubecoords::Czyx> {
let parts: Vec<&str> = s.split('.').collect();
if parts.len() != 4 {
return None;
}
let nums: Option<Vec<u8>> = parts.iter().map(|p| p.parse::<u8>().ok()).collect();
let nums = nums?;
Some(cubecoords::Czyx::new(nums[0], nums[1], nums[2], nums[3]))
}
/// Parse a cubevm op name (case-insensitive) into an [`Op`]. `arg` is the
/// operand byte for ops that take one (const/load/store/jmp/jz/jnz/call/ret/
/// syscall); it is ignored for argument-less ops.
fn make_op(t: &str, arg: u8) -> Result<Op, String> {
Ok(match t.to_ascii_lowercase().as_str() {
"nop" => Op::Nop,
"halt" => Op::Halt,
"const" => Op::Const(arg),
"load" => Op::Load(arg),
"store" => Op::Store(arg),
"add" => Op::Add,
"sub" => Op::Sub,
"mul" => Op::Mul,
"div" => Op::Div,
"mod" => Op::Mod,
"and" => Op::And,
"or" => Op::Or,
"xor" => Op::Xor,
"shl" => Op::Shl,
"shr" => Op::Shr,
"eq" => Op::Eq,
"ne" => Op::Ne,
"lt" => Op::Lt,
"gt" => Op::Gt,
"le" => Op::Le,
"ge" => Op::Ge,
"jmp" => Op::Jmp(arg),
"jz" => Op::Jz(arg),
"jnz" => Op::Jnz(arg),
"call" => Op::CallLink(arg),
"ret" => Op::Ret,
"syscall" => Op::Syscall(arg),
other => return Err(format!("prog: unknown op {other}")),
})
}
/// True for ops that consume the next token as a u8 operand.
fn takes_arg(t: &str) -> bool {
matches!(
t.to_ascii_lowercase().as_str(),
"const" | "load" | "store" | "jmp" | "jz" | "jnz" | "call" | "syscall"
)
}
/// Parse a transform token into a [`TransformId`].
pub fn parse_transform(s: &str) -> Option<TransformId> {
match s {
"none" => Some(TransformId::None),
"gcm" => Some(TransformId::Aes256Gcm),
"chacha" => Some(TransformId::ChaCha20Poly1305),
"xts" => Some(TransformId::Aes256Xts),
_ => None,
}
}