diff --git a/AGENTS.md b/AGENTS.md index 70fccbb..f9d3b4d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -66,6 +66,7 @@ proposed capabilities as plans, not existing behavior. - `crates/runa-code/`: source identities and in-memory Rune compile cache. - `crates/runa-actor/`: actor value and lifecycle contracts. - `crates/runa-runtime/`: single-worker scheduling and in-memory transitions. +- `crates/runa-image-api/`: trusted in-process services for operating on an image. - `runa-std/actors/`: default Rune actor scripts loaded by the local CLI. - `docs/architecture.md`: architecture, roadmap, risks, and acceptance scenario. - `docs/decisions/`: architectural decisions. diff --git a/CHANGELOG.md b/CHANGELOG.md index c303be9..ef04e87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,8 @@ All notable changes to this project will be documented in this file. retry, exposed in the CLI and documented with native side-effect caveats. - Active actor execution telemetry in snapshots: completed VM steps, instruction pointer, and call-frame depth. +- Trusted in-process `runa-image-api` service boundary for code and actor + operations, now used by the local CLI. - Execution-model decision and Phase 1 runtime documentation. - Lifecycle tests proving failed-message retry from committed state and stop/restart preservation of reserved-first FIFO order. diff --git a/Cargo.lock b/Cargo.lock index 21e40ab..815904e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -395,6 +395,7 @@ dependencies = [ "runa-actor", "runa-code", "runa-core", + "runa-image-api", "runa-rune", "runa-runtime", ] @@ -420,6 +421,16 @@ dependencies = [ name = "runa-core" version = "0.0.0" +[[package]] +name = "runa-image-api" +version = "0.0.0" +dependencies = [ + "runa-actor", + "runa-code", + "runa-core", + "runa-runtime", +] + [[package]] name = "runa-rune" version = "0.0.0" diff --git a/Cargo.toml b/Cargo.toml index 902f0fa..cd28a1b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "crates/runa-actor", "crates/runa-code", "crates/runa-core", + "crates/runa-image-api", "crates/runa-rune", "crates/runa-runtime", ] diff --git a/README.md b/README.md index 58d1b7d..323910e 100644 --- a/README.md +++ b/README.md @@ -13,11 +13,12 @@ experiment using Rune 0.14.2: script execution, native calls, async suspension, instruction budgets, stepping, and compiled-code sharing. It also runs a minimal single-worker actor demonstration with bounded mailboxes and explicit message effects. Source and compiled implementation identities are BLAKE3 content -addresses with an in-memory compile cache. Persistence, interactive behavior -editing, canonicalization, the graphical world, and development API are not +addresses with an in-memory compile cache. Persistence, canonicalization, the +network development API, graphical world, and VS Code extension are not implemented yet. The code library now supports an in-memory versioned behavior registry, and the CLI exposes publishing, history, rollback, safe-boundary -installation, pause/resume, and cancellation. +installation, pause/resume, and cancellation through a trusted in-process image +API. It is not a network API and does not provide authentication. The prototype uses worker-local invocation stacks and cooperative instruction slices, and actor snapshots report basic active-execution progress. Native @@ -104,7 +105,8 @@ test task runs `cargo test --workspace`. │ ├── runa-actor/ # Actor value contracts │ ├── runa-code/ # Source identities and compile cache │ ├── runa-rune/ # Phase 0 Rune experiments -│ └── runa-runtime/ # Single-worker actor prototype +│ ├── runa-runtime/ # Single-worker actor prototype +│ └── runa-image-api/ # Trusted in-process image operations ├── docs/ │ ├── architecture.md │ └── decisions/ diff --git a/crates/runa-image-api/Cargo.toml b/crates/runa-image-api/Cargo.toml new file mode 100644 index 0000000..ec5f724 --- /dev/null +++ b/crates/runa-image-api/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "runa-image-api" +version = "0.0.0" +edition = "2024" + +[dependencies] +runa-actor = { path = "../runa-actor" } +runa-code = { path = "../runa-code" } +runa-core = { path = "../runa-core" } +runa-runtime = { path = "../runa-runtime" } diff --git a/crates/runa-image-api/src/lib.rs b/crates/runa-image-api/src/lib.rs new file mode 100644 index 0000000..e214025 --- /dev/null +++ b/crates/runa-image-api/src/lib.rs @@ -0,0 +1,229 @@ +//! Trusted, in-process application services for a running Runa image. + +use std::num::NonZeroUsize; +use std::sync::Arc; + +use runa_actor::{ActorId, ActorSnapshot, Event, Message}; +use runa_code::{BehaviorSnapshot, CodeStore}; +use runa_core::ImplementationId; +use runa_runtime::{Runtime, RuntimeResult}; + +pub struct ImageApi { + code: CodeStore, + runtime: Runtime, +} + +impl Default for ImageApi { + fn default() -> Self { + Self::new() + } +} + +impl ImageApi { + pub fn new() -> Self { + Self { + code: CodeStore::default(), + runtime: Runtime::default(), + } + } + + pub fn publish_behavior( + &mut self, + name: &str, + expected_revision: Option, + source: &str, + ) -> RuntimeResult { + let artifact = self.code.compile(source)?; + self.runtime.publish(Arc::clone(&artifact))?; + self.code.publish_behavior(name, expected_revision, source) + } + + pub fn rollback_behavior( + &mut self, + name: &str, + expected_revision: u64, + target_revision: u64, + ) -> RuntimeResult { + self.code + .rollback_behavior(name, expected_revision, target_revision) + } + + pub fn behavior(&self, name: &str) -> Option { + self.code.behavior(name) + } + + pub fn behaviors(&self) -> Vec { + self.code.behaviors() + } + + pub fn behavior_source(&self, name: &str, revision: u64) -> RuntimeResult<&str> { + let behavior = self.code.behavior(name).ok_or("unknown behavior")?; + let version = behavior + .history + .iter() + .find(|version| version.revision == revision) + .ok_or("unknown behavior revision")?; + self.code + .artifact(version.implementation_id) + .map(|artifact| artifact.original_source()) + .ok_or_else(|| "behavior artifact is unavailable".into()) + } + + pub fn spawn( + &mut self, + behavior_name: &str, + state: i64, + capacity: NonZeroUsize, + ) -> RuntimeResult { + let behavior = self + .code + .behavior(behavior_name) + .ok_or("unknown behavior")?; + self.runtime.spawn(behavior.current, state, capacity) + } + + pub fn send(&mut self, actor: ActorId, message: Message) -> RuntimeResult<()> { + self.runtime.send(actor, message) + } + + pub fn tick(&mut self, ticks: usize, budget: NonZeroUsize) { + for _ in 0..ticks { + self.runtime.tick(budget); + } + } + + pub fn inspect(&self, actor: ActorId) -> RuntimeResult { + self.runtime.inspect(actor) + } + + pub fn actors(&self) -> Vec { + self.runtime.actors() + } + + pub fn events(&mut self) -> Vec { + self.runtime.drain_events() + } + + pub fn stop(&mut self, actor: ActorId) -> RuntimeResult<()> { + self.runtime.stop(actor) + } + + pub fn restart(&mut self, actor: ActorId) -> RuntimeResult<()> { + self.runtime.restart(actor) + } + + pub fn pause(&mut self, actor: ActorId) -> RuntimeResult<()> { + self.runtime.pause(actor) + } + + pub fn resume(&mut self, actor: ActorId) -> RuntimeResult<()> { + self.runtime.resume(actor) + } + + pub fn cancel(&mut self, actor: ActorId) -> RuntimeResult<()> { + self.runtime.cancel(actor) + } + + pub fn install( + &mut self, + actor: ActorId, + behavior_name: &str, + ) -> RuntimeResult { + self.install_with_migration(actor, behavior_name, Ok) + } + + pub fn install_with_migration( + &mut self, + actor: ActorId, + behavior_name: &str, + migrate: impl FnOnce(i64) -> RuntimeResult, + ) -> RuntimeResult { + let behavior = self + .code + .behavior(behavior_name) + .ok_or("unknown behavior")?; + self.runtime + .install_with_migration(actor, behavior.current, migrate)?; + Ok(behavior.current) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn capacity(n: usize) -> NonZeroUsize { + NonZeroUsize::new(n).expect("capacity must be positive") + } + + #[test] + fn image_api_publishes_spawns_inspects_and_exposes_events() -> RuntimeResult<()> { + let mut image = ImageApi::new(); + let behavior = image.publish_behavior( + "counter", + None, + "pub fn handle(state, kind, value) { (state + value, []) }", + )?; + assert_eq!( + image.behavior_source("counter", behavior.revision)?, + "pub fn handle(state, kind, value) { (state + value, []) }" + ); + let actor = image.spawn("counter", 5, capacity(2))?; + image.send(actor, Message { kind: 0, value: 7 })?; + image.tick(100, capacity(10)); + + assert_eq!(image.inspect(actor)?.state, 12); + assert_eq!(image.inspect(actor)?.implementation, behavior.current); + assert!( + image + .events() + .iter() + .any(|event| matches!(event, Event::Committed(id) if *id == actor)) + ); + Ok(()) + } + + #[test] + fn image_api_rejects_stale_behavior_updates_without_changing_current() -> RuntimeResult<()> { + let mut image = ImageApi::new(); + let first = image.publish_behavior( + "counter", + None, + "pub fn handle(state, kind, value) { (state + value, []) }", + )?; + assert!( + image + .publish_behavior( + "counter", + None, + "pub fn handle(state, kind, value) { (state - value, []) }" + ) + .is_err() + ); + assert_eq!(image.behavior("counter").unwrap().current, first.current); + Ok(()) + } + + #[test] + fn image_api_installs_behavior_with_atomic_state_migration() -> RuntimeResult<()> { + let mut image = ImageApi::new(); + let first = image.publish_behavior( + "counter", + None, + "pub fn handle(state, kind, value) { (state + value, []) }", + )?; + image.publish_behavior( + "counter", + Some(first.revision), + "pub fn handle(state, kind, value) { (state * value, []) }", + )?; + let actor = image.spawn("counter", 5, capacity(2))?; + let implementation = image.install_with_migration(actor, "counter", |state| { + Ok(state.checked_mul(10).ok_or("migration overflow")?) + })?; + + assert_eq!(image.inspect(actor)?.state, 50); + assert_eq!(image.inspect(actor)?.implementation, implementation); + Ok(()) + } +} diff --git a/crates/runa/Cargo.toml b/crates/runa/Cargo.toml index d3ff6c3..a8e2b77 100644 --- a/crates/runa/Cargo.toml +++ b/crates/runa/Cargo.toml @@ -7,5 +7,6 @@ edition = "2024" runa-actor = { path = "../runa-actor" } runa-code = { path = "../runa-code" } runa-core = { path = "../runa-core" } +runa-image-api = { path = "../runa-image-api" } runa-rune = { path = "../runa-rune" } runa-runtime = { path = "../runa-runtime" } diff --git a/crates/runa/src/main.rs b/crates/runa/src/main.rs index 90b2210..538f7f8 100644 --- a/crates/runa/src/main.rs +++ b/crates/runa/src/main.rs @@ -2,26 +2,20 @@ use std::io::{self, BufRead, Write}; use std::num::NonZeroUsize; use runa_actor::{ActorId, Message}; -use runa_code::CodeStore; +use runa_image_api::ImageApi; use runa_rune::ExperimentResult; -use runa_runtime::Runtime; const MAILBOX_CAPACITY: usize = 64; const DEFAULT_TICK_BUDGET: usize = 32; const DEFAULT_BEHAVIOR: &str = "counter"; fn main() -> ExperimentResult<()> { - let mut code = CodeStore::default(); - let behavior = code.publish_behavior( + let mut image = ImageApi::new(); + let behavior = image.publish_behavior( DEFAULT_BEHAVIOR, None, include_str!("../../../runa-std/actors/counter.rn"), )?; - let artifact = code - .artifact(behavior.current) - .ok_or("published behavior artifact is missing")?; - let mut runtime = Runtime::default(); - runtime.publish(std::sync::Arc::clone(artifact))?; println!("Runa — Rune Actors"); println!( @@ -32,8 +26,7 @@ fn main() -> ExperimentResult<()> { run_repl( io::stdin().lock(), io::stdout().lock(), - &mut code, - &mut runtime, + &mut image, DEFAULT_BEHAVIOR, )?; Ok(()) @@ -42,8 +35,7 @@ fn main() -> ExperimentResult<()> { fn run_repl( mut input: R, mut output: W, - code: &mut CodeStore, - runtime: &mut Runtime, + image: &mut ImageApi, default_behavior: &str, ) -> io::Result<()> { let mut line = String::new(); @@ -62,9 +54,9 @@ fn run_repl( let args: Vec<_> = words.collect(); let result = match command { "help" => print_help(&mut output), - "spawn" => spawn_command(&mut output, &args, default_behavior, code, runtime), + "spawn" => spawn_command(&mut output, &args, default_behavior, image), "behaviors" if args.is_empty() => { - for behavior in code.behaviors() { + for behavior in image.behaviors() { writeln!( output, "{} revision {} ({})", @@ -74,12 +66,12 @@ fn run_repl( Ok(()) } "behaviors" => writeln!(output, "error: usage: behaviors"), - "publish" => publish_command(&mut output, &args, code, runtime), - "history" => history_command(&mut output, &args, code), - "rollback" => rollback_command(&mut output, &args, code), - "install" => install_command(&mut output, &args, code, runtime), + "publish" => publish_command(&mut output, &args, image), + "history" => history_command(&mut output, &args, image), + "rollback" => rollback_command(&mut output, &args, image), + "install" => install_command(&mut output, &args, image), "actors" if args.is_empty() => { - for actor in runtime.actors() { + for actor in image.actors() { writeln!(output, "{actor:#?}")?; } Ok(()) @@ -87,7 +79,7 @@ fn run_repl( "actors" => writeln!(output, "error: usage: actors"), "send" => match parse_send_args(&args) { Ok((actor, kind, value)) => { - match runtime.send(ActorId(actor), Message { kind, value }) { + match image.send(ActorId(actor), Message { kind, value }) { Ok(()) => writeln!(output, "message queued"), Err(error) => writeln!(output, "error: {error}"), } @@ -96,51 +88,32 @@ fn run_repl( }, "tick" => match parse_tick_args(&args) { Ok((count, budget)) => { - for _ in 0..count { - runtime.tick(budget); - } + image.tick(count, budget); writeln!(output, "ran {count} scheduler tick(s)") } Err(error) => writeln!(output, "error: {error}"), }, "inspect" => match parse_exact::(&args) { - Ok([id]) => match runtime.inspect(ActorId(id)) { + Ok([id]) => match image.inspect(ActorId(id)) { Ok(actor) => writeln!(output, "{actor:#?}"), Err(error) => writeln!(output, "error: {error}"), }, Err(error) => writeln!(output, "error: {error}"), }, "events" if args.is_empty() => { - for event in runtime.drain_events() { + for event in image.events() { writeln!(output, "{event:#?}")?; } Ok(()) } "events" => writeln!(output, "error: usage: events"), - "stop" => { - lifecycle_command(&mut output, &args, |runtime, id| runtime.stop(id), runtime) + "stop" => lifecycle_command(&mut output, &args, |image, id| image.stop(id), image), + "restart" => { + lifecycle_command(&mut output, &args, |image, id| image.restart(id), image) } - "restart" => lifecycle_command( - &mut output, - &args, - |runtime, id| runtime.restart(id), - runtime, - ), - "pause" => { - lifecycle_command(&mut output, &args, |runtime, id| runtime.pause(id), runtime) - } - "resume" => lifecycle_command( - &mut output, - &args, - |runtime, id| runtime.resume(id), - runtime, - ), - "cancel" => lifecycle_command( - &mut output, - &args, - |runtime, id| runtime.cancel(id), - runtime, - ), + "pause" => lifecycle_command(&mut output, &args, |image, id| image.pause(id), image), + "resume" => lifecycle_command(&mut output, &args, |image, id| image.resume(id), image), + "cancel" => lifecycle_command(&mut output, &args, |image, id| image.cancel(id), image), "quit" | "exit" if args.is_empty() => break, "quit" | "exit" => writeln!(output, "error: usage: quit"), _ => writeln!(output, "error: unknown command `{command}`; type `help`"), @@ -153,11 +126,11 @@ fn run_repl( fn lifecycle_command( output: &mut W, args: &[&str], - operation: impl FnOnce(&mut Runtime, ActorId) -> runa_runtime::RuntimeResult<()>, - runtime: &mut Runtime, + operation: impl FnOnce(&mut ImageApi, ActorId) -> ExperimentResult<()>, + image: &mut ImageApi, ) -> io::Result<()> { match parse_exact::(args) { - Ok([id]) => match operation(runtime, ActorId(id)) { + Ok([id]) => match operation(image, ActorId(id)) { Ok(()) => writeln!(output, "ok"), Err(error) => writeln!(output, "error: {error}"), }, @@ -169,8 +142,7 @@ fn spawn_command( output: &mut W, args: &[&str], default_behavior: &str, - code: &CodeStore, - runtime: &mut Runtime, + image: &mut ImageApi, ) -> io::Result<()> { let (state_arg, behavior_name) = match args { [state] => (*state, default_behavior), @@ -186,18 +158,18 @@ fn spawn_command( Ok(state) => state, Err(_) => return writeln!(output, "error: invalid initial state: `{state_arg}`"), }; - let Some(behavior) = code.behavior(behavior_name) else { - return writeln!(output, "error: unknown behavior `{behavior_name}`"); - }; - match runtime.spawn( - behavior.current, + match image.spawn( + behavior_name, state, NonZeroUsize::new(MAILBOX_CAPACITY).expect("capacity is nonzero"), ) { Ok(id) => writeln!( output, "spawned {id:?} with `{behavior_name}` revision {}", - behavior.revision + image + .behavior(behavior_name) + .expect("spawn resolved behavior") + .revision ), Err(error) => writeln!(output, "error: {error}"), } @@ -206,8 +178,7 @@ fn spawn_command( fn publish_command( output: &mut W, args: &[&str], - code: &mut CodeStore, - runtime: &mut Runtime, + image: &mut ImageApi, ) -> io::Result<()> { let [name, expected_revision, source_path] = args else { return writeln!( @@ -229,14 +200,7 @@ fn publish_command( return writeln!(output, "error: could not read `{source_path}`: {error}"); } }; - let artifact = match code.compile(&source) { - Ok(artifact) => artifact, - Err(error) => return writeln!(output, "error: {error}"), - }; - if let Err(error) = runtime.publish(std::sync::Arc::clone(&artifact)) { - return writeln!(output, "error: {error}"); - } - let behavior = match code.publish_behavior(name, expected_revision, &source) { + let behavior = match image.publish_behavior(name, expected_revision, &source) { Ok(behavior) => behavior, Err(error) => return writeln!(output, "error: {error}"), }; @@ -247,11 +211,11 @@ fn publish_command( ) } -fn history_command(output: &mut W, args: &[&str], code: &CodeStore) -> io::Result<()> { +fn history_command(output: &mut W, args: &[&str], image: &ImageApi) -> io::Result<()> { let [name] = args else { return writeln!(output, "error: usage: history "); }; - let Some(behavior) = code.behavior(name) else { + let Some(behavior) = image.behavior(name) else { return writeln!(output, "error: unknown behavior `{name}`"); }; for version in behavior.history { @@ -272,7 +236,7 @@ fn history_command(output: &mut W, args: &[&str], code: &CodeStore) -> fn rollback_command( output: &mut W, args: &[&str], - code: &mut CodeStore, + image: &mut ImageApi, ) -> io::Result<()> { let [name, expected_revision, target_revision] = args else { return writeln!( @@ -288,7 +252,7 @@ fn rollback_command( Ok(revision) => revision, Err(_) => return writeln!(output, "error: invalid target revision"), }; - match code.rollback_behavior(name, expected_revision, target_revision) { + match image.rollback_behavior(name, expected_revision, target_revision) { Ok(behavior) => writeln!( output, "rolled back `{}` to implementation {} as revision {}", @@ -301,8 +265,7 @@ fn rollback_command( fn install_command( output: &mut W, args: &[&str], - code: &CodeStore, - runtime: &mut Runtime, + image: &mut ImageApi, ) -> io::Result<()> { let [actor_id, name] = args else { return writeln!(output, "error: usage: install "); @@ -311,15 +274,15 @@ fn install_command( Ok(id) => ActorId(id), Err(_) => return writeln!(output, "error: invalid actor ID"), }; - let Some(behavior) = code.behavior(name) else { - return writeln!(output, "error: unknown behavior `{name}`"); - }; - match runtime.install(actor_id, behavior.current) { - Ok(()) => writeln!( - output, - "installed `{}` revision {} on actor {}", - behavior.name, behavior.revision, actor_id.0 - ), + match image.install(actor_id, name) { + Ok(implementation) => { + let behavior = image.behavior(name).expect("install resolved behavior"); + writeln!( + output, + "installed `{}` revision {} on actor {} ({implementation})", + behavior.name, behavior.revision, actor_id.0 + ) + } Err(error) => writeln!(output, "error: {error}"), } } @@ -408,30 +371,24 @@ fn print_help(output: &mut W) -> io::Result<()> { mod tests { use super::*; - #[test] - fn repl_drives_actor_messages_and_inspection() -> ExperimentResult<()> { - let mut runtime = Runtime::default(); - let mut code = CodeStore::default(); - code.publish_behavior( + fn image() -> ExperimentResult { + let mut image = ImageApi::new(); + image.publish_behavior( DEFAULT_BEHAVIOR, None, include_str!("../../../runa-std/actors/counter.rn"), )?; - let implementation = code.behavior(DEFAULT_BEHAVIOR).unwrap().current; - runtime.publish(std::sync::Arc::clone( - code.artifact(implementation).unwrap(), - ))?; + Ok(image) + } + + #[test] + fn repl_drives_actor_messages_and_inspection() -> ExperimentResult<()> { + let mut image = image()?; let input = io::Cursor::new( "spawn 0\nspawn 10\nsend 1 1 2\ntick 100\ninspect 1\ninspect 2\nevents\nquit\n", ); let mut output = Vec::new(); - run_repl( - input, - &mut output, - &mut code, - &mut runtime, - DEFAULT_BEHAVIOR, - )?; + run_repl(input, &mut output, &mut image, DEFAULT_BEHAVIOR)?; let output = String::from_utf8(output)?; assert!(output.contains("ActorId(\n 1,")); assert!(output.contains("ActorId(\n 2,")); @@ -442,26 +399,10 @@ mod tests { #[test] fn repl_reports_bad_input_and_continues() -> ExperimentResult<()> { - let mut runtime = Runtime::default(); - let mut code = CodeStore::default(); - code.publish_behavior( - DEFAULT_BEHAVIOR, - None, - include_str!("../../../runa-std/actors/counter.rn"), - )?; - let implementation = code.behavior(DEFAULT_BEHAVIOR).unwrap().current; - runtime.publish(std::sync::Arc::clone( - code.artifact(implementation).unwrap(), - ))?; + let mut image = image()?; let input = io::Cursor::new("spawn nope\nsend 999 0 1\ntick 1 0\nunknown\nquit\n"); let mut output = Vec::new(); - run_repl( - input, - &mut output, - &mut code, - &mut runtime, - DEFAULT_BEHAVIOR, - )?; + run_repl(input, &mut output, &mut image, DEFAULT_BEHAVIOR)?; let output = String::from_utf8(output)?; assert!(output.contains("invalid initial state")); assert!(output.contains("unknown actor")); @@ -472,17 +413,7 @@ mod tests { #[test] fn repl_manages_named_behavior_revisions_and_installs_them() -> ExperimentResult<()> { - let mut code = CodeStore::default(); - code.publish_behavior( - DEFAULT_BEHAVIOR, - None, - include_str!("../../../runa-std/actors/counter.rn"), - )?; - let implementation = code.behavior(DEFAULT_BEHAVIOR).unwrap().current; - let mut runtime = Runtime::default(); - runtime.publish(std::sync::Arc::clone( - code.artifact(implementation).unwrap(), - ))?; + let mut image = image()?; let source_path = concat!( env!("CARGO_MANIFEST_DIR"), "/../../runa-std/actors/counter.rn" @@ -491,13 +422,7 @@ mod tests { "behaviors\npublish counter 1 {source_path}\nhistory counter\nrollback counter 2 1\nspawn 0 counter\ninstall 1 counter\nsend 1 0 1\ntick 1 1\ncancel 1\npause 1\nresume 1\ninspect 1\nevents\nquit\n" )); let mut output = Vec::new(); - run_repl( - input, - &mut output, - &mut code, - &mut runtime, - DEFAULT_BEHAVIOR, - )?; + run_repl(input, &mut output, &mut image, DEFAULT_BEHAVIOR)?; let output = String::from_utf8(output)?; assert!(output.contains("published `counter` revision 2")); assert!(output.contains("revision 1:")); @@ -508,8 +433,11 @@ mod tests { assert!(output.contains("Paused(")); assert!(output.contains("Resumed(")); assert!(output.contains("InvocationCancelled(")); - assert_eq!(code.behavior(DEFAULT_BEHAVIOR).unwrap().revision, 3); - assert_eq!(runtime.inspect(ActorId(1))?.implementation, implementation); + assert_eq!(image.behavior(DEFAULT_BEHAVIOR).unwrap().revision, 3); + assert_eq!( + image.inspect(ActorId(1))?.implementation, + image.behavior(DEFAULT_BEHAVIOR).unwrap().current + ); Ok(()) } } diff --git a/docs/architecture.md b/docs/architecture.md index f5068ac..71548f3 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -308,7 +308,7 @@ The diagram is conceptual; Cargo dependencies should be kept minimal. A dependen | `runa-actor` | Mailbox and state-transition semantics, message reservation, acknowledgment, effects, actor lifecycle definitions | `runa-core` | | `runa-rune` | Rune compilation, VM execution, suspension, interruption hooks, language-specific diagnostics | `runa-core`, Rune libraries | | `runa-runtime` | Scheduling, actor execution coordination, state commits, crash recovery, binding actor semantics to Rune execution | `runa-core`, `runa-actor`, `runa-code`, `runa-rune` | -| `runa-image-api` | Authorized in-process operations for spawning, inspecting, sending, editing, upgrading, and debugging | `runa-core`, `runa-runtime` | +| `runa-image-api` | Trusted in-process application services for spawning, inspecting, sending, editing, and upgrading | `runa-core`, `runa-actor`, `runa-code`, `runa-runtime` | | `runa-protocol` | Versioned transport-neutral request/response/event DTOs and validation | `runa-core` | | `runa-server` | WebSocket/JSON-RPC transport, sessions, authentication, subscriptions | `runa-protocol`, `runa-image-api` | | `runa-graphics` | Native window, rendering and input mechanisms; no actor policy | `runa-core` if necessary | @@ -316,8 +316,17 @@ The diagram is conceptual; Cargo dependencies should be kept minimal. A dependen | `runa` | Startup, configuration, resource wiring, shutdown | Top-level crates it instantiates | The current prototype has added `runa-code` for exact-source identity and -in-memory compilation caching. It deliberately does not canonicalize source -yet; canonicalization needs a semantics-preserving, source-map-aware policy. +in-memory compilation caching, and `runa-image-api` as the trusted, +in-process application-service boundary used by the local CLI. It deliberately +does not canonicalize source yet; canonicalization needs a semantics-preserving, +source-map-aware policy. The image API is not yet a network API and performs no +authentication or authorization; callers must be trusted by the host. + +The initial `runa-image-api` provides process-local behavior source retrieval, +publication, history and rollback, actor lifecycle and messaging, snapshots, +ticks, and event retrieval. The CLI uses these application services rather +than reaching into the code store and runtime directly. Versioned wire DTOs +and transport are the next separate layer. **Important:** `runa-core` must not contain schedulers, mailbox implementations, persistence engines, Rune VM types, service traits used by only one crate, generic helper collections, or GUI abstractions. Prefer a type defined in its owning crate until two independent consumers demonstrably need a stable shared representation. @@ -326,7 +335,7 @@ yet; canonicalization needs a semantics-preserving, source-map-aware policy. - `runa-actor` defines actor rules and data contracts; it does **not** spawn threads or call Rune. - `runa-rune` executes Rune code but does **not** own actor scheduling, mailboxes, or image APIs. - `runa-runtime` coordinates both. Initially it can depend directly on `runa-rune`; introduce an execution-adapter trait only when a second implementation or test double justifies it. Place the trait at the lowest layer that actually consumes it, rather than automatically in `runa-core`. -- `runa-image-api` is the single application-service entry point for in-world tools and external clients. Its methods enforce permissions, lifecycle invariants, and revision checks. +- `runa-image-api` is the single application-service entry point for in-world tools and external clients. It centralizes lifecycle invariants and revision checks; host-level permissions and authentication remain required at any external boundary. - `runa-protocol` holds wire DTOs, **not** runtime service implementations. Translate DTOs at the server boundary; never serialize internal actor locks or VM references. - `runa-world` uses the same `runa-image-api` operations as the external bridge. It does not reach into runtime internals for convenience. - Native platform handles stay behind `runa-graphics` (or a later platform crate), not in `runa-core`. @@ -421,6 +430,10 @@ inspection remain future work. - Headless server, versioned protocol, actor and mailbox operations, source editing, events. - CLI client that can spawn, inspect, send messages, and replace behaviors. +The first Phase 5 increment is an in-process trusted application API used by +the CLI. A transport-neutral versioned protocol and network server are not yet +implemented. + ### Phase 6 — Graphical world - Native window/rendering, input, scene manager, Rune-driven button and window. diff --git a/docs/runtime.md b/docs/runtime.md index 2c29782..f46be41 100644 --- a/docs/runtime.md +++ b/docs/runtime.md @@ -47,7 +47,16 @@ The default per-actor slice budget is 32 instructions. - `runa-rune` owns compilation and typed bounded invocation execution. - `runa-runtime` owns implementation registration, queues, private committed state, scheduling, message effects and lifecycle coordination. -- `runa` composes the experiment and demonstration. +- `runa-image-api` composes code publication and runtime operations behind a + trusted in-process service boundary. +- `runa` provides the local CLI client for that boundary. + +The image API is process-local and intended for trusted callers. It has no +transport, authentication, or authorization layer; do not expose it directly +to untrusted clients. +Its install operation also supports the runtime's explicit host-provided +state-migration closure. `behavior_source` retrieves the exact original +published source for a selected behavior revision. An implementation is published once under an ID. Actors reference that exact implementation. Implementation IDs use source content and the compile