Skip to content

Commit b2ae6c8

Browse files
authored
feat!: migrate to per-domain WIT + wasm32-unknown-unknown (#14)
## 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 b4cc2e8 commit b2ae6c8

6 files changed

Lines changed: 63 additions & 37 deletions

File tree

‎.cargo/config.toml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,8 @@
11
[build]
22
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

‎Cargo.toml‎

Lines changed: 5 additions & 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

@@ -19,3 +19,7 @@ lto = true
1919
codegen-units = 1
2020
strip = true
2121
panic = "abort"
22+
23+
# Local-development override: point at the in-repo sdk-rust workspace.
24+
# This is removed once sdk-rust 0.7.0 ships to crates.io.
25+
# See CAPSULE_MIGRATION_GUIDE.md for context.

‎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: 46 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -196,8 +196,18 @@ impl ContextEngine {
196196
config.hook_timeout_ms, config.keep_recent
197197
));
198198

199-
let sub =
200-
ipc::subscribe("context_engine.v1.*").map_err(|e| SysError::ApiError(e.to_string()))?;
199+
// Subscribe to the two request topics this capsule serves. The
200+
// pre-refactor wildcard (`context_engine.v1.*`) is not legal
201+
// under the kernel ACL — the manifest's [subscribe] declares
202+
// specific exact-topic permissions, and a wildcard request
203+
// exceeds them. The dispatcher reads `result.messages[].topic`
204+
// and routes on it; receiving both topics on one Subscription
205+
// (which the wildcard enabled) was a convenience, not a
206+
// requirement.
207+
let compact_sub = ipc::subscribe("context_engine.v1.compact")
208+
.map_err(|e| SysError::ApiError(e.to_string()))?;
209+
let estimate_sub = ipc::subscribe("context_engine.v1.estimate_tokens")
210+
.map_err(|e| SysError::ApiError(e.to_string()))?;
201211

202212
// Subscribe to our own hook topics so we can drain them.
203213
let hook_sub = ipc::subscribe("context_engine.v1.hook.before_compaction")
@@ -212,24 +222,26 @@ impl ContextEngine {
212222
log::info("Context Engine capsule ready");
213223

214224
loop {
215-
// Block until a message arrives (up to 60s), eliminating busy-spin polling.
216-
match ipc::recv(&sub, 60_000) {
217-
Ok(result) => {
218-
dispatch_poll_result(&result, &config);
219-
}
220-
Err(_) => break,
225+
// Block briefly on each request channel, then drain the
226+
// hook fan-out channels to prevent backpressure. The
227+
// pre-refactor single-wildcard subscription is fanned out
228+
// here into two short polls so cancellation latency stays
229+
// bounded by `HOOK_POLL_INTERVAL_MS * 2`.
230+
const HOOK_POLL_INTERVAL_MS: u64 = 1_000;
231+
// Timeout is normal — fall through. Real errors surface
232+
// when recv returns on a closed subscription, in which
233+
// case the loop exits when the host stops invoking run.
234+
if let Ok(result) = compact_sub.recv(HOOK_POLL_INTERVAL_MS) {
235+
dispatch_poll_result(&result, &config);
236+
}
237+
if let Ok(result) = estimate_sub.recv(HOOK_POLL_INTERVAL_MS) {
238+
dispatch_poll_result(&result, &config);
221239
}
222240

223241
// Drain hook topics to prevent backpressure.
224-
let _ = ipc::poll(&hook_sub);
225-
let _ = ipc::poll(&after_sub);
242+
let _ = hook_sub.poll();
243+
let _ = after_sub.poll();
226244
}
227-
228-
let _ = ipc::unsubscribe(&sub);
229-
let _ = ipc::unsubscribe(&hook_sub);
230-
let _ = ipc::unsubscribe(&after_sub);
231-
232-
Ok(())
233245
}
234246
}
235247

@@ -407,12 +419,13 @@ fn fire_before_compaction(
407419
message_count: u32,
408420
config: &Config,
409421
) -> MergedBeforeCompaction {
422+
// `SystemTime::now()` panics on `wasm32-unknown-unknown`. Use the
423+
// monotonic host clock (which works) plus a per-capsule atomic
424+
// counter to produce a unique-enough request_id for the hook
425+
// fan-out reply-topic suffix.
410426
let request_id = format!(
411427
"compact-{}-{}",
412-
std::time::SystemTime::now()
413-
.duration_since(std::time::UNIX_EPOCH)
414-
.unwrap_or_default()
415-
.as_millis(),
428+
astrid_sdk::time::monotonic().as_nanos(),
416429
REQUEST_COUNTER.fetch_add(1, Ordering::Relaxed)
417430
);
418431

@@ -443,36 +456,39 @@ fn fire_before_compaction(
443456
log::error(format!(
444457
"Failed to publish context_engine.v1.hook.before_compaction event: {e}"
445458
));
446-
let _ = ipc::unsubscribe(&sub);
459+
// `sub` drops here, releasing the subscription.
447460
return MergedBeforeCompaction {
448461
skip: false,
449462
protected_ids: HashSet::new(),
450463
};
451464
}
452465

453466
// Block-wait for hook responses within the configured timeout.
467+
// `std::time::Instant::now()` panics on `wasm32-unknown-unknown`;
468+
// use the monotonic host clock via `astrid_sdk::time` instead.
454469
let mut responses: Vec<BeforeCompactionHookResponse> = Vec::new();
455-
let deadline =
456-
std::time::Instant::now() + std::time::Duration::from_millis(config.hook_timeout_ms);
457-
458-
while std::time::Instant::now() < deadline && responses.len() < MAX_HOOK_RESPONSES {
459-
let remaining_ms = deadline
460-
.saturating_duration_since(std::time::Instant::now())
461-
.as_millis();
470+
let start = astrid_sdk::time::monotonic();
471+
let timeout_dur = std::time::Duration::from_millis(config.hook_timeout_ms);
472+
473+
while astrid_sdk::time::monotonic().saturating_sub(start) < timeout_dur
474+
&& responses.len() < MAX_HOOK_RESPONSES
475+
{
476+
let elapsed = astrid_sdk::time::monotonic().saturating_sub(start);
477+
let remaining_ms = timeout_dur.saturating_sub(elapsed).as_millis();
462478
if remaining_ms == 0 {
463479
break;
464480
}
465481
let timeout = u64::try_from(remaining_ms).unwrap_or(u64::MAX);
466482

467-
match ipc::recv(&sub, timeout) {
483+
match sub.recv(timeout) {
468484
Ok(result) => {
469485
responses.extend(parse_hook_responses(&result));
470486
}
471487
_ => break,
472488
}
473489
}
474490

475-
let _ = ipc::unsubscribe(&sub);
491+
// Subscription drops at scope exit; no manual unsubscribe needed.
476492

477493
if !responses.is_empty() {
478494
log::info(format!(

‎src/tests.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ fn make_poll_result(messages: Vec<(&str, serde_json::Value)>) -> ipc::PollResult
1919
topic: topic.to_string(),
2020
payload: serde_json::to_string(&payload).unwrap(),
2121
source_id: "test".to_string(),
22+
principal: ipc::PrincipalAttribution::System,
2223
})
2324
.collect(),
2425
dropped: 0,

0 commit comments

Comments
 (0)