Skip to content

Commit a8b30ec

Browse files
authored
feat: Proof of concept for remote sandbox (#56)
* feat: Proof of concept for remote sandbox * fix: Make Docker builds work
1 parent dec6949 commit a8b30ec

File tree

30 files changed

+1660
-269
lines changed

30 files changed

+1660
-269
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,9 @@ url = { version = "2" }
3838
usuba-bundle = { path = "./rust/usuba-bundle" }
3939
utoipa = { version = "4" }
4040
utoipa-swagger-ui = { version = "7" }
41+
wasmtime = { version = "21" }
4142
wasm-bindgen = { version = "0.2" }
42-
wasmtime-environ = { version = "20" }
43+
wasmtime-environ = { version = "21" }
4344
web-sys = { version = "0.3" }
4445
wit-bindgen = { version = "0.25" }
4546
wit-parser = { version = "0.208" }

rust/usuba-compat/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@ crate-type = ["cdylib", "rlib"]
1010
blake3 = { workspace = true }
1111
js-component-bindgen = { workspace = true }
1212
wit-bindgen = { workspace = true }
13-
wasmtime-environ = { workspace = true, features = ["component-model", "compile"] }
13+
wasmtime-environ = { version = "20", features = ["component-model", "compile"] }

rust/usuba/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ edition = "2021"
99
[dependencies]
1010
anyhow = { workspace = true }
1111
async-trait = { workspace = true }
12-
axum = { workspace = true, features = ["multipart"] }
12+
axum = { workspace = true, features = ["multipart", "macros"] }
1313
blake3 = { workspace = true }
1414
bytes = { workspace = true }
1515
hyper-util = { workspace = true }
@@ -27,6 +27,8 @@ tracing-subscriber = { workspace = true }
2727
usuba-bundle = { workspace = true }
2828
utoipa = { workspace = true, features = ["axum_extras"] }
2929
utoipa-swagger-ui = { workspace = true, features = ["axum"] }
30+
wasmtime = { workspace = true }
31+
wasmtime-wasi = { version = "21" }
3032
wit-parser = { workspace = true }
3133
# gcloud-sdk = { version = "0.24.6", features = ["google-cloud-aiplatform-v1"] }
3234

rust/usuba/Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ COPY ./rust ./rust
1515
COPY ./typescript ./typescript
1616

1717
RUN rustup target add wasm32-wasi
18-
RUN cargo install wasm-tools
18+
RUN cargo install wasm-tools wit-deps-cli
1919

20-
WORKDIR /build-root/typescript/packages/usuba-ui
20+
WORKDIR /build-root/typescript
2121

2222
RUN npm install -g @bytecodealliance/jco
2323
RUN npm ci

rust/usuba/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ extern crate tracing;
44
mod bake;
55
mod error;
66
pub mod openapi;
7+
mod recipe;
78
pub mod routes;
89
mod serve;
910
mod storage;
1011

1112
pub use bake::*;
1213
pub use error::*;
14+
pub use recipe::*;
1315
pub use serve::*;
1416
pub use storage::*;

rust/usuba/src/openapi.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
use utoipa::OpenApi;
22

33
use crate::{
4-
routes::{BuildModuleRequest, BuildModuleResponse, BundleRequest},
4+
routes::{
5+
BuildModuleRequest, BuildModuleResponse, BundleRequest, EvalRecipeRequest,
6+
EvalRecipeResponse, JsonValue,
7+
},
58
ErrorResponse,
69
};
710

@@ -10,13 +13,17 @@ use crate::{
1013
paths(
1114
crate::routes::build_module,
1215
crate::routes::retrieve_module,
13-
crate::routes::bundle_javascript
16+
crate::routes::bundle_javascript,
17+
crate::routes::eval_recipe
1418
),
1519
components(
1620
schemas(BuildModuleResponse),
1721
schemas(ErrorResponse),
1822
schemas(BuildModuleRequest),
19-
schemas(BundleRequest)
23+
schemas(BundleRequest),
24+
schemas(EvalRecipeRequest),
25+
schemas(EvalRecipeResponse),
26+
schemas(JsonValue)
2027
)
2128
)]
2229
pub struct OpenApiDocs;

rust/usuba/src/recipe/common.rs

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
use std::collections::BTreeMap;
2+
3+
use wasmtime::{
4+
component::{bindgen, Resource},
5+
Module,
6+
};
7+
8+
bindgen!({
9+
world: "common",
10+
path: "../../typescript/common/module/wit"
11+
});
12+
13+
pub use common::data::types::{Dictionary, Reference, Value};
14+
use wasmtime_wasi::WasiView;
15+
16+
pub trait InputOutput: Send + Sync + Clone + std::fmt::Debug {
17+
fn read(&self, key: &str) -> Option<Value>;
18+
fn write(&mut self, key: &str, value: Value);
19+
}
20+
21+
pub struct ResourceTable<T> {
22+
next_index: u32,
23+
resources: BTreeMap<u32, T>,
24+
}
25+
26+
impl<T> ResourceTable<T> {
27+
pub fn add(&mut self, resource: T) -> u32 {
28+
let index = self.next_index;
29+
30+
self.next_index = self.next_index + 1;
31+
self.resources.insert(index, resource);
32+
33+
index
34+
}
35+
36+
pub fn lookup(&self, index: u32) -> Option<&T> {
37+
self.resources.get(&index)
38+
}
39+
40+
pub fn remove(&mut self, index: u32) {
41+
self.resources.remove(&index);
42+
}
43+
}
44+
45+
impl<T> Default for ResourceTable<T> {
46+
fn default() -> Self {
47+
Self {
48+
next_index: Default::default(),
49+
resources: BTreeMap::new(),
50+
}
51+
}
52+
}
53+
54+
#[repr(transparent)]
55+
pub struct HostReference(String);
56+
57+
pub struct ModuleEnvironment<Io>
58+
where
59+
Io: InputOutput,
60+
{
61+
io: Io,
62+
references: ResourceTable<HostReference>,
63+
64+
wasi_resources: wasmtime_wasi::ResourceTable,
65+
wasi_ctx: wasmtime_wasi::WasiCtx,
66+
}
67+
68+
impl<Io> ModuleEnvironment<Io>
69+
where
70+
Io: InputOutput,
71+
{
72+
pub fn new(io: Io) -> Self {
73+
ModuleEnvironment {
74+
io,
75+
references: ResourceTable::default(),
76+
77+
wasi_resources: wasmtime_wasi::ResourceTable::new(),
78+
wasi_ctx: wasmtime_wasi::WasiCtx::builder().build(),
79+
}
80+
}
81+
82+
pub fn take_io(self) -> Io {
83+
self.io
84+
}
85+
}
86+
87+
impl<Io> common::io::state::Host for ModuleEnvironment<Io>
88+
where
89+
Io: InputOutput,
90+
{
91+
fn read(&mut self, name: String) -> Option<Resource<Reference>> {
92+
warn!("Reading input: {name}");
93+
if self.io.read(&name).is_none() {
94+
return None;
95+
}
96+
97+
let reference = HostReference(name);
98+
let index = self.references.add(reference);
99+
100+
Some(Resource::new_own(index))
101+
}
102+
103+
fn write(&mut self, name: String, value: Value) -> () {
104+
warn!("Writing output: {name}");
105+
self.io.write(&name, value);
106+
}
107+
}
108+
109+
impl<Io> common::data::types::HostDictionary for ModuleEnvironment<Io>
110+
where
111+
Io: InputOutput,
112+
{
113+
fn get(
114+
&mut self,
115+
_resource: Resource<Dictionary>,
116+
_key: String,
117+
) -> Option<wasmtime::component::Resource<Reference>> {
118+
unimplemented!("Dictionary not supported yet saaawiii");
119+
}
120+
121+
fn drop(&mut self, _rep: Resource<Dictionary>) -> wasmtime::Result<()> {
122+
unimplemented!("Dictionary not supported yet saaawiii");
123+
}
124+
}
125+
126+
impl<Io> common::data::types::HostReference for ModuleEnvironment<Io>
127+
where
128+
Io: InputOutput,
129+
{
130+
/// Dereference a reference to a value
131+
/// This call is fallible (for example, if the dereference is not allowed)
132+
/// The value may be none (for example, if it is strictly opaque)
133+
fn deref(&mut self, resource: Resource<Reference>) -> Result<Option<Value>, String> {
134+
let HostReference(key) = self
135+
.references
136+
.lookup(resource.rep())
137+
.ok_or_else(|| String::from("Attempted to deref an untracked Reference"))?;
138+
139+
Ok(self.io.read(key))
140+
}
141+
142+
fn drop(&mut self, rep: Resource<Reference>) -> wasmtime::Result<()> {
143+
Ok(self.references.remove(rep.rep()))
144+
}
145+
}
146+
147+
impl<Io> common::data::types::Host for ModuleEnvironment<Io> where Io: InputOutput {}
148+
149+
impl<Io> WasiView for ModuleEnvironment<Io>
150+
where
151+
Io: InputOutput,
152+
{
153+
fn table(&mut self) -> &mut wasmtime_wasi::ResourceTable {
154+
&mut self.wasi_resources
155+
}
156+
157+
fn ctx(&mut self) -> &mut wasmtime_wasi::WasiCtx {
158+
&mut self.wasi_ctx
159+
}
160+
}

rust/usuba/src/recipe/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// pub mod common;
2+
pub mod common;
3+
4+
mod runtime;
5+
6+
pub use runtime::*;

rust/usuba/src/recipe/runtime.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
use wasmtime::component::{Component, Linker};
2+
use wasmtime::{Engine, Store};
3+
4+
use crate::common::exports::common::module::module::GuestBody;
5+
use crate::{Bake, Baker, UsubaError};
6+
7+
use super::common::Common;
8+
pub use super::common::{Dictionary, InputOutput, ModuleEnvironment, Value};
9+
10+
const COMMON_MODULE_WIT: &[u8] =
11+
include_bytes!("../../../../typescript/common/module/wit/module.wit");
12+
13+
const COMMON_IO_WIT: &[u8] = include_bytes!("../../../../typescript/common/io/wit/io.wit");
14+
15+
const COMMON_DATA_WIT: &[u8] = include_bytes!("../../../../typescript/common/data/wit/data.wit");
16+
17+
pub struct Runtime {}
18+
19+
impl Runtime {
20+
pub async fn eval<Io: InputOutput + 'static>(
21+
&mut self,
22+
content_type: String,
23+
source_code: String,
24+
io: Io,
25+
) -> Result<Io, UsubaError> {
26+
let component_baker = match content_type.as_str() {
27+
"text/javascript" => Baker::JavaScript,
28+
"text/x-python" => Baker::Python,
29+
_ => return Err(UsubaError::BadRequest),
30+
};
31+
32+
let component_wasm = component_baker
33+
.bake(
34+
"common",
35+
vec![COMMON_MODULE_WIT.into()],
36+
source_code.into(),
37+
vec![COMMON_DATA_WIT.into(), COMMON_IO_WIT.into()],
38+
)
39+
.await?;
40+
41+
let mut config = wasmtime::Config::default();
42+
config.async_support(false);
43+
44+
let engine = Engine::new(&config)?;
45+
46+
let mut store = Store::new(&engine, ModuleEnvironment::new(io));
47+
48+
let component = Component::new(&engine, component_wasm)?;
49+
let mut linker = Linker::new(&engine);
50+
51+
wasmtime_wasi::add_to_linker_sync(&mut linker)?;
52+
53+
Common::add_to_linker(&mut linker, |environment| environment)?;
54+
55+
let (common, _inst) = Common::instantiate(&mut store, &component, &linker)?;
56+
57+
let store = tokio::task::spawn_blocking(move || {
58+
let common_module = common.common_module_module();
59+
60+
match common_module.call_create(&mut store) {
61+
Ok(body_resource) => {
62+
common
63+
.common_module_module()
64+
.body()
65+
.call_run(&mut store, body_resource)?;
66+
}
67+
Err(error) => {
68+
error!("Create failed: {}", error);
69+
}
70+
};
71+
72+
Ok(store) as wasmtime::Result<Store<ModuleEnvironment<Io>>, wasmtime::Error>
73+
})
74+
.await??;
75+
76+
Ok(store.into_data().take_io())
77+
}
78+
}

0 commit comments

Comments
 (0)