205 lineas en 73 ficheros de crates, mas la prosa de CLAUDE.md y del skill, que se me habian quedado afuera de los barridos anteriores (no eran ni recetas ni docs/ ni scripts/). EL BARRIDO ANCHO ESTUVO A UN COMMIT DE ROMPER EL CORPUS ENTERO. El primer intento reescribia los .rs completos, no solo los comentarios. Entre las lineas de codigo que tocaba estaban SIETE etiquetas de separacion de dominio, que son ENTRADA DE HASH: b"hammer-tree-v1" <- el prefijo de ArtifactHash::of_tree (hash.rs:60) b"hammer-seed-v1" la funcion que hashea TODOS los artefactos: b"hammer-stage1-rootfs-v2" cambiarla mueve los 4750 hashes del store b"hammer-product-rootfs-v3" b"hammer-product-attested-v2" b"hammer-builder-rootfs-v1" b"hammer-attest-dev-rootkey-0001!!" <- clave raiz de atestacion, [u8;32] Revertido y rehecho solo sobre comentarios, esquivando ademas las cadenas crudas de Rust (r#"..."#) porque el SYSTEM_PROMPT del traductor tiene lineas que empiezan como comentario. Controles: las 7 etiquetas siguen ahi, el diff toca CERO lineas de codigo, 605 tests en verde y el hash de zlib sigue en b3:dc363f26. La leccion es la misma de toda esta etapa: un literal que parece prosa puede ser entrada de hash, y la unica forma de saberlo es mirar donde se usa.
142 lines
5.2 KiB
Rust
142 lines
5.2 KiB
Rust
//! `/run/init.control` — FIFO de control humano. Ver `docs/07-agent-bus.md` §1.
|
|
//!
|
|
//! Modelo Fase 5: el FIFO existe y acepta escrituras (`echo "start web" > …`); las líneas
|
|
//! recibidas se loguean por ahora. La supervisión real de servicios llega cuando entre el
|
|
//! init propio del track posterior (`/etc/service/*/run` estilo s6/runit).
|
|
//!
|
|
//! Por qué un FIFO y no un socket: es lo MÁS unix posible (`cat`, `echo`, redirección),
|
|
//! ergonomía máxima sin parser ni framing. Para programas serios está el `/run/agent.sock`.
|
|
|
|
use std::io::{BufRead, BufReader, Write};
|
|
use std::path::{Path, PathBuf};
|
|
|
|
/// Crea (o reusa) el FIFO en `path` y devuelve la ruta canónica. Si el path existe y NO es
|
|
/// un FIFO falla con mensaje claro: no queremos pisar archivos por accidente.
|
|
pub fn ensure_fifo(path: &Path) -> std::io::Result<PathBuf> {
|
|
use std::os::unix::fs::FileTypeExt;
|
|
if let Some(parent) = path.parent() {
|
|
std::fs::create_dir_all(parent)?;
|
|
}
|
|
match std::fs::symlink_metadata(path) {
|
|
Ok(m) if m.file_type().is_fifo() => return Ok(path.to_path_buf()),
|
|
Ok(_) => {
|
|
return Err(std::io::Error::new(
|
|
std::io::ErrorKind::AlreadyExists,
|
|
format!(
|
|
"init-control: {} existe y NO es un FIFO; mueve o borra el archivo",
|
|
path.display()
|
|
),
|
|
));
|
|
}
|
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
|
|
Err(e) => return Err(e),
|
|
}
|
|
// `nix::unistd::mkfifo` con permisos 0o660: owner + group escriben/leen, world nada.
|
|
// El humano normalmente está en el grupo del daemon (p. ej. `takana`); la IA agente
|
|
// entra por el socket.
|
|
use nix::sys::stat::Mode;
|
|
nix::unistd::mkfifo(path, Mode::S_IRUSR | Mode::S_IWUSR | Mode::S_IRGRP | Mode::S_IWGRP)
|
|
.map_err(std::io::Error::from)?;
|
|
Ok(path.to_path_buf())
|
|
}
|
|
|
|
/// Lazo bloqueante: abre el FIFO en modo lectura y loguea cada línea. Cuando el último
|
|
/// escritor cierra, `read_line` devuelve 0 (EOF); volvemos a abrir para no salir del lazo.
|
|
///
|
|
/// Pensado para correr en su propio thread desde `main`.
|
|
pub fn run_reader(path: &Path) -> std::io::Result<()> {
|
|
loop {
|
|
let f = match std::fs::OpenOptions::new().read(true).open(path) {
|
|
Ok(f) => f,
|
|
Err(e) => {
|
|
tracing::warn!(error = %e, "init-control: no pude abrir para lectura");
|
|
return Err(e);
|
|
}
|
|
};
|
|
let mut reader = BufReader::new(f);
|
|
loop {
|
|
let mut line = String::new();
|
|
match reader.read_line(&mut line) {
|
|
Ok(0) => break, // EOF: re-abrimos
|
|
Ok(_) => {
|
|
let trimmed = line.trim_end_matches('\n');
|
|
tracing::info!(line = %trimmed, "init-control");
|
|
}
|
|
Err(e) => {
|
|
tracing::warn!(error = %e, "init-control: error de lectura");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Envía una línea al FIFO. Si nadie está leyendo, **bloquea** hasta que el reader la
|
|
/// consuma: ése es exactamente el contrato de un FIFO sin O_NONBLOCK, y la garantía que el
|
|
/// agente espera (`InitAck` significa "la línea entró al canal").
|
|
pub fn send_to_fifo(path: &Path, line: &str) -> std::io::Result<()> {
|
|
let mut f = std::fs::OpenOptions::new().write(true).open(path)?;
|
|
f.write_all(line.as_bytes())?;
|
|
if !line.ends_with('\n') {
|
|
f.write_all(b"\n")?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn ensure_fifo_creates_when_missing() {
|
|
let d = tempfile::tempdir().unwrap();
|
|
let p = d.path().join("init.ctl");
|
|
ensure_fifo(&p).unwrap();
|
|
use std::os::unix::fs::FileTypeExt;
|
|
assert!(p.symlink_metadata().unwrap().file_type().is_fifo());
|
|
}
|
|
|
|
#[test]
|
|
fn ensure_fifo_idempotent() {
|
|
let d = tempfile::tempdir().unwrap();
|
|
let p = d.path().join("init.ctl");
|
|
ensure_fifo(&p).unwrap();
|
|
ensure_fifo(&p).unwrap(); // sin error: ya es FIFO
|
|
}
|
|
|
|
#[test]
|
|
fn ensure_fifo_rejects_non_fifo() {
|
|
let d = tempfile::tempdir().unwrap();
|
|
let p = d.path().join("not-a-fifo");
|
|
std::fs::write(&p, b"x").unwrap();
|
|
let err = ensure_fifo(&p).unwrap_err().to_string();
|
|
assert!(err.contains("NO es un FIFO"), "{err}");
|
|
}
|
|
|
|
#[test]
|
|
fn send_and_read_one_line() {
|
|
let d = tempfile::tempdir().unwrap();
|
|
let p = d.path().join("init.ctl");
|
|
ensure_fifo(&p).unwrap();
|
|
|
|
// Reader en background; sólo lee una línea y termina.
|
|
let reader_path = p.clone();
|
|
let reader = std::thread::spawn(move || {
|
|
let f = std::fs::OpenOptions::new()
|
|
.read(true)
|
|
.open(&reader_path)
|
|
.unwrap();
|
|
let mut r = BufReader::new(f);
|
|
let mut s = String::new();
|
|
r.read_line(&mut s).unwrap();
|
|
s
|
|
});
|
|
|
|
// Pequeño retraso para asegurar que el reader llega a `open` antes del writer.
|
|
std::thread::sleep(std::time::Duration::from_millis(50));
|
|
send_to_fifo(&p, "start web").unwrap();
|
|
let got = reader.join().unwrap();
|
|
assert_eq!(got, "start web\n");
|
|
}
|
|
}
|