Skip to content

Commit d532cec

Browse files
authored
feat!: migrate to per-domain WIT + wasm32-unknown-unknown (#18)
## Summary Migrate this capsule to: 1. The per-domain WIT host ABI introduced in `astrid-runtime/astrid#752` (paired with `astrid-runtime/sdk-rust#44`). 2. `wasm32-unknown-unknown` as the canonical build target — zero `wasi:*` imports, every host call routed through audited `astrid:*` interfaces. ## Changes - `.cargo/config.toml` — `target = "wasm32-unknown-unknown"` with `--cfg=getrandom_backend="custom"` rustflag so `getrandom 0.4` picks up the SDK's `__getrandom_v03_custom` extern (routes entropy through `astrid:sys/host.random-bytes`). - `rust-toolchain.toml` — `targets = ["wasm32-unknown-unknown"]`. - `[patch.crates-io]` — points `astrid-sdk*` / `astrid-sys` / `astrid-types` at the in-tree workspaces so the polyrepo cross-cuts resolve. - Call-site updates for the new typed-`error-code` SDK surface where touched. ## Pairs with - `astrid-runtime/astrid#752` — kernel-side migration - `astrid-runtime/sdk-rust#44` — SDK migration - All other `unicity-astrid/capsule-*` PRs on `feat/per-domain-wit` ## Test Plan - [x] `cargo build --target wasm32-unknown-unknown --release` clean - [x] `cargo clippy --release -- -D warnings` clean - [x] Loads + runs under the migrated kernel — verified end-to-end via `astrid run "say hi"`
1 parent e3e9eeb commit d532cec

8 files changed

Lines changed: 48 additions & 31 deletions

File tree

‎.cargo/config.toml‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,8 @@
11
[build]
2-
target = "wasm32-wasip1"
2+
target = "wasm32-unknown-unknown"
3+
4+
[target.wasm32-unknown-unknown]
5+
# Activates astrid-sys's custom getrandom backend that routes to
6+
# `astrid:sys.random-bytes`. Without this, uuid v4 / HashMap RNG seed
7+
# init fail to link on `wasm32-unknown-unknown`.
8+
rustflags = ["--cfg=getrandom_backend=\"custom\""]

‎.github/workflows/ci.yml‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ on:
88

99
env:
1010
CARGO_TERM_COLOR: always
11-
RUSTFLAGS: "-D warnings"
1211

1312
jobs:
1413
check:
@@ -19,7 +18,7 @@ jobs:
1918

2019
- uses: dtolnay/rust-toolchain@stable
2120
with:
22-
targets: wasm32-wasip1
21+
targets: wasm32-unknown-unknown
2322
components: clippy, rustfmt
2423

2524
- uses: Swatinem/rust-cache@v2
@@ -28,14 +27,14 @@ jobs:
2827
run: cargo fmt --all --check
2928

3029
- name: Clippy
31-
run: cargo clippy --target wasm32-wasip1 -- -D warnings
30+
run: cargo clippy --target wasm32-unknown-unknown -- -D warnings
3231

3332
- name: Build WASM
34-
run: cargo build --release --target wasm32-wasip1
33+
run: cargo build --release --target wasm32-unknown-unknown
3534

3635
- name: Upload WASM artifact
3736
uses: actions/upload-artifact@v4
3837
with:
3938
name: capsule-wasm
40-
path: target/wasm32-wasip1/release/*.wasm
39+
path: target/wasm32-unknown-unknown/release/*.wasm
4140
if-no-files-found: error

‎.github/workflows/release.yml‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,16 @@ jobs:
2121
- name: Install Rust toolchain
2222
uses: dtolnay/rust-toolchain@stable
2323
with:
24-
targets: wasm32-wasip1
24+
targets: wasm32-unknown-unknown
2525

2626
- uses: Swatinem/rust-cache@v2
2727

2828
- name: Build release WASM
29-
run: cargo build --release --target wasm32-wasip1
29+
run: cargo build --release --target wasm32-unknown-unknown
3030

3131
- name: Prepare release asset
3232
run: |
33-
WASM_FILE=$(find target/wasm32-wasip1/release -maxdepth 1 -name '*.wasm' -not -name '*.d' | head -1)
33+
WASM_FILE=$(find target/wasm32-unknown-unknown/release -maxdepth 1 -name '*.wasm' -not -name '*.d' | head -1)
3434
if [ -z "$WASM_FILE" ]; then
3535
echo "Error: No .wasm file found"
3636
exit 1

‎Capsule.toml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ capabilities = { net = ["*"] }
1616

1717
[publish]
1818
"llm.v1.stream.openai-compat" = { wit = "@unicity-astrid/wit/llm/stream-event" }
19+
# Exact topic for the post-#752 registry fan-out (registry subscribes to
20+
# the unsuffixed name). The `.*` wildcard below stays for any future
21+
# call-site that wants per-provider response correlation routing.
22+
"llm.v1.response.describe" = { wit = "@unicity-astrid/wit/llm/describe-response" }
1923
"llm.v1.response.describe.*" = { wit = "@unicity-astrid/wit/llm/describe-response" }
2024

2125
[subscribe]

‎Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ publish = false
99
crate-type = ["cdylib"]
1010

1111
[dependencies]
12-
astrid-sdk = { version = "0.6.1", features = ["derive"] }
12+
astrid-sdk = { version = "0.7", features = ["derive"] }
1313
serde = { version = "1.0", features = ["derive"] }
1414
serde_json = "1.0"
1515
uuid = { version = "1.0", features = ["serde"] }

‎README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ The capsule reads these environment variables during `astrid init`:
4747
## Development
4848

4949
```bash
50-
rustup target add wasm32-wasip1
51-
cargo build --target wasm32-wasip1 --release
50+
rustup target add wasm32-unknown-unknown
51+
cargo build --target wasm32-unknown-unknown --release
5252
```
5353

5454
## License

‎rust-toolchain.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
[toolchain]
22
channel = "1.94.0"
3-
targets = ["wasm32-wasip1"]
3+
targets = ["wasm32-unknown-unknown"]
44
components = ["rustfmt", "clippy"]

‎src/lib.rs‎

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,18 @@ impl OpenAICompatProvider {
6464

6565
/// Returns provider metadata for IPC-based provider discovery.
6666
///
67-
/// Called by the registry capsule via `hooks::trigger("llm.v1.request.describe")`.
68-
/// Returns the provider's model ID, capabilities, and IPC routing topics.
67+
/// The registry capsule publishes a `llm.v1.request.describe` envelope
68+
/// and drains responses on `llm.v1.response.describe` for a bounded
69+
/// window. Each provider capsule subscribes to the request topic and
70+
/// publishes its capability descriptor on the response topic. This
71+
/// replaces the pre-#752 `hooks::trigger` fan-out path that returned
72+
/// interceptor results through kernel-mediated dispatch — under the
73+
/// new ABI the interceptor return value is no longer fanned out, so
74+
/// the provider must publish explicitly.
75+
///
76+
/// The return value is kept (same shape) so other interceptor callers
77+
/// continue to see the descriptor; the explicit `ipc::publish_json`
78+
/// is what registry's new fan-out actually consumes.
6979
#[astrid::interceptor("llm_describe")]
7080
pub fn llm_describe(&self, _payload: serde_json::Value) -> Result<serde_json::Value, SysError> {
7181
let model = env::var("model").unwrap_or_else(|_| "unknown".into());
@@ -77,7 +87,7 @@ impl OpenAICompatProvider {
7787
.ok()
7888
.and_then(|v| v.parse::<u64>().ok())
7989
.unwrap_or(8_192);
80-
Ok(serde_json::json!({
90+
let response = serde_json::json!({
8191
"providers": [{
8292
"id": "openai-compat",
8393
"description": format!("OpenAI-compatible provider (default model: {model})"),
@@ -87,7 +97,9 @@ impl OpenAICompatProvider {
8797
"context_window": context_window,
8898
"max_output_tokens": max_output,
8999
}]
90-
}))
100+
});
101+
ipc::publish_json("llm.v1.response.describe", &response)?;
102+
Ok(response)
91103
}
92104
}
93105

@@ -178,39 +190,35 @@ impl OpenAICompatProvider {
178190
.header("authorization", format!("Bearer {api_key}"))
179191
.json(&request_body)?;
180192

181-
let resp = http::stream_start(&req)?;
193+
let stream = http::stream_start(&req)?;
182194

183-
if resp.status != 200 {
184-
// Drain the error body for the error message.
195+
if stream.status() != 200 {
196+
// Drain the error body for the error message. Stream drops at
197+
// scope exit; no manual close required.
185198
let mut error_body = String::new();
186-
while let Some(chunk) = http::stream_read(&resp.handle)? {
199+
while let Some(chunk) = stream.read_chunk()? {
187200
error_body.push_str(&String::from_utf8_lossy(&chunk));
188201
if error_body.len() > 4096 {
189202
error_body.truncate(4096);
190203
break;
191204
}
192205
}
193-
let _ = http::stream_close(&resp.handle);
194206
return Err(SysError::ApiError(format!(
195207
"API error ({}): {error_body}",
196-
resp.status
208+
stream.status()
197209
)));
198210
}
199211

200-
let result = Self::parse_sse_stream_live(request_id, &resp.handle);
201-
let _ = http::stream_close(&resp.handle);
202-
result
212+
Self::parse_sse_stream_live(request_id, &stream)
213+
// `stream` drops here, releasing the kernel-side HTTP stream.
203214
}
204215

205216
/// Stream SSE chunks in real-time, publishing IPC events as they arrive.
206-
fn parse_sse_stream_live(
207-
request_id: Uuid,
208-
stream: &http::HttpStreamHandle,
209-
) -> Result<(), SysError> {
217+
fn parse_sse_stream_live(request_id: Uuid, stream: &http::HttpStream) -> Result<(), SysError> {
210218
let mut active_tools: Vec<(String, String)> = Vec::new();
211219
let mut line_buffer = String::new();
212220

213-
while let Some(chunk) = http::stream_read(stream)? {
221+
while let Some(chunk) = stream.read_chunk()? {
214222
let chunk_str = String::from_utf8_lossy(&chunk);
215223
line_buffer.push_str(&chunk_str);
216224

0 commit comments

Comments
 (0)