wip7
This commit is contained in:
parent
354af5a192
commit
39f3623af1
3 changed files with 103 additions and 17 deletions
|
|
@ -479,3 +479,37 @@ fn the_ladder_allowance_is_three_escalations_the_drain_and_a_minute() {
|
|||
// The sweep above is only as long as this constant: pin the number the spec derives (3 * 120 + 2 + 60).
|
||||
assert_eq!(LADDER_S, 422);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_yield_closes_stdin_at_idle_only_and_a_session_with_a_line_outstanding_or_a_turn_open_waits() {
|
||||
// Outstanding write (the start line has no echo yet): held.
|
||||
assert_eq!(feed(working(), Event::Yield, 1).1, Action::None);
|
||||
// Mid-turn: held, and it acts at the idle the result brings.
|
||||
let (state, actions) = run(vec![(Event::Echo, 0), (Event::Yield, 1)]);
|
||||
assert_eq!(actions[1], Action::None);
|
||||
assert_eq!(feed(state, OK, 5).1, Action::CloseStdin);
|
||||
// Idle: at once, and only once.
|
||||
let (state, _) = idle_at(5);
|
||||
let (state, action) = feed(state, Event::Yield, 6);
|
||||
assert_eq!(action, Action::CloseStdin);
|
||||
assert_eq!(feed(state, Event::Tick, 7).1, Action::None, "stdin is closed once");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_yield_takes_precedence_over_the_text_that_waits_and_over_a_flush_not_yet_written() {
|
||||
// Text waits at idle, then the session is told to yield: nothing more is written to it.
|
||||
let (state, _) = idle_at(5);
|
||||
let (state, _) = feed(state, Event::Inbox { pending: true }, 5);
|
||||
let after_tells = state.clone();
|
||||
assert_eq!(feed(after_tells, Event::Yield, 6).1, Action::CloseStdin);
|
||||
let (state, _) = idle_at(5);
|
||||
assert_eq!(feed(feed(state, Event::Yield, 5).0, Event::Inbox { pending: true }, 6).1, Action::None, "closed: nothing is written after the yield");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_flush_already_written_ends_the_session_by_itself_and_a_yield_does_not_cut_in() {
|
||||
let (state, actions) = run(vec![(Event::Echo, 0), (req("a", CEILING_TOKENS), 1), (OK, 2)]);
|
||||
assert_eq!(actions[2], flush(Why::Ceiling), "the ceiling's flush is written at idle");
|
||||
let (_, action) = feed(state, Event::Yield, 3);
|
||||
assert_eq!(action, Action::None, "the flush's own answer closes stdin");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1686,3 +1686,53 @@ fn a_brief_the_run_claims_is_in_the_task_once_and_not_again_under_also_open() {
|
|||
assert_eq!(start.matches("claimed follow-up").count(), 1, "{start}");
|
||||
assert!(start.contains("Do the claimed thing.") && !start.contains("Also open for you"), "{start}");
|
||||
}
|
||||
|
||||
/// The Persona's boundary: the bubblewrap wrapper `ClaudeSpawner` runs when it has a sandbox.
|
||||
#[cfg(unix)]
|
||||
mod sandbox {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn the_sandbox_arguments_are_pinned() {
|
||||
let args = sandbox_args(Path::new("/var/lib/mind-x/sandbox"), Path::new("/run/mind-x"));
|
||||
let expected: Vec<String> = [
|
||||
"--ro-bind", "/", "/", "--dev", "/dev", "--proc", "/proc", "--tmpfs", "/tmp", "--tmpfs", "/run", "--ro-bind-try", "/run/systemd/resolve", "/run/systemd/resolve", "--bind", "/run/mind-x", "/run/mind-x", "--tmpfs",
|
||||
"/etc/gamecult", "--tmpfs", "/var/lib", "--bind", "/var/lib/mind-x/sandbox", "/var/lib/mind-x/sandbox", "--unshare-pid", "--die-with-parent", "--new-session", "--chdir", "/var/lib/mind-x/sandbox", "--",
|
||||
]
|
||||
.map(String::from)
|
||||
.to_vec();
|
||||
assert_eq!(args, expected);
|
||||
// The credentials are hidden before the sandbox and the scratch are bound back, and the network is not unshared.
|
||||
let at = |needle: &str| args.iter().position(|arg| arg == needle).unwrap();
|
||||
assert!(at("/run") < at("/run/mind-x") && at("/var/lib") < at("/var/lib/mind-x/sandbox"));
|
||||
assert!(!args.iter().any(|arg| arg == "--unshare-net" || arg == "--unshare-all"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_sandboxed_spawner_runs_bwrap_with_its_arguments_then_claude_and_the_child_sees_no_credential() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let (bin, scratch, sandbox) = (dir.path().join("bin"), dir.path().join("scratch"), dir.path().join("sandbox"));
|
||||
for made in [&bin, &scratch, &sandbox] {
|
||||
std::fs::create_dir(made).unwrap();
|
||||
}
|
||||
let (argv, environment) = (dir.path().join("argv"), dir.path().join("environment"));
|
||||
let fake = bin.join("bwrap");
|
||||
std::fs::write(&fake, format!("#!/bin/sh\nprintf '%s\n' \"$@\" > {}\nenv > {}\n", argv.display(), environment.display())).unwrap();
|
||||
std::fs::set_permissions(&fake, std::fs::Permissions::from_mode(0o755)).unwrap();
|
||||
let mut unit = unit_environment();
|
||||
unit.retain(|(name, _)| name != "PATH");
|
||||
unit.push(("PATH".into(), format!("{}:/usr/bin:/bin", bin.display()).into()));
|
||||
unit.push((SCRATCH_VARIABLE.into(), scratch.clone().into_os_string()));
|
||||
let spawner = ClaudeSpawner::in_sandbox(unit, "MIND_PERSONA_CONFIG_SEED", Some(sandbox.clone())).unwrap();
|
||||
let mut child = spawner.spawn(&["--flag".to_string(), "value".to_string()], &token_holding("tok-canary")).unwrap();
|
||||
while !matches!(child.next(StdDuration::from_secs(20)), Read::Exited(_) | Read::Timeout) {}
|
||||
let recorded: Vec<String> = std::fs::read_to_string(&argv).unwrap().lines().map(String::from).collect();
|
||||
let mut expected = sandbox_args(&sandbox, &scratch);
|
||||
expected.extend(["claude", "--flag", "value"].map(String::from));
|
||||
assert_eq!(recorded, expected, "bwrap, its arguments, then claude and its own");
|
||||
let seen = std::fs::read_to_string(&environment).unwrap();
|
||||
assert!(!seen.contains("canary") && !seen.contains("UNLISTED"), "the child's environment is the allow-list: {seen}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,14 +41,7 @@ mind_body::chat_rudp: struct RudpChat pub-fields[]
|
|||
mind_body::cli: fn instance_and_endpoint (args : & [String] , env_endpoint : Option < String > , usage : & str) -> Result < (Slug , SocketAddr) >
|
||||
mind_body::cli: fn operator_name (sudo_user : Option < String > , user : Option < String >) -> String
|
||||
mind_body::cli: fn run (args : & [String] , root : & Path , set_by : & str , now : DateTime < Utc > , out : & mut impl Write , stop_units : & mut dyn FnMut (& Slug) -> Result < () > ,) -> Result < () >
|
||||
mind_body::connector: const MAX_OUTPUT_TOKENS : u32
|
||||
mind_body::connector: fn endpoint (text : & str) -> Result < SocketAddr >
|
||||
mind_body::connector: fn key (key_file_text : & str) -> String
|
||||
mind_body::connector: fn render_plan (windows : & [PlanQuota] , now_ms : u64) -> String
|
||||
mind_body::connector: impl Connector for KitConnector
|
||||
mind_body::connector: impl KitConnector
|
||||
mind_body::connector: impl KitConnector: fn new (endpoint : SocketAddr , key_text : & str , model : impl Into < String > , caller : impl Into < String >) -> Result < Self >
|
||||
mind_body::connector: struct KitConnector pub-fields[]
|
||||
mind_body::control: # [derive (Clone , Copy , Debug , PartialEq , Eq)] pub enum HeldReason { BrakeAbsent , BrakeHeld , Undecodable , }
|
||||
mind_body::control: # [derive (Debug , PartialEq , Eq)] pub enum Effective { Held { reason : HeldReason } , Released (Grant) , }
|
||||
mind_body::control: # [derive (Debug , PartialEq , Eq)] pub enum SwarmEffective { Held (HeldReason) , Released , }
|
||||
|
|
@ -154,46 +147,52 @@ mind_body::lease_client: struct SocketLender pub-fields[]
|
|||
mind_body::lease_client: struct TokenFd pub-fields[]
|
||||
mind_body::lease_client::socket: impl Lease for Granted
|
||||
mind_body::persona: # [derive (Clone , Copy , Debug , PartialEq , Eq , Serialize , Deserialize)] # [serde (rename_all = "lowercase")] pub enum SettleOutcome { Done , Declined , Wrong , }
|
||||
mind_body::persona: # [derive (Clone , Copy , Debug , PartialEq , Eq)] pub enum Failure { Held (HeldReason) , Chat , Persona , Doctrine , DoctrineTooLarge , Connector , Undecodable , RoomNotAllowed , Send , SelfRun , Memory , Ack , }
|
||||
mind_body::persona: # [derive (Clone , Copy , Debug , PartialEq , Eq)] pub enum Failure { Held (HeldReason) , Chat , Persona , Doctrine , DoctrineTooLarge , Lease , Session , Undecodable , RoomNotAllowed , Send , SelfRun , Memory , Ack , }
|
||||
mind_body::persona: # [derive (Debug , PartialEq , Eq)] pub enum Outcome { Dead (HeldReason) , NoRun , Served { run : PipelineRef , answered : u32 , failure : Option < Failure > , self_run : Option < PipelineRef > } , }
|
||||
mind_body::persona: const AGENT : & str
|
||||
mind_body::persona: const DOCTRINE_MAX_BYTES : usize
|
||||
mind_body::persona: const ENV_CONFIG_SEED : & str
|
||||
mind_body::persona: const ENV_DOCTRINE : & str
|
||||
mind_body::persona: const ENV_OPERATOR : & str
|
||||
mind_body::persona: const ENV_MINDS : & str
|
||||
mind_body::persona: const ENV_MODEL : & str
|
||||
mind_body::persona: const ENV_OPERATOR : & str
|
||||
mind_body::persona: const ENV_ROOMS : & str
|
||||
mind_body::persona: const ENV_RUN_CAP : & str
|
||||
mind_body::persona: const ENV_SELF_SENDER : & str
|
||||
mind_body::persona: const MAX_TURNS_PER_START : u32
|
||||
mind_body::persona: fn reply_schema (rooms : & [String] , campaigns : & [String] , people : & [String] , open : & [String]) -> String
|
||||
mind_body::persona: fn start (organs : & Organs , control : & impl ControlSource , swarm : & impl ControlSource , config : & Config) -> Result < Outcome >
|
||||
mind_body::persona: const PERSONA_POLL_S : i64
|
||||
mind_body::persona: const SANDBOX_DIR : & str
|
||||
mind_body::persona: fn carry (organs : & Organs , control : & impl ControlSource , swarm : & impl ControlSource , config : & Config) -> Result < Outcome >
|
||||
mind_body::persona: fn reply_schema (rooms : & [String] , campaigns : & [String]) -> String
|
||||
mind_body::persona: impl Config
|
||||
mind_body::persona: impl Config: fn from_args (args : & [String] , usage : & str) -> Result < (Self , Vec < String >) >
|
||||
mind_body::persona: impl Config: fn from_args_and_env (args : & [String] , lookup : impl Fn (& str) -> Option < String > , usage : & str) -> Result < (Self , Vec < String >) >
|
||||
mind_body::persona: impl Conversation
|
||||
mind_body::persona: impl FileTrace
|
||||
mind_body::persona: impl FileTrace: fn new (path : impl Into < PathBuf >) -> Self
|
||||
mind_body::persona: impl HuginnPersona
|
||||
mind_body::persona: impl HuginnPersona: fn new (client : HuginnClient) -> Self
|
||||
mind_body::persona: impl PersonaState for HuginnPersona
|
||||
mind_body::persona: impl Sender
|
||||
mind_body::persona: impl Shown
|
||||
mind_body::persona: impl Trace for FileTrace
|
||||
mind_body::persona: impl Trace for Noted < '_ >
|
||||
mind_body::persona: impl fmt :: Display for Failure
|
||||
mind_body::persona: impl fmt :: Display for Outcome
|
||||
mind_body::persona: pub trait Connector { fn turn (& self , request : & TurnRequest) -> Result < String > ; fn usage_readout (& self) -> Result < UsageReadout > ; }
|
||||
mind_body::persona: impl< C : ControlSource , S : ControlSource > PersonaInbox < '_ , C , S >
|
||||
mind_body::persona: impl< C : ControlSource , S : ControlSource > SessionInbox for PersonaInbox < '_ , C , S >
|
||||
mind_body::persona: pub trait PersonaState { fn instructions (& self) -> Result < String > ; }
|
||||
mind_body::persona: pub trait Trace { fn record (& self , trace : & TurnTrace) -> Result < () > ; }
|
||||
mind_body::persona: pub use model_connector :: { PlanQuota , UsageReadout } ;
|
||||
mind_body::persona: struct Config # [derive (Clone , Debug , PartialEq , Eq)] pub-fields[rooms: Vec < String >, operators: Vec < String >, minds: Vec < String >, own_sender: String, max_turns: u32, doctrine: Vec < PathBuf >]
|
||||
mind_body::persona: pub use model_connector :: PlanQuota ;
|
||||
mind_body::persona: struct Config # [derive (Clone , Debug , PartialEq , Eq)] pub-fields[rooms: Vec < String >, operators: Vec < String >, minds: Vec < String >, own_sender: String, model: String, doctrine: Vec < PathBuf >]
|
||||
mind_body::persona: struct FileTrace pub-fields[]
|
||||
mind_body::persona: struct HuginnPersona pub-fields[]
|
||||
mind_body::persona: struct Organs< 'a > pub-fields[mind: & 'a dyn MindPort, launcher: & 'a dyn Launcher, clock: & 'a dyn Clock, host: & 'a str, chat: & 'a dyn Chat, connector: & 'a dyn Connector, persona: & 'a dyn PersonaState, trace: & 'a dyn Trace, tempo: Option < & 'a dyn TempoView >]
|
||||
mind_body::persona: struct Organs< 'a > pub-fields[mind: & 'a dyn MindPort, launcher: & 'a dyn Launcher, clock: & 'a dyn Clock, host: & 'a str, chat: & 'a dyn Chat, persona: & 'a dyn PersonaState, trace: & 'a dyn Trace, tempo: Option < & 'a dyn TempoView >, lender: & 'a dyn Lender, spawner: & 'a dyn Spawner, run_cap_s: u64, sandbox: & 'a Path, scratch: & 'a Path]
|
||||
mind_body::persona: struct PersonaReply # [derive (Clone , Debug , PartialEq , Eq , Serialize , Deserialize)] pub-fields[say: Vec < Say >, start_self_run: Option < StartSelfRun >, remember: Vec < Remember >, settle: Vec < Settle >, tempo_vote_beat_s: Option < Option < u32 > >, urgency_permille: Option < u16 >]
|
||||
mind_body::persona: struct Remember # [derive (Clone , Debug , PartialEq , Eq , Serialize , Deserialize)] pub-fields[sort: MemorySort, text: String, about: Vec < String >, salience_permille: u16]
|
||||
mind_body::persona: struct Say # [derive (Clone , Debug , PartialEq , Eq , Serialize , Deserialize)] pub-fields[room: String, text: String]
|
||||
mind_body::persona: struct Settle # [derive (Clone , Debug , PartialEq , Eq , Serialize , Deserialize)] pub-fields[item: String, outcome: SettleOutcome, why: String]
|
||||
mind_body::persona: struct StartSelfRun # [derive (Clone , Debug , PartialEq , Eq , Serialize , Deserialize)] pub-fields[brief: String, repo: String, campaign: String, for_item: Option < String >]
|
||||
mind_body::persona: struct TraceItem # [derive (Clone , Debug , PartialEq , Eq , Serialize , Deserialize)] pub-fields[provenance: String, room: String, text: String]
|
||||
mind_body::persona: struct TurnRequest # [derive (Clone , Debug , PartialEq , Eq)] pub-fields[caller: String, request_id: String, instructions: String, input: String, output_schema_json: String]
|
||||
mind_body::persona: struct TurnTrace # [derive (Clone , Debug , PartialEq , Eq , DatabaseEntry)] pub-fields[run: String, turn: u32, at: DateTime < Utc >, inputs: Vec < TraceItem >, outputs: Vec < TraceItem >, outcome: String]
|
||||
mind_body::queue: # [derive (Clone , Copy , Debug , PartialEq , Eq)] pub enum Breaker { Clear , Trip , }
|
||||
mind_body::queue: const CONSECUTIVE : usize
|
||||
|
|
@ -240,17 +239,20 @@ mind_body::self_run: fn start_message (instance : & Slug , run : & PipelineRef ,
|
|||
mind_body::self_run: fn writes_hour_cache (line : & str) -> Option < bool >
|
||||
mind_body::self_run: impl Child for ClaudeProcess
|
||||
mind_body::self_run: impl ClaudeSpawner
|
||||
mind_body::self_run: impl ClaudeSpawner: fn in_sandbox (unit_environment : impl IntoIterator < Item = (OsString , OsString) > , seed_variable : & str , sandbox : Option < PathBuf >) -> Result < Self >
|
||||
mind_body::self_run: impl ClaudeSpawner: fn new (unit_environment : impl IntoIterator < Item = (OsString , OsString) >) -> Result < Self >
|
||||
mind_body::self_run: impl Drop for FreshConfig
|
||||
mind_body::self_run: impl FreshConfig
|
||||
mind_body::self_run: impl Heard
|
||||
mind_body::self_run: impl SelfRunOutcome
|
||||
mind_body::self_run: impl SelfRunOutcome: fn exit_status (& self) -> i32
|
||||
mind_body::self_run: impl SessionInbox for SelfInbox < '_ >
|
||||
mind_body::self_run: impl Spawner for ClaudeSpawner
|
||||
mind_body::self_run: impl Stop
|
||||
mind_body::self_run: impl fmt :: Display for SeedLinkRefused
|
||||
mind_body::self_run: impl fmt :: Display for SelfRunOutcome
|
||||
mind_body::self_run: impl std :: error :: Error for SeedLinkRefused
|
||||
mind_body::self_run: impl< 'a > SelfInbox < 'a >
|
||||
mind_body::self_run: mod inbox
|
||||
mind_body::self_run: pub trait Child { fn write_line (& mut self , line : & str) ; fn close_input (& mut self) ; fn next (& mut self , timeout : StdDuration) -> Read ; fn stop (& mut self , how : Stop) -> bool ; }
|
||||
mind_body::self_run: pub trait Spawner { fn spawn (& self , args : & [String] , token : & TokenFd) -> Result < Box < dyn Child > > ; }
|
||||
|
|
|
|||
Loading…
Reference in a new issue