bxctl: reindex, and memory log, undo, pending, accept, reject
Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
This commit is contained in:
+13
-1
@@ -28,7 +28,13 @@ usage: bxctl <command> [options]
|
||||
Check that the grants load.
|
||||
|
||||
bxctl audit verify [--home DIR]
|
||||
Check the audit log's hash chain, reading the files directly.";
|
||||
Check the audit log's hash chain, reading the files directly.
|
||||
|
||||
bxctl reindex [--memory-socket PATH]
|
||||
Rebuild memoryd's index from the session logs and notes.
|
||||
|
||||
bxctl memory log [N] | undo <id> | pending | accept <id> | reject <id> [--memory-socket PATH]
|
||||
List memory edits, undo the latest edit of a file, or answer writes waiting for review.";
|
||||
|
||||
// The options every `chat` call carries. `socket` is the loop socket; `admin_socket` is the admin
|
||||
// socket the other subcommands use. Both default to the paths under `$BOXMAKER_HOME`.
|
||||
@@ -90,6 +96,10 @@ pub enum Command {
|
||||
AuditVerify {
|
||||
home: PathBuf,
|
||||
},
|
||||
Memory {
|
||||
socket: PathBuf,
|
||||
ask: crate::memory::Ask,
|
||||
},
|
||||
}
|
||||
|
||||
pub fn parse(args: &[String], home: &Path) -> Result<Command, UsageError> {
|
||||
@@ -104,6 +114,8 @@ pub fn parse(args: &[String], home: &Path) -> Result<Command, UsageError> {
|
||||
"refuse" | "deny" => parse_refuse(rest, home),
|
||||
"grants" => parse_grants(rest, home),
|
||||
"audit" => parse_audit(rest, home),
|
||||
"reindex" => crate::memory::parse_reindex(rest, home),
|
||||
"memory" => crate::memory::parse_memory(rest, home),
|
||||
// A flag before the command, or any unknown command, is not valid.
|
||||
_ => Err(UsageError),
|
||||
}
|
||||
|
||||
@@ -4,4 +4,5 @@ pub mod admin;
|
||||
pub mod chat;
|
||||
pub mod cli;
|
||||
pub mod escape;
|
||||
pub mod memory;
|
||||
pub mod verify;
|
||||
|
||||
@@ -49,6 +49,16 @@ fn main() -> ExitCode {
|
||||
reason,
|
||||
} => exit(|| cmd_refuse(&admin_socket, approval, reason.as_deref())),
|
||||
Command::GrantsCheck { admin_socket } => exit(|| cmd_grants_check(&admin_socket)),
|
||||
Command::Memory { socket, ask } => {
|
||||
let mut out = std::io::stdout().lock();
|
||||
match bxctl::memory::run(&socket, &ask, bxctl::memory::MEMORY_TIMEOUT, &mut out) {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
Err(e) => {
|
||||
eprintln!("bxctl: {e}");
|
||||
ExitCode::from(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
Command::AuditVerify { home } => {
|
||||
let mut out = std::io::stdout().lock();
|
||||
match bxctl::verify::run(&home, &mut out) {
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
//! The owner's memory commands over `memoryd`'s owner socket: `reindex`, and `memory log`,
|
||||
//! `undo`, `pending`, `accept`, `reject` (M5a spec, section 10).
|
||||
|
||||
use std::io::Write;
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
|
||||
use proto::{
|
||||
Empty, Envelope, MemoryAdmin, MemoryCommand, Message, PROTOCOL_VERSION, read_frame, write_frame,
|
||||
};
|
||||
|
||||
use crate::cli::{Command, UsageError};
|
||||
use crate::escape::escape_json_text;
|
||||
|
||||
/// How long `bxctl` waits for `memoryd`: a reindex reads every session log.
|
||||
pub const MEMORY_TIMEOUT: Duration = Duration::from_secs(300);
|
||||
|
||||
pub fn default_socket(home: &Path) -> PathBuf {
|
||||
home.join("run").join("owner-memory").join("admin.sock")
|
||||
}
|
||||
|
||||
/// What to ask `memoryd`.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Ask {
|
||||
Reindex,
|
||||
Admin(MemoryCommand, String),
|
||||
}
|
||||
|
||||
/// `bxctl reindex [--memory-socket PATH]`.
|
||||
pub fn parse_reindex(args: &[String], home: &Path) -> Result<Command, UsageError> {
|
||||
let (socket, rest) = socket_flag(args, home)?;
|
||||
if !rest.is_empty() {
|
||||
return Err(UsageError);
|
||||
}
|
||||
Ok(Command::Memory {
|
||||
socket,
|
||||
ask: Ask::Reindex,
|
||||
})
|
||||
}
|
||||
|
||||
/// `bxctl memory log [N] | undo <id> | pending | accept <id> | reject <id>`, each with an optional
|
||||
/// `--memory-socket PATH`.
|
||||
pub fn parse_memory(args: &[String], home: &Path) -> Result<Command, UsageError> {
|
||||
let (socket, rest) = socket_flag(args, home)?;
|
||||
let rest: Vec<&str> = rest.iter().map(String::as_str).collect();
|
||||
let ask = match rest.as_slice() {
|
||||
["log"] => Ask::Admin(MemoryCommand::Log, String::new()),
|
||||
["log", n] => {
|
||||
if n.parse::<usize>().is_err() {
|
||||
return Err(UsageError);
|
||||
}
|
||||
Ask::Admin(MemoryCommand::Log, n.to_string())
|
||||
}
|
||||
["pending"] => Ask::Admin(MemoryCommand::Pending, String::new()),
|
||||
["undo", id] => Ask::Admin(MemoryCommand::Undo, id.to_string()),
|
||||
["accept", id] => Ask::Admin(MemoryCommand::Accept, id.to_string()),
|
||||
["reject", id] => Ask::Admin(MemoryCommand::Reject, id.to_string()),
|
||||
_ => return Err(UsageError),
|
||||
};
|
||||
Ok(Command::Memory { socket, ask })
|
||||
}
|
||||
|
||||
/// Take `--memory-socket PATH` out of `args`, once at most; the rest in order.
|
||||
fn socket_flag(args: &[String], home: &Path) -> Result<(PathBuf, Vec<String>), UsageError> {
|
||||
let mut socket = default_socket(home);
|
||||
let mut rest = Vec::with_capacity(args.len());
|
||||
let mut seen = false;
|
||||
let mut i = 0;
|
||||
while i < args.len() {
|
||||
if args[i] == "--memory-socket" {
|
||||
if seen {
|
||||
return Err(UsageError);
|
||||
}
|
||||
seen = true;
|
||||
socket = PathBuf::from(args.get(i + 1).cloned().ok_or(UsageError)?);
|
||||
i += 2;
|
||||
} else {
|
||||
rest.push(args[i].clone());
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
Ok((socket, rest))
|
||||
}
|
||||
|
||||
/// `n` with commas between thousands.
|
||||
fn grouped(n: u64) -> String {
|
||||
let digits = n.to_string();
|
||||
let mut out = String::with_capacity(digits.len() + digits.len() / 3);
|
||||
for (i, c) in digits.chars().enumerate() {
|
||||
if i > 0 && (digits.len() - i).is_multiple_of(3) {
|
||||
out.push(',');
|
||||
}
|
||||
out.push(c);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Ask `memoryd` and print its answer. `Err` is the line for stderr.
|
||||
pub fn run(socket: &Path, ask: &Ask, timeout: Duration, out: &mut dyn Write) -> Result<(), String> {
|
||||
let mut stream = UnixStream::connect(socket)
|
||||
.map_err(|e| format!("cannot reach memoryd at {}: {e}", socket.display()))?;
|
||||
stream
|
||||
.set_read_timeout(Some(timeout))
|
||||
.map_err(|e| format!("cannot reach memoryd at {}: {e}", socket.display()))?;
|
||||
stream
|
||||
.set_write_timeout(Some(timeout))
|
||||
.map_err(|e| format!("cannot reach memoryd at {}: {e}", socket.display()))?;
|
||||
|
||||
let msg = match ask {
|
||||
Ask::Reindex => Message::Reindex(Empty {}),
|
||||
Ask::Admin(command, arg) => Message::MemoryAdmin(MemoryAdmin {
|
||||
command: *command,
|
||||
arg: arg.clone(),
|
||||
}),
|
||||
};
|
||||
let env = Envelope {
|
||||
v: PROTOCOL_VERSION,
|
||||
id: 1,
|
||||
r#final: true,
|
||||
msg,
|
||||
};
|
||||
write_frame(&mut stream, &env)
|
||||
.map_err(|e| format!("cannot reach memoryd at {}: {e}", socket.display()))?;
|
||||
|
||||
let reply = read_frame(&mut stream).map_err(|e| format!("memoryd did not answer: {e}"))?;
|
||||
let text = match reply.msg {
|
||||
Message::Reindexed(r) => format!(
|
||||
"reindexed: {} logs, {} records, {} notes",
|
||||
grouped(r.logs),
|
||||
grouped(r.records),
|
||||
grouped(r.notes)
|
||||
),
|
||||
Message::MemoryAdminReply(reply) => reply.text,
|
||||
Message::Error(e) => return Err(format!("memoryd: {}", escape_json_text(&e.detail))),
|
||||
_ => return Err("memoryd answered with the wrong message".to_string()),
|
||||
};
|
||||
|
||||
writeln!(out, "{text}").map_err(|e| e.to_string())
|
||||
}
|
||||
Reference in New Issue
Block a user