actr-framework 0.3.0

Actor-RTC framework core (stub for code generation testing)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
//! DynClib-only C ABI for actr workloads.
//!
//! This module is the handwritten C ABI used by the DynClib workload variant
//! (loaded via dlopen). The WASM variant does NOT consume these types — it
//! goes through wit-bindgen-generated code against `core/framework/wit/actr-workload.wit`.
//!
//! Do NOT add wasm-path code paths here. Do NOT reference this module from
//! the wasm guest adapter. This module is kept in sync with the WIT contract
//! by `tools/wit-lint`.

use crate::Dest;
use actr_protocol::prost::Message as ProstMessage;
use actr_protocol::{ActrError, ActrId, ActrType, DataStream, PayloadType};

/// ABI error codes.
pub mod code {
    /// Operation succeeded.
    pub const SUCCESS: i32 = 0;
    /// Generic unrecoverable error.
    pub const GENERIC_ERROR: i32 = -1;
    /// Initialization failed.
    pub const INIT_FAILED: i32 = -2;
    /// Message handling failed.
    pub const HANDLE_FAILED: i32 = -3;
    /// Memory allocation failed.
    pub const ALLOC_FAILED: i32 = -4;
    /// Protocol / codec error.
    pub const PROTOCOL_ERROR: i32 = -5;
    /// Guest-provided reply buffer is too small.
    pub const BUFFER_TOO_SMALL: i32 = -6;
    /// Unsupported runtime operation code.
    pub const UNSUPPORTED_OP: i32 = -7;
}

/// Internal ABI version numbers.
pub mod version {
    /// ABI version 1.
    pub const V1: u32 = 1;
}

/// Runtime operation codes carried inside [`AbiFrame`].
pub mod op {
    pub const HOST_CALL: u32 = 1;
    pub const HOST_TELL: u32 = 2;
    pub const HOST_CALL_RAW: u32 = 3;
    pub const HOST_DISCOVER: u32 = 4;
    pub const HOST_REGISTER_STREAM: u32 = 5;
    pub const HOST_UNREGISTER_STREAM: u32 = 6;
    pub const HOST_SEND_DATA_STREAM: u32 = 7;
    pub const GUEST_HANDLE: u32 = 101;
    pub const GUEST_DATA_STREAM: u32 = 102;
    pub const GUEST_LIFECYCLE: u32 = 103;
    pub const GUEST_HOOK: u32 = 104;
}

/// Lifecycle hook identifiers carried by [`GuestLifecycleV1`].
pub mod lifecycle_hook {
    pub const ON_START: u32 = 1;
    pub const ON_READY: u32 = 2;
    pub const ON_STOP: u32 = 3;
}

/// Observation hook identifiers carried by [`GuestHookV1`].
pub mod runtime_hook {
    pub const ON_SIGNALING_CONNECTING: u32 = 1;
    pub const ON_SIGNALING_CONNECTED: u32 = 2;
    pub const ON_SIGNALING_DISCONNECTED: u32 = 3;
    pub const ON_WEBSOCKET_CONNECTING: u32 = 4;
    pub const ON_WEBSOCKET_CONNECTED: u32 = 5;
    pub const ON_WEBSOCKET_DISCONNECTED: u32 = 6;
    pub const ON_WEBRTC_CONNECTING: u32 = 7;
    pub const ON_WEBRTC_CONNECTED: u32 = 8;
    pub const ON_WEBRTC_DISCONNECTED: u32 = 9;
    pub const ON_CREDENTIAL_RENEWED: u32 = 10;
    pub const ON_CREDENTIAL_EXPIRING: u32 = 11;
    pub const ON_MAILBOX_BACKPRESSURE: u32 = 12;
}

/// Dedicated payload used by `actr_init`.
#[derive(Clone, PartialEq, prost::Message)]
pub struct InitPayloadV1 {
    #[prost(uint32, tag = "1")]
    pub version: u32,
    #[prost(string, tag = "2")]
    pub actr_type: String,
    #[prost(bytes = "vec", tag = "3")]
    pub credential: Vec<u8>,
    #[prost(bytes = "vec", tag = "4")]
    pub actor_id: Vec<u8>,
    #[prost(uint32, tag = "5")]
    pub realm_id: u32,
}

/// Runtime frame used by both host->guest and guest->host invocation.
///
/// TODO: This type is temporarily `pub` because `actr_hyper` still performs
/// cross-crate host-side ABI encoding and decoding through
/// `actr_framework::guest::dynclib_abi`. Once the shared runtime ABI types
/// are moved to a better ownership boundary, narrow this visibility to the
/// intended internal-only surface.
#[derive(Clone, PartialEq, prost::Message)]
pub struct AbiFrame {
    #[prost(uint32, tag = "1")]
    pub abi_version: u32,
    #[prost(uint32, tag = "2")]
    pub op: u32,
    #[prost(bytes = "vec", tag = "3")]
    pub payload: Vec<u8>,
}

/// Runtime reply frame.
///
/// TODO: Keep visibility aligned with [`AbiFrame`]. This is public only as a
/// temporary crate-topology workaround while host-side runtime code lives in
/// `actr_hyper`.
#[derive(Clone, PartialEq, prost::Message)]
pub struct AbiReply {
    #[prost(uint32, tag = "1")]
    pub abi_version: u32,
    #[prost(int32, tag = "2")]
    pub status: i32,
    #[prost(bytes = "vec", tag = "3")]
    pub payload: Vec<u8>,
}

/// Invocation context injected by Hyper before entering guest handle logic.
#[derive(Clone, PartialEq, prost::Message)]
pub struct InvocationContextV1 {
    #[prost(message, required, tag = "1")]
    pub self_id: ActrId,
    #[prost(message, optional, tag = "2")]
    pub caller_id: Option<ActrId>,
    #[prost(string, tag = "3")]
    pub request_id: String,
}

/// Runtime host->guest handle payload.
#[derive(Clone, PartialEq, prost::Message)]
pub struct GuestHandleV1 {
    #[prost(message, required, tag = "1")]
    pub ctx: InvocationContextV1,
    #[prost(bytes = "vec", tag = "2")]
    pub rpc_envelope: Vec<u8>,
}

/// Runtime host->guest DataStream payload.
#[derive(Clone, PartialEq, prost::Message)]
pub struct GuestDataStreamV1 {
    #[prost(message, required, tag = "1")]
    pub chunk: DataStream,
    #[prost(message, required, tag = "2")]
    pub sender: ActrId,
}

/// Runtime host->guest lifecycle hook payload.
#[derive(Clone, PartialEq, prost::Message)]
pub struct GuestLifecycleV1 {
    #[prost(message, required, tag = "1")]
    pub ctx: InvocationContextV1,
    #[prost(uint32, tag = "2")]
    pub hook: u32,
}

/// Wall-clock timestamp represented as seconds + nanoseconds since Unix epoch.
#[derive(Clone, PartialEq, prost::Message)]
pub struct TimestampV1 {
    #[prost(uint64, tag = "1")]
    pub seconds: u64,
    #[prost(uint32, tag = "2")]
    pub nanoseconds: u32,
}

/// Peer-scoped event payload for WebSocket / WebRTC hooks.
#[derive(Clone, PartialEq, prost::Message)]
pub struct PeerEventV1 {
    #[prost(message, required, tag = "1")]
    pub peer: ActrId,
    #[prost(bool, optional, tag = "2")]
    pub relayed: Option<bool>,
}

/// Credential lifecycle event payload.
#[derive(Clone, PartialEq, prost::Message)]
pub struct CredentialEventV1 {
    #[prost(message, required, tag = "1")]
    pub new_expiry: TimestampV1,
}

/// Mailbox backpressure event payload.
#[derive(Clone, PartialEq, prost::Message)]
pub struct BackpressureEventV1 {
    #[prost(uint64, tag = "1")]
    pub queue_len: u64,
    #[prost(uint64, tag = "2")]
    pub threshold: u64,
}

/// Runtime host->guest observation hook payload.
#[derive(Clone, PartialEq, prost::Message)]
pub struct GuestHookV1 {
    #[prost(message, required, tag = "1")]
    pub ctx: InvocationContextV1,
    #[prost(uint32, tag = "2")]
    pub hook: u32,
    #[prost(message, optional, tag = "3")]
    pub peer: Option<PeerEventV1>,
    #[prost(message, optional, tag = "4")]
    pub credential: Option<CredentialEventV1>,
    #[prost(message, optional, tag = "5")]
    pub backpressure: Option<BackpressureEventV1>,
}

/// ABI-level destination encoding (replaces hand-rolled 0x00/0x01/0x02 byte protocol).
#[derive(Clone, PartialEq, prost::Message)]
pub struct DestV1 {
    #[prost(oneof = "DestKind", tags = "1, 2, 3")]
    pub kind: Option<DestKind>,
}

/// Destination variants carried inside [`DestV1`].
#[derive(Clone, PartialEq, prost::Oneof)]
pub enum DestKind {
    #[prost(bool, tag = "1")]
    Shell(bool),
    #[prost(bool, tag = "2")]
    Local(bool),
    #[prost(message, tag = "3")]
    Actor(ActrId),
}

impl DestV1 {
    /// Construct a shell destination.
    pub fn shell() -> Self {
        Self {
            kind: Some(DestKind::Shell(true)),
        }
    }

    /// Construct a local destination.
    pub fn local() -> Self {
        Self {
            kind: Some(DestKind::Local(true)),
        }
    }

    /// Construct an actor destination.
    pub fn actor(id: ActrId) -> Self {
        Self {
            kind: Some(DestKind::Actor(id)),
        }
    }

    /// Convert the ABI destination into the framework destination.
    pub fn try_into_dest(self) -> Result<Dest, ActrError> {
        match self.kind {
            Some(DestKind::Shell(_)) => Ok(Dest::Shell),
            Some(DestKind::Local(_)) => Ok(Dest::Local),
            Some(DestKind::Actor(id)) => Ok(Dest::Actor(id)),
            None => Err(ActrError::DecodeFailure(
                "destination kind is missing".into(),
            )),
        }
    }
}

/// Runtime guest->host call payload.
#[derive(Clone, PartialEq, prost::Message)]
pub struct HostCallV1 {
    #[prost(string, tag = "1")]
    pub route_key: String,
    #[prost(message, required, tag = "2")]
    pub dest: DestV1,
    #[prost(bytes = "vec", tag = "3")]
    pub payload: Vec<u8>,
}

/// Runtime guest->host tell payload.
#[derive(Clone, PartialEq, prost::Message)]
pub struct HostTellV1 {
    #[prost(string, tag = "1")]
    pub route_key: String,
    #[prost(message, required, tag = "2")]
    pub dest: DestV1,
    #[prost(bytes = "vec", tag = "3")]
    pub payload: Vec<u8>,
}

/// Runtime guest->host raw call payload.
#[derive(Clone, PartialEq, prost::Message)]
pub struct HostCallRawV1 {
    #[prost(string, tag = "1")]
    pub route_key: String,
    #[prost(message, required, tag = "2")]
    pub target: ActrId,
    #[prost(bytes = "vec", tag = "3")]
    pub payload: Vec<u8>,
}

/// Runtime guest->host discovery payload.
#[derive(Clone, PartialEq, prost::Message)]
pub struct HostDiscoverV1 {
    #[prost(message, required, tag = "1")]
    pub target_type: ActrType,
}

/// Runtime guest->host DataStream registration payload.
#[derive(Clone, PartialEq, prost::Message)]
pub struct HostRegisterStreamV1 {
    #[prost(string, tag = "1")]
    pub stream_id: String,
}

/// Runtime guest->host DataStream unregistration payload.
#[derive(Clone, PartialEq, prost::Message)]
pub struct HostUnregisterStreamV1 {
    #[prost(string, tag = "1")]
    pub stream_id: String,
}

/// Runtime guest->host DataStream send payload.
#[derive(Clone, PartialEq, prost::Message)]
pub struct HostSendDataStreamV1 {
    #[prost(message, required, tag = "1")]
    pub dest: DestV1,
    #[prost(message, required, tag = "2")]
    pub chunk: DataStream,
    #[prost(enumeration = "PayloadType", tag = "3")]
    pub payload_type: i32,
}

/// Payloads that can automatically construct runtime frames.
pub trait AbiPayload: ProstMessage + Default + Sized {
    const ABI_VERSION: u32;
    const OP: u32;

    fn to_frame(&self) -> Result<AbiFrame, i32> {
        let mut payload = Vec::new();
        self.encode(&mut payload)
            .map_err(|_| code::PROTOCOL_ERROR)?;

        Ok(AbiFrame {
            abi_version: Self::ABI_VERSION,
            op: Self::OP,
            payload,
        })
    }

    fn decode_payload(bytes: &[u8]) -> Result<Self, i32> {
        Self::decode(bytes).map_err(|_| code::PROTOCOL_ERROR)
    }
}

impl AbiPayload for HostCallV1 {
    const ABI_VERSION: u32 = version::V1;
    const OP: u32 = op::HOST_CALL;
}

impl AbiPayload for HostTellV1 {
    const ABI_VERSION: u32 = version::V1;
    const OP: u32 = op::HOST_TELL;
}

impl AbiPayload for HostCallRawV1 {
    const ABI_VERSION: u32 = version::V1;
    const OP: u32 = op::HOST_CALL_RAW;
}

impl AbiPayload for HostDiscoverV1 {
    const ABI_VERSION: u32 = version::V1;
    const OP: u32 = op::HOST_DISCOVER;
}

impl AbiPayload for HostRegisterStreamV1 {
    const ABI_VERSION: u32 = version::V1;
    const OP: u32 = op::HOST_REGISTER_STREAM;
}

impl AbiPayload for HostUnregisterStreamV1 {
    const ABI_VERSION: u32 = version::V1;
    const OP: u32 = op::HOST_UNREGISTER_STREAM;
}

impl AbiPayload for HostSendDataStreamV1 {
    const ABI_VERSION: u32 = version::V1;
    const OP: u32 = op::HOST_SEND_DATA_STREAM;
}

impl AbiPayload for GuestHandleV1 {
    const ABI_VERSION: u32 = version::V1;
    const OP: u32 = op::GUEST_HANDLE;
}

impl AbiPayload for GuestDataStreamV1 {
    const ABI_VERSION: u32 = version::V1;
    const OP: u32 = op::GUEST_DATA_STREAM;
}

impl AbiPayload for GuestLifecycleV1 {
    const ABI_VERSION: u32 = version::V1;
    const OP: u32 = op::GUEST_LIFECYCLE;
}

impl AbiPayload for GuestHookV1 {
    const ABI_VERSION: u32 = version::V1;
    const OP: u32 = op::GUEST_HOOK;
}

/// Encode a protobuf message into bytes.
pub fn encode_message<M: ProstMessage>(message: &M) -> Result<Vec<u8>, i32> {
    let mut out = Vec::new();
    message.encode(&mut out).map_err(|_| code::PROTOCOL_ERROR)?;
    Ok(out)
}

/// Decode a protobuf message from bytes.
pub fn decode_message<M: ProstMessage + Default>(bytes: &[u8]) -> Result<M, i32> {
    M::decode(bytes).map_err(|_| code::PROTOCOL_ERROR)
}

/// Encode a successful runtime reply.
pub fn success_reply(payload: Vec<u8>) -> Result<Vec<u8>, i32> {
    encode_message(&AbiReply {
        abi_version: version::V1,
        status: code::SUCCESS,
        payload,
    })
}

/// Encode a failed runtime reply.
pub fn error_reply(status: i32, message: impl Into<Vec<u8>>) -> Result<Vec<u8>, i32> {
    encode_message(&AbiReply {
        abi_version: version::V1,
        status,
        payload: message.into(),
    })
}

// ─────────────────────────────────────────────────────────────────────────────
// Shared guest-side helpers (used by both WASM and DynClib contexts)
// ─────────────────────────────────────────────────────────────────────────────

/// Convert a [`crate::Dest`] to the ABI-level [`DestV1`].
pub fn dest_to_v1(dest: &crate::Dest) -> DestV1 {
    match dest {
        crate::Dest::Shell => DestV1::shell(),
        crate::Dest::Local => DestV1::local(),
        crate::Dest::Actor(id) => DestV1::actor(id.clone()),
    }
}

/// Convert an ABI-level [`DestV1`] back to [`crate::Dest`].
///
/// Returns `None` if the `kind` field is absent.
pub fn dest_v1_to_dest(v1: &DestV1) -> Option<crate::Dest> {
    v1.clone().try_into_dest().ok()
}

/// Convert an ABI error code to an [`actr_protocol::ActrError`].
pub fn abi_error_to_actr(code_val: i32) -> actr_protocol::ActrError {
    use actr_protocol::ActrError;
    match code_val {
        code::GENERIC_ERROR => ActrError::Internal("host returned generic ABI error".into()),
        code::INIT_FAILED => ActrError::Internal("host initialization failed".into()),
        code::HANDLE_FAILED => ActrError::Internal("guest handle failed".into()),
        code::ALLOC_FAILED => ActrError::Internal("memory allocation failed".into()),
        code::PROTOCOL_ERROR => ActrError::DecodeFailure("ABI payload decode failed".into()),
        code::BUFFER_TOO_SMALL => {
            ActrError::Internal("reply buffer too small for host invoke".into())
        }
        code::UNSUPPORTED_OP => ActrError::NotImplemented("unsupported ABI operation".into()),
        other => ActrError::Internal(format!("unexpected ABI status code {other}")),
    }
}

/// Convert an [`AbiReply`] with a non-success status to an [`actr_protocol::ActrError`].
pub fn reply_to_actr_error(reply: AbiReply) -> actr_protocol::ActrError {
    use actr_protocol::ActrError;
    if reply.payload.is_empty() {
        return abi_error_to_actr(reply.status);
    }

    let message = String::from_utf8(reply.payload)
        .unwrap_or_else(|_| format!("guest returned status {}", reply.status));

    match reply.status {
        code::PROTOCOL_ERROR => ActrError::DecodeFailure(message),
        code::UNSUPPORTED_OP => ActrError::NotImplemented(message),
        _ => ActrError::Internal(message),
    }
}