Skip to content

Commit 16e2bcc

Browse files
committed
feat(rivetkit): add experimental Actor Runtime Socket
1 parent 663a5d0 commit 16e2bcc

43 files changed

Lines changed: 4678 additions & 118 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CLAUDE.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ Design constraints, invariants, and reference commands for the Rivet monorepo. F
2828

2929
When talking about "Rivet Actors" make sure to capitalize "Rivet Actor" as a proper noun and lowercase "actor" as a generic noun.
3030

31+
**Actor Runtime Socket** is the product name for the generic actor-local protocol. SQLite is its first capability and Unix sockets are its current transport; do not use "Actor SQLite UDS API" or "SQLite UDS" as the product name.
32+
3133
## Commands
3234

3335
### Build + test
@@ -196,7 +198,7 @@ When the user asks to track something in a note, store it in `~/.agents/notes/`
196198
- rivetkit (TypeScript) owns only: workflow engine, agent-os, client library, Zod schema validation for user-defined types, and actor definition types.
197199
- Errors use universal `RivetError` (group/code/message/metadata) at all boundaries. No custom error classes in TS.
198200
- CBOR serialization at all cross-language boundaries. JSON only for HTTP inspector endpoints.
199-
- Pegboard orchestrates actor exclusivity: at most one actor instance for a given actor id may be running or accessing that actor's KV at a time. `pegboard-envoy` and `envoy-client` may rely on this invariant and should not add separate KV concurrency fences for same-actor access; the lost-timeout + ping protocol is responsible for making overlapping actors impossible.
201+
- Pegboard orchestrates actor exclusivity: at most one actor instance for a given actor id may be running or accessing that actor's storage at a time. This is the actor single-writer invariant: a Rivet Actor is the single writer for both KV and SQLite. `pegboard-envoy`, `envoy-client`, and remote/wasm SQLite may rely on this invariant and must not add envoy-protocol lease keys, engine-side transaction ownership, or separate same-actor concurrency fences. Coordinate LocalNative and remote/wasm transactions through the same actor-local `rivetkit-core` coordinator before backend dispatch. Actor Runtime Socket `leaseKey` values identify connection-local transaction handles and never cross depot, envoy, or engine boundaries. The lost-timeout + ping protocol is responsible for making overlapping actor generations impossible.
200202

201203
### Monorepo orientation
202204

@@ -226,6 +228,7 @@ When the user asks to track something in a note, store it in `~/.agents/notes/`
226228
- Treat `envoy <-> pegboard-envoy` as untrusted.
227229
- Treat traffic inside the engine over `nats`, `fdb`, and other internal backends as trusted.
228230
- Treat `gateway`, `api`, `pegboard-envoy`, `nats`, `fdb`, and similar engine-internal services as one trusted internal boundary once traffic is inside the engine.
231+
- Treat a client connected to an actor-local Unix socket as a trusted, application-local peer, not an adversarial security boundary. Socket ownership, permissions, and per-generation lifetime provide isolation. Continue enforcing framing, resource bounds, cancellation, and shutdown behavior for correctness and faulty clients, but do not assign security severity based on malicious local traffic unless this trust model changes.
229232
- Validate and authorize all client-originated data at the engine edge before it reaches trusted internal systems.
230233
- Validate and authorize all envoy-originated data at `pegboard-envoy` before it reaches trusted internal systems.
231234

Cargo.lock

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ members = [
5959
"engine/packages/util-serde",
6060
"engine/packages/workflow-worker",
6161
"engine/sdks/rust/api-full",
62+
"engine/sdks/rust/actor-runtime-socket-protocol",
6263
"engine/sdks/rust/data",
6364
"engine/sdks/rust/envoy-client",
6465
"engine/sdks/rust/envoy-protocol",
@@ -586,6 +587,10 @@ members = [
586587
path = "engine/sdks/rust/envoy-protocol"
587588
version = "=2.3.4"
588589

590+
[workspace.dependencies.rivet-actor-runtime-socket-protocol]
591+
path = "engine/sdks/rust/actor-runtime-socket-protocol"
592+
version = "=2.3.4"
593+
589594
[workspace.dependencies.rivet-depot-protocol]
590595
path = "engine/sdks/rust/depot-protocol"
591596

engine/packages/depot-client/src/query.rs

Lines changed: 103 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
use std::error::Error;
12
use std::ffi::{CStr, CString};
3+
use std::fmt;
24
use std::os::raw::c_char;
35
use std::ptr;
46

@@ -10,9 +12,25 @@ use libsqlite3_sys::{
1012
sqlite3_bind_int64, sqlite3_bind_null, sqlite3_bind_text, sqlite3_changes, sqlite3_column_blob,
1113
sqlite3_column_bytes, sqlite3_column_count, sqlite3_column_double, sqlite3_column_int64,
1214
sqlite3_column_name, sqlite3_column_text, sqlite3_column_type, sqlite3_errmsg,
13-
sqlite3_finalize, sqlite3_last_insert_rowid, sqlite3_prepare_v2, sqlite3_step,
15+
sqlite3_extended_errcode, sqlite3_finalize, sqlite3_last_insert_rowid, sqlite3_prepare_v2,
16+
sqlite3_step,
1417
};
1518

19+
#[derive(Debug)]
20+
pub struct SqliteStatementError {
21+
pub code: i32,
22+
pub statement_index: u32,
23+
pub message: String,
24+
}
25+
26+
impl fmt::Display for SqliteStatementError {
27+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28+
f.write_str(&self.message)
29+
}
30+
}
31+
32+
impl Error for SqliteStatementError {}
33+
1634
pub fn execute_statement(
1735
db: *mut sqlite3,
1836
sql: &str,
@@ -22,7 +40,7 @@ pub fn execute_statement(
2240
let mut stmt = ptr::null_mut();
2341
let rc = unsafe { sqlite3_prepare_v2(db, c_sql.as_ptr(), -1, &mut stmt, ptr::null_mut()) };
2442
if rc != SQLITE_OK {
25-
return Err(sqlite_error(db, "failed to prepare sqlite statement"));
43+
return Err(sqlite_error(db, 0, "failed to prepare sqlite statement"));
2644
}
2745
if stmt.is_null() {
2846
return Ok(ExecResult { changes: 0 });
@@ -39,7 +57,7 @@ pub fn execute_statement(
3957
break;
4058
}
4159
if step_rc != SQLITE_ROW {
42-
return Err(sqlite_error(db, "failed to execute sqlite statement"));
60+
return Err(sqlite_error(db, 0, "failed to execute sqlite statement"));
4361
}
4462
}
4563

@@ -64,7 +82,7 @@ pub fn query_statement(
6482
let mut stmt = ptr::null_mut();
6583
let rc = unsafe { sqlite3_prepare_v2(db, c_sql.as_ptr(), -1, &mut stmt, ptr::null_mut()) };
6684
if rc != SQLITE_OK {
67-
return Err(sqlite_error(db, "failed to prepare sqlite query"));
85+
return Err(sqlite_error(db, 0, "failed to prepare sqlite query"));
6886
}
6987
if stmt.is_null() {
7088
return Ok(QueryResult {
@@ -87,7 +105,7 @@ pub fn query_statement(
87105
break;
88106
}
89107
if step_rc != SQLITE_ROW {
90-
return Err(sqlite_error(db, "failed to step sqlite query"));
108+
return Err(sqlite_error(db, 0, "failed to step sqlite query"));
91109
}
92110

93111
let mut row = Vec::with_capacity(columns.len());
@@ -119,6 +137,7 @@ pub fn execute_single_statement(
119137
if rc != SQLITE_OK {
120138
return Err(sqlite_error(
121139
db,
140+
0,
122141
"failed to prepare sqlite execute statement",
123142
));
124143
}
@@ -128,7 +147,12 @@ pub fn execute_single_statement(
128147
sqlite3_finalize(stmt);
129148
}
130149
}
131-
return Err(anyhow!("sqlite execute only supports a single statement"));
150+
return Err(SqliteStatementError {
151+
code: -1,
152+
statement_index: 0,
153+
message: "sqlite execute only supports a single statement".to_owned(),
154+
}
155+
.into());
132156
}
133157
if stmt.is_null() {
134158
return Ok(ExecuteResult {
@@ -152,7 +176,11 @@ pub fn execute_single_statement(
152176
break;
153177
}
154178
if step_rc != SQLITE_ROW {
155-
return Err(sqlite_error(db, "failed to step sqlite execute statement"));
179+
return Err(sqlite_error(
180+
db,
181+
0,
182+
"failed to step sqlite execute statement",
183+
));
156184
}
157185

158186
let mut row = Vec::with_capacity(columns.len());
@@ -181,6 +209,7 @@ pub fn execute_single_statement(
181209
pub fn exec_statements(db: *mut sqlite3, sql: &str) -> Result<QueryResult> {
182210
let c_sql = CString::new(sql).map_err(|err| anyhow!(err.to_string()))?;
183211
let mut remaining = c_sql.as_ptr();
212+
let mut statement_index = 0_u32;
184213
let mut final_result = QueryResult {
185214
columns: Vec::new(),
186215
rows: Vec::new(),
@@ -191,7 +220,11 @@ pub fn exec_statements(db: *mut sqlite3, sql: &str) -> Result<QueryResult> {
191220
let mut tail = ptr::null();
192221
let rc = unsafe { sqlite3_prepare_v2(db, remaining, -1, &mut stmt, &mut tail) };
193222
if rc != SQLITE_OK {
194-
return Err(sqlite_error(db, "failed to prepare sqlite exec statement"));
223+
return Err(sqlite_error(
224+
db,
225+
statement_index,
226+
"failed to prepare sqlite exec statement",
227+
));
195228
}
196229

197230
if stmt.is_null() {
@@ -211,7 +244,11 @@ pub fn exec_statements(db: *mut sqlite3, sql: &str) -> Result<QueryResult> {
211244
break;
212245
}
213246
if step_rc != SQLITE_ROW {
214-
return Err(sqlite_error(db, "failed to step sqlite exec statement"));
247+
return Err(sqlite_error(
248+
db,
249+
statement_index,
250+
"failed to step sqlite exec statement",
251+
));
215252
}
216253

217254
let mut row = Vec::with_capacity(columns.len());
@@ -237,6 +274,7 @@ pub fn exec_statements(db: *mut sqlite3, sql: &str) -> Result<QueryResult> {
237274
break;
238275
}
239276
remaining = tail;
277+
statement_index = statement_index.saturating_add(1);
240278
}
241279

242280
Ok(final_result)
@@ -274,7 +312,7 @@ fn bind_params(
274312
};
275313

276314
if rc != SQLITE_OK {
277-
return Err(sqlite_error(db, "failed to bind sqlite parameter"));
315+
return Err(sqlite_error(db, 0, "failed to bind sqlite parameter"));
278316
}
279317
}
280318

@@ -336,17 +374,25 @@ fn has_non_whitespace_tail(tail: *const c_char) -> bool {
336374
bytes.iter().any(|byte| !byte.is_ascii_whitespace())
337375
}
338376

339-
fn sqlite_error(db: *mut sqlite3, context: &str) -> anyhow::Error {
340-
let message = unsafe {
377+
fn sqlite_error(db: *mut sqlite3, statement_index: u32, context: &str) -> anyhow::Error {
378+
let (code, detail) = unsafe {
341379
if db.is_null() {
342-
"unknown sqlite error".to_string()
380+
(-1, "unknown sqlite error".to_string())
343381
} else {
344-
CStr::from_ptr(sqlite3_errmsg(db))
345-
.to_string_lossy()
346-
.into_owned()
382+
(
383+
sqlite3_extended_errcode(db),
384+
CStr::from_ptr(sqlite3_errmsg(db))
385+
.to_string_lossy()
386+
.into_owned(),
387+
)
347388
}
348389
};
349-
anyhow!("{context}: {message}")
390+
SqliteStatementError {
391+
code,
392+
statement_index,
393+
message: format!("{context}: {detail}"),
394+
}
395+
.into()
350396
}
351397

352398
#[cfg(test)]
@@ -430,6 +476,25 @@ mod tests {
430476
assert_eq!(result.rows, vec![vec![ColumnValue::Integer(2)]]);
431477
}
432478

479+
#[test]
480+
fn exec_reports_the_actual_failing_statement_index() {
481+
let db = MemoryDb::open();
482+
exec_statements(
483+
db.as_ptr(),
484+
"CREATE TABLE unique_items(value INTEGER UNIQUE);",
485+
)
486+
.unwrap();
487+
488+
let error = exec_statements(
489+
db.as_ptr(),
490+
"INSERT INTO unique_items VALUES (1); INSERT INTO unique_items VALUES (1);",
491+
)
492+
.expect_err("the second statement should violate the unique constraint");
493+
let typed = error.downcast_ref::<SqliteStatementError>().unwrap();
494+
assert_eq!(typed.code, libsqlite3_sys::SQLITE_CONSTRAINT_UNIQUE);
495+
assert_eq!(typed.statement_index, 1);
496+
}
497+
433498
#[test]
434499
fn execute_single_statement_returns_rows_and_metadata() {
435500
let db = MemoryDb::open();
@@ -524,6 +589,27 @@ mod tests {
524589
err.to_string().contains("single statement"),
525590
"unexpected error: {err:#}"
526591
);
592+
let typed = err.downcast_ref::<SqliteStatementError>().unwrap();
593+
assert_eq!(typed.code, -1);
594+
assert_eq!(typed.statement_index, 0);
595+
}
596+
597+
#[test]
598+
fn execute_single_statement_reports_extended_result_code() {
599+
let db = MemoryDb::open();
600+
exec_statements(
601+
db.as_ptr(),
602+
"CREATE TABLE unique_items(value TEXT UNIQUE); INSERT INTO unique_items VALUES ('same');",
603+
)
604+
.unwrap();
605+
let err = execute_single_statement(
606+
db.as_ptr(),
607+
"INSERT INTO unique_items VALUES ('same')",
608+
None,
609+
)
610+
.expect_err("unique constraint should fail");
611+
let typed = err.downcast_ref::<SqliteStatementError>().unwrap();
612+
assert_eq!(typed.code, libsqlite3_sys::SQLITE_CONSTRAINT_UNIQUE);
527613
}
528614

529615
#[test]
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[package]
2+
name = "rivet-actor-runtime-socket-protocol"
3+
version.workspace = true
4+
authors.workspace = true
5+
license.workspace = true
6+
homepage.workspace = true
7+
repository.workspace = true
8+
edition.workspace = true
9+
description = "Versioned protocol for the experimental Actor Runtime Socket"
10+
11+
[dependencies]
12+
anyhow.workspace = true
13+
serde_bare.workspace = true
14+
serde.workspace = true
15+
vbare.workspace = true
16+
17+
[build-dependencies]
18+
vbare-compiler.workspace = true

0 commit comments

Comments
 (0)