feat: define versioned Runa protocol DTOs
Add the runa-protocol crate with version-1 request, response, and event DTOs, serde serialization, and basic envelope and input validation. Document the wire contract and clarify that transport, dispatch, and authentication remain future work. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
515a6439b5
commit
a65c66db01
|
|
@ -67,6 +67,7 @@ proposed capabilities as plans, not existing behavior.
|
||||||
- `crates/runa-actor/`: actor value and lifecycle contracts.
|
- `crates/runa-actor/`: actor value and lifecycle contracts.
|
||||||
- `crates/runa-runtime/`: single-worker scheduling and in-memory transitions.
|
- `crates/runa-runtime/`: single-worker scheduling and in-memory transitions.
|
||||||
- `crates/runa-image-api/`: trusted in-process services for operating on an image.
|
- `crates/runa-image-api/`: trusted in-process services for operating on an image.
|
||||||
|
- `crates/runa-protocol/`: versioned transport-neutral request, response, and event DTOs.
|
||||||
- `runa-std/actors/`: default Rune actor scripts loaded by the local CLI.
|
- `runa-std/actors/`: default Rune actor scripts loaded by the local CLI.
|
||||||
- `docs/architecture.md`: architecture, roadmap, risks, and acceptance scenario.
|
- `docs/architecture.md`: architecture, roadmap, risks, and acceptance scenario.
|
||||||
- `docs/decisions/`: architectural decisions.
|
- `docs/decisions/`: architectural decisions.
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,8 @@ All notable changes to this project will be documented in this file.
|
||||||
instruction pointer, and call-frame depth.
|
instruction pointer, and call-frame depth.
|
||||||
- Trusted in-process `runa-image-api` service boundary for code and actor
|
- Trusted in-process `runa-image-api` service boundary for code and actor
|
||||||
operations, now used by the local CLI.
|
operations, now used by the local CLI.
|
||||||
|
- Versioned, transport-neutral `runa-protocol` request, response, and event
|
||||||
|
DTOs with serde support and request validation.
|
||||||
- Execution-model decision and Phase 1 runtime documentation.
|
- Execution-model decision and Phase 1 runtime documentation.
|
||||||
- Lifecycle tests proving failed-message retry from committed state and
|
- Lifecycle tests proving failed-message retry from committed state and
|
||||||
stop/restart preservation of reserved-first FIFO order.
|
stop/restart preservation of reserved-first FIFO order.
|
||||||
|
|
|
||||||
|
|
@ -431,6 +431,14 @@ dependencies = [
|
||||||
"runa-runtime",
|
"runa-runtime",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "runa-protocol"
|
||||||
|
version = "0.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "runa-rune"
|
name = "runa-rune"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
|
|
@ -590,6 +598,19 @@ dependencies = [
|
||||||
"syn 3.0.9",
|
"syn 3.0.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.154"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"memchr",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
"zmij",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sharded-slab"
|
name = "sharded-slab"
|
||||||
version = "0.1.7"
|
version = "0.1.7"
|
||||||
|
|
@ -800,3 +821,9 @@ dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.119",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zmij"
|
||||||
|
version = "1.0.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ members = [
|
||||||
"crates/runa-code",
|
"crates/runa-code",
|
||||||
"crates/runa-core",
|
"crates/runa-core",
|
||||||
"crates/runa-image-api",
|
"crates/runa-image-api",
|
||||||
|
"crates/runa-protocol",
|
||||||
"crates/runa-rune",
|
"crates/runa-rune",
|
||||||
"crates/runa-runtime",
|
"crates/runa-runtime",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,8 @@ network development API, graphical world, and VS Code extension are not
|
||||||
implemented yet. The code library now supports an in-memory versioned behavior
|
implemented yet. The code library now supports an in-memory versioned behavior
|
||||||
registry, and the CLI exposes publishing, history, rollback, safe-boundary
|
registry, and the CLI exposes publishing, history, rollback, safe-boundary
|
||||||
installation, pause/resume, and cancellation through a trusted in-process image
|
installation, pause/resume, and cancellation through a trusted in-process image
|
||||||
API. It is not a network API and does not provide authentication.
|
API. A versioned transport-neutral protocol contract is now defined, but no
|
||||||
|
network server or authentication is implemented.
|
||||||
|
|
||||||
The prototype uses worker-local invocation stacks and cooperative instruction
|
The prototype uses worker-local invocation stacks and cooperative instruction
|
||||||
slices, and actor snapshots report basic active-execution progress. Native
|
slices, and actor snapshots report basic active-execution progress. Native
|
||||||
|
|
@ -106,9 +107,11 @@ test task runs `cargo test --workspace`.
|
||||||
│ ├── runa-code/ # Source identities and compile cache
|
│ ├── runa-code/ # Source identities and compile cache
|
||||||
│ ├── runa-rune/ # Phase 0 Rune experiments
|
│ ├── 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
|
│ ├── runa-image-api/ # Trusted in-process image operations
|
||||||
|
│ └── runa-protocol/ # Versioned transport-neutral wire DTOs
|
||||||
├── docs/
|
├── docs/
|
||||||
│ ├── architecture.md
|
│ ├── architecture.md
|
||||||
|
│ ├── protocol.md
|
||||||
│ └── decisions/
|
│ └── decisions/
|
||||||
├── runa-std/
|
├── runa-std/
|
||||||
│ ├── core/
|
│ ├── core/
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
[package]
|
||||||
|
name = "runa-protocol"
|
||||||
|
version = "0.0.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { version = "1.0.229", features = ["derive"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
serde_json = "1"
|
||||||
|
|
@ -0,0 +1,440 @@
|
||||||
|
//! Versioned, transport-neutral wire contracts for Runa clients and servers.
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
pub const PROTOCOL_VERSION: u16 = 1;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct Request {
|
||||||
|
pub version: u16,
|
||||||
|
pub request_id: String,
|
||||||
|
pub command: Command,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Request {
|
||||||
|
pub fn validate(&self) -> Result<(), ProtocolError> {
|
||||||
|
if self.version != PROTOCOL_VERSION {
|
||||||
|
return Err(ProtocolError::new(
|
||||||
|
ErrorCode::UnsupportedVersion,
|
||||||
|
format!("unsupported protocol version {}", self.version),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if self.request_id.trim().is_empty() {
|
||||||
|
return Err(ProtocolError::new(
|
||||||
|
ErrorCode::InvalidRequest,
|
||||||
|
"request_id must not be empty",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
match &self.command {
|
||||||
|
Command::PublishBehavior {
|
||||||
|
name,
|
||||||
|
expected_revision,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
validate_name(name)?;
|
||||||
|
validate_optional_revision(*expected_revision)?;
|
||||||
|
}
|
||||||
|
Command::RollbackBehavior {
|
||||||
|
name,
|
||||||
|
expected_revision,
|
||||||
|
target_revision,
|
||||||
|
} => {
|
||||||
|
validate_name(name)?;
|
||||||
|
validate_revision(*expected_revision)?;
|
||||||
|
validate_revision(*target_revision)?;
|
||||||
|
}
|
||||||
|
Command::GetBehavior { name } => validate_name(name)?,
|
||||||
|
Command::GetBehaviorSource { name, revision } => {
|
||||||
|
validate_name(name)?;
|
||||||
|
validate_revision(*revision)?;
|
||||||
|
}
|
||||||
|
Command::Spawn {
|
||||||
|
behavior_name,
|
||||||
|
mailbox_capacity,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
validate_name(behavior_name)?;
|
||||||
|
if *mailbox_capacity == 0 {
|
||||||
|
return Err(invalid_request("mailbox_capacity must be positive"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Command::Send { actor_id, .. }
|
||||||
|
| Command::InspectActor { actor_id }
|
||||||
|
| Command::StopActor { actor_id }
|
||||||
|
| Command::RestartActor { actor_id }
|
||||||
|
| Command::PauseActor { actor_id }
|
||||||
|
| Command::ResumeActor { actor_id }
|
||||||
|
| Command::CancelActor { actor_id } => validate_actor_id(*actor_id)?,
|
||||||
|
Command::InstallBehavior {
|
||||||
|
actor_id,
|
||||||
|
behavior_name,
|
||||||
|
} => {
|
||||||
|
validate_actor_id(*actor_id)?;
|
||||||
|
validate_name(behavior_name)?;
|
||||||
|
}
|
||||||
|
Command::Tick {
|
||||||
|
instruction_budget, ..
|
||||||
|
} if *instruction_budget == 0 => {
|
||||||
|
return Err(invalid_request("instruction_budget must be positive"));
|
||||||
|
}
|
||||||
|
Command::GetBehaviors
|
||||||
|
| Command::ListActors
|
||||||
|
| Command::GetEvents
|
||||||
|
| Command::Tick { .. } => {}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "operation", content = "parameters", rename_all = "snake_case")]
|
||||||
|
pub enum Command {
|
||||||
|
PublishBehavior {
|
||||||
|
name: String,
|
||||||
|
expected_revision: Option<u64>,
|
||||||
|
source: String,
|
||||||
|
},
|
||||||
|
RollbackBehavior {
|
||||||
|
name: String,
|
||||||
|
expected_revision: u64,
|
||||||
|
target_revision: u64,
|
||||||
|
},
|
||||||
|
GetBehavior {
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
|
GetBehaviors,
|
||||||
|
GetBehaviorSource {
|
||||||
|
name: String,
|
||||||
|
revision: u64,
|
||||||
|
},
|
||||||
|
Spawn {
|
||||||
|
behavior_name: String,
|
||||||
|
state: i64,
|
||||||
|
mailbox_capacity: usize,
|
||||||
|
},
|
||||||
|
Send {
|
||||||
|
actor_id: u64,
|
||||||
|
message: MessageDto,
|
||||||
|
},
|
||||||
|
Tick {
|
||||||
|
ticks: usize,
|
||||||
|
instruction_budget: usize,
|
||||||
|
},
|
||||||
|
InspectActor {
|
||||||
|
actor_id: u64,
|
||||||
|
},
|
||||||
|
ListActors,
|
||||||
|
GetEvents,
|
||||||
|
StopActor {
|
||||||
|
actor_id: u64,
|
||||||
|
},
|
||||||
|
RestartActor {
|
||||||
|
actor_id: u64,
|
||||||
|
},
|
||||||
|
PauseActor {
|
||||||
|
actor_id: u64,
|
||||||
|
},
|
||||||
|
ResumeActor {
|
||||||
|
actor_id: u64,
|
||||||
|
},
|
||||||
|
CancelActor {
|
||||||
|
actor_id: u64,
|
||||||
|
},
|
||||||
|
InstallBehavior {
|
||||||
|
actor_id: u64,
|
||||||
|
behavior_name: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct Response {
|
||||||
|
pub version: u16,
|
||||||
|
pub request_id: String,
|
||||||
|
pub result: Result<Reply, ProtocolError>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "kind", content = "data", rename_all = "snake_case")]
|
||||||
|
pub enum Reply {
|
||||||
|
Acknowledged,
|
||||||
|
Behavior(BehaviorDto),
|
||||||
|
Behaviors(Vec<BehaviorDto>),
|
||||||
|
BehaviorSource { source: String },
|
||||||
|
ActorSpawned { actor_id: u64 },
|
||||||
|
Actor(ActorSnapshotDto),
|
||||||
|
Actors(Vec<ActorSnapshotDto>),
|
||||||
|
Events(Vec<EventDto>),
|
||||||
|
TickCompleted { ticks: usize },
|
||||||
|
BehaviorInstalled { implementation_id: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct ProtocolEvent {
|
||||||
|
pub version: u16,
|
||||||
|
pub event: EventDto,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "event", content = "data", rename_all = "snake_case")]
|
||||||
|
pub enum EventDto {
|
||||||
|
Spawned {
|
||||||
|
actor_id: u64,
|
||||||
|
},
|
||||||
|
Committed {
|
||||||
|
actor_id: u64,
|
||||||
|
},
|
||||||
|
Failed {
|
||||||
|
actor_id: u64,
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
|
Stopped {
|
||||||
|
actor_id: u64,
|
||||||
|
},
|
||||||
|
Restarted {
|
||||||
|
actor_id: u64,
|
||||||
|
},
|
||||||
|
Paused {
|
||||||
|
actor_id: u64,
|
||||||
|
},
|
||||||
|
Resumed {
|
||||||
|
actor_id: u64,
|
||||||
|
},
|
||||||
|
InvocationCancelled {
|
||||||
|
actor_id: u64,
|
||||||
|
},
|
||||||
|
BehaviorInstalled {
|
||||||
|
actor_id: u64,
|
||||||
|
implementation_id: String,
|
||||||
|
},
|
||||||
|
StateMigrated {
|
||||||
|
actor_id: u64,
|
||||||
|
previous_state: i64,
|
||||||
|
current_state: i64,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct BehaviorDto {
|
||||||
|
pub name: String,
|
||||||
|
pub revision: u64,
|
||||||
|
pub current_implementation_id: String,
|
||||||
|
pub history: Vec<BehaviorVersionDto>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct BehaviorVersionDto {
|
||||||
|
pub revision: u64,
|
||||||
|
pub implementation_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct MessageDto {
|
||||||
|
pub kind: i64,
|
||||||
|
pub value: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct ActorSnapshotDto {
|
||||||
|
pub actor_id: u64,
|
||||||
|
pub mailbox_id: u64,
|
||||||
|
pub implementation_id: String,
|
||||||
|
pub incarnation: u64,
|
||||||
|
pub state: i64,
|
||||||
|
pub status: ActorStatusDto,
|
||||||
|
pub execution: Option<ExecutionSnapshotDto>,
|
||||||
|
pub queued: Vec<MessageDto>,
|
||||||
|
pub reserved: Option<MessageDto>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "status", content = "message", rename_all = "snake_case")]
|
||||||
|
pub enum ActorStatusDto {
|
||||||
|
Idle,
|
||||||
|
Running,
|
||||||
|
Awaiting,
|
||||||
|
Paused,
|
||||||
|
Stopped,
|
||||||
|
Failed(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct ExecutionSnapshotDto {
|
||||||
|
pub vm_steps: u64,
|
||||||
|
pub instruction_pointer: usize,
|
||||||
|
pub call_depth: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum ErrorCode {
|
||||||
|
InvalidRequest,
|
||||||
|
UnsupportedVersion,
|
||||||
|
NotFound,
|
||||||
|
Conflict,
|
||||||
|
CapacityExceeded,
|
||||||
|
RuntimeFailure,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct ProtocolError {
|
||||||
|
pub code: ErrorCode,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProtocolError {
|
||||||
|
pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
code,
|
||||||
|
message: message.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_name(name: &str) -> Result<(), ProtocolError> {
|
||||||
|
if name.trim().is_empty() {
|
||||||
|
return Err(invalid_request("behavior name must not be empty"));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_actor_id(actor_id: u64) -> Result<(), ProtocolError> {
|
||||||
|
if actor_id == 0 {
|
||||||
|
return Err(invalid_request("actor_id must be positive"));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_revision(revision: u64) -> Result<(), ProtocolError> {
|
||||||
|
if revision == 0 {
|
||||||
|
return Err(invalid_request("revision must be positive"));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_optional_revision(revision: Option<u64>) -> Result<(), ProtocolError> {
|
||||||
|
if let Some(revision) = revision {
|
||||||
|
validate_revision(revision)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn invalid_request(message: impl Into<String>) -> ProtocolError {
|
||||||
|
ProtocolError::new(ErrorCode::InvalidRequest, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn request(command: Command) -> Request {
|
||||||
|
Request {
|
||||||
|
version: PROTOCOL_VERSION,
|
||||||
|
request_id: "req-1".to_owned(),
|
||||||
|
command,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_uses_a_stable_tagged_wire_shape() {
|
||||||
|
let value = serde_json::to_value(request(Command::Send {
|
||||||
|
actor_id: 7,
|
||||||
|
message: MessageDto { kind: 2, value: 9 },
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
value,
|
||||||
|
serde_json::json!({
|
||||||
|
"version": PROTOCOL_VERSION,
|
||||||
|
"request_id": "req-1",
|
||||||
|
"command": {
|
||||||
|
"operation": "send",
|
||||||
|
"parameters": {
|
||||||
|
"actor_id": 7,
|
||||||
|
"message": { "kind": 2, "value": 9 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn requests_validate_versions_and_required_positive_values() {
|
||||||
|
let mut request = request(Command::Spawn {
|
||||||
|
behavior_name: "counter".to_owned(),
|
||||||
|
state: 0,
|
||||||
|
mailbox_capacity: 1,
|
||||||
|
});
|
||||||
|
assert_eq!(request.validate(), Ok(()));
|
||||||
|
|
||||||
|
request.version += 1;
|
||||||
|
assert_eq!(
|
||||||
|
request.validate().unwrap_err().code,
|
||||||
|
ErrorCode::UnsupportedVersion
|
||||||
|
);
|
||||||
|
|
||||||
|
request.version = PROTOCOL_VERSION;
|
||||||
|
request.command = Command::Spawn {
|
||||||
|
behavior_name: "counter".to_owned(),
|
||||||
|
state: 0,
|
||||||
|
mailbox_capacity: 0,
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
request.validate().unwrap_err().code,
|
||||||
|
ErrorCode::InvalidRequest
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn requests_reject_empty_correlation_ids_and_zero_actor_ids() {
|
||||||
|
let mut request = request(Command::GetEvents);
|
||||||
|
request.request_id = " ".to_owned();
|
||||||
|
assert_eq!(
|
||||||
|
request.validate().unwrap_err().code,
|
||||||
|
ErrorCode::InvalidRequest
|
||||||
|
);
|
||||||
|
|
||||||
|
request.request_id = "req-2".to_owned();
|
||||||
|
request.command = Command::StopActor { actor_id: 0 };
|
||||||
|
assert_eq!(
|
||||||
|
request.validate().unwrap_err().code,
|
||||||
|
ErrorCode::InvalidRequest
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn response_round_trips_with_typed_error_or_reply() {
|
||||||
|
let response = Response {
|
||||||
|
version: PROTOCOL_VERSION,
|
||||||
|
request_id: "req-1".to_owned(),
|
||||||
|
result: Err(ProtocolError::new(ErrorCode::NotFound, "unknown actor")),
|
||||||
|
};
|
||||||
|
let encoded = serde_json::to_string(&response).unwrap();
|
||||||
|
let decoded: Response = serde_json::from_str(&encoded).unwrap();
|
||||||
|
assert_eq!(decoded, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn event_envelopes_are_versioned_and_serializable() {
|
||||||
|
let event = ProtocolEvent {
|
||||||
|
version: PROTOCOL_VERSION,
|
||||||
|
event: EventDto::StateMigrated {
|
||||||
|
actor_id: 3,
|
||||||
|
previous_state: 4,
|
||||||
|
current_state: 40,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let encoded = serde_json::to_string(&event).unwrap();
|
||||||
|
let decoded: ProtocolEvent = serde_json::from_str(&encoded).unwrap();
|
||||||
|
assert_eq!(decoded, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -309,7 +309,7 @@ The diagram is conceptual; Cargo dependencies should be kept minimal. A dependen
|
||||||
| `runa-rune` | Rune compilation, VM execution, suspension, interruption hooks, language-specific diagnostics | `runa-core`, Rune libraries |
|
| `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-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` | Trusted in-process application services for spawning, inspecting, sending, editing, and upgrading | `runa-core`, `runa-actor`, `runa-code`, `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-protocol` | Versioned transport-neutral request/response/event DTOs and validation | `serde` |
|
||||||
| `runa-server` | WebSocket/JSON-RPC transport, sessions, authentication, subscriptions | `runa-protocol`, `runa-image-api` |
|
| `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 |
|
| `runa-graphics` | Native window, rendering and input mechanisms; no actor policy | `runa-core` if necessary |
|
||||||
| `runa-world` | Morphic-style graphical composition and in-world development UI | `runa-image-api`, `runa-graphics` |
|
| `runa-world` | Morphic-style graphical composition and in-world development UI | `runa-image-api`, `runa-graphics` |
|
||||||
|
|
@ -325,8 +325,9 @@ authentication or authorization; callers must be trusted by the host.
|
||||||
The initial `runa-image-api` provides process-local behavior source retrieval,
|
The initial `runa-image-api` provides process-local behavior source retrieval,
|
||||||
publication, history and rollback, actor lifecycle and messaging, snapshots,
|
publication, history and rollback, actor lifecycle and messaging, snapshots,
|
||||||
ticks, and event retrieval. The CLI uses these application services rather
|
ticks, and event retrieval. The CLI uses these application services rather
|
||||||
than reaching into the code store and runtime directly. Versioned wire DTOs
|
than reaching into the code store and runtime directly. `runa-protocol` now
|
||||||
and transport are the next separate layer.
|
defines versioned, serializable request, response, and event DTOs without
|
||||||
|
depending on a transport or the runtime implementation.
|
||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
|
|
@ -430,9 +431,10 @@ inspection remain future work.
|
||||||
- Headless server, versioned protocol, actor and mailbox operations, source editing, events.
|
- Headless server, versioned protocol, actor and mailbox operations, source editing, events.
|
||||||
- CLI client that can spawn, inspect, send messages, and replace behaviors.
|
- 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 first Phase 5 increments are a trusted in-process application API used by
|
||||||
the CLI. A transport-neutral versioned protocol and network server are not yet
|
the CLI and a transport-neutral versioned protocol contract. The protocol is a
|
||||||
implemented.
|
Rust DTO crate with serde serialization; a network server and authentication
|
||||||
|
are not yet implemented.
|
||||||
|
|
||||||
### Phase 6 — Graphical world
|
### Phase 6 — Graphical world
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
# Runa Protocol
|
||||||
|
|
||||||
|
`runa-protocol` defines versioned request, response, and event data contracts
|
||||||
|
for clients and a future server. It is independent of transport and runtime
|
||||||
|
implementation; the current crate does not dispatch requests, expose a network
|
||||||
|
endpoint, or provide authentication.
|
||||||
|
|
||||||
|
## Version 1
|
||||||
|
|
||||||
|
Every request and response carries `version: 1`. Requests also carry a
|
||||||
|
non-empty `request_id` used to correlate the response. A server must reject
|
||||||
|
unsupported versions with `unsupported_version` rather than interpreting a
|
||||||
|
request using a different contract. Events carry the protocol version and do
|
||||||
|
not have a request ID.
|
||||||
|
|
||||||
|
Requests have a tagged command object:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"request_id": "req-1",
|
||||||
|
"command": {
|
||||||
|
"operation": "send",
|
||||||
|
"parameters": {
|
||||||
|
"actor_id": 7,
|
||||||
|
"message": { "kind": 2, "value": 9 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The version 1 operations are behavior publication, rollback, lookup, source
|
||||||
|
retrieval, actor spawn, send, tick, inspect, actor listing, event retrieval,
|
||||||
|
stop, restart, pause, resume, cancellation, and behavior installation.
|
||||||
|
Migration closures are intentionally excluded: they are host callbacks, not
|
||||||
|
portable wire values.
|
||||||
|
|
||||||
|
Responses echo the request ID and carry either a typed reply or a protocol
|
||||||
|
error. Events use a separate versioned envelope. Actor and behavior snapshots
|
||||||
|
are wire DTOs, not serialized runtime objects; implementation identities are
|
||||||
|
represented by their stable display strings. Runtime errors and validation
|
||||||
|
errors must be mapped by a future server to the defined error codes without
|
||||||
|
claiming a more specific category than it can establish.
|
||||||
|
|
||||||
|
The current validation checks the protocol version, request ID, positive
|
||||||
|
actor/revision identifiers where required, non-empty behavior names, mailbox
|
||||||
|
capacity, and instruction budget. Behavior-name grammar, actor existence,
|
||||||
|
current behavior revisions, mailbox capacity at send time, and all runtime
|
||||||
|
invariants remain authoritative at the application-service boundary.
|
||||||
|
|
@ -49,6 +49,8 @@ The default per-actor slice budget is 32 instructions.
|
||||||
state, scheduling, message effects and lifecycle coordination.
|
state, scheduling, message effects and lifecycle coordination.
|
||||||
- `runa-image-api` composes code publication and runtime operations behind a
|
- `runa-image-api` composes code publication and runtime operations behind a
|
||||||
trusted in-process service boundary.
|
trusted in-process service boundary.
|
||||||
|
- `runa-protocol` defines versioned transport-neutral request, response, and
|
||||||
|
event DTOs; it does not dispatch requests or access runtime state.
|
||||||
- `runa` provides the local CLI client for that boundary.
|
- `runa` provides the local CLI client for that boundary.
|
||||||
|
|
||||||
The image API is process-local and intended for trusted callers. It has no
|
The image API is process-local and intended for trusted callers. It has no
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue