#20·sdk-js

Runtime bridge reads the decorator registry before any construction - all lifecycle/tool/run registrations appear empty on published 0.1.0

Author: eren-karakus0Created Jul 3, 2026Updated Jul 3, 2026

Summary

The SDK registers @tool / @interceptor / @command / @install / @upgrade / @run methods via TC39 context.addInitializer(...) (packages/astrid-sdk/src/tool.ts, src/capsule.ts). Per the decorators proposal, initializers added by non-static method decorators run during instance construction — i.e. only when new CapsuleClass() first executes.

createBridge() in packages/astrid-sdk/src/runtime/bridge.ts, however, reads the registration maps before constructing anything:

  • astridInstall() checks r.installMethod === undefined → returns (the instance that would have populated it is only constructed after this check);
  • run() checks r.runMethod === undefined → returns immediately, so the kernel sees "run loop exited before signaling ready" and health-restarts the capsule forever;
  • tool_describe / hook dispatch in astridHookTrigger see empty tools / interceptors / commands maps.

The comment at the top of src/runtime/registry.ts states the assumption that breaks:

Decorators run during class evaluation, which happens at module init. The bridge is constructed AFTER all decorators have populated the registry, so reads are safe.

That holds for the @capsule class decorator (registerCapsule runs at class evaluation), but not for the method decorators — their addInitializer callbacks only fire at first construction, which the bridge never triggers before reading.

Net effect on a stock capsule built with the published 0.1.0 packages and run on released kernels (verified on astrid 0.9.0/0.9.1, Ubuntu 24.04): @install hooks silently no-op, @run loops never start, and bus-routed tool dispatch never reaches the capsule. Because @capsule does register the constructor, the failure looks like "the capsule is fine but empty".

Reproduction

  1. npm i @unicity-astrid/[email protected] @unicity-astrid/[email protected]
  2. Any capsule with @install that logs; build; astrid capsule install .
  3. Observe: Lifecycle hook completed successfully in <2 ms with no guest log output. Add @run with runtime.signalReady(): observe run loop exited before signaling ready + health-restart loop.

Fix that worked for me

Warm the registry with one throwaway construction before the first read, and make duplicate records idempotent (the warm-up construction plus the bridge's own later new r.ctor() calls would otherwise trip the duplicate guards in registry.ts):

javascript
// bridge.js — inside createBridge()
let warmed = false;
function reg() {
  const r = getRegistration();
  if (r !== undefined && !warmed) {
    warmed = true;
    try { new r.ctor(); } catch { /* state-free constructor */ }
  }
  // ... existing undefined check ...
}
javascript
// registry.js — recordTool/recordInterceptor/recordCommand/recordInstall/
// recordUpgrade/recordRun: replace the duplicate-throw with an early return.

A cleaner long-term fix: record method metadata from the decorator call itself rather than addInitializer — the method name and options are already known at decoration time, so no construction would be needed at all. That would also resolve the re-registration-per-construction concern the automated audit flagged in #18.

With this patch applied on top of the published package, my capsule's @install hook executed a full HTTP session inside the kernel sandbox, and @run + runtime.signalReady() kept the daemon healthy on astrid 0.9.0. The exact postinstall patch I apply is here: patch-sdk.mjs.

Happy to turn this into a PR against packages/astrid-sdk if you'd take one — just tell me which direction you prefer (warm-up vs. decoration-time registration).

Environment

  • @unicity-astrid/sdk 0.1.0, @unicity-astrid/build 0.1.0 (npm)
  • astrid 0.9.0 / 0.9.1 release binaries, x86_64-unknown-linux-gnu
  • Ubuntu 24.04 (WSL2), Node 22
  • Capsule source: TypeScript, standard TC39 decorators (experimentalDecorators: false, target ES2022), built via astrid-js-build → ComponentizeJS → wasm32-wasip2

Related observation (separate, minor)

astrid 0.9.1 fails to pre-instantiate JS capsules with component imports instance astrid:process/[email protected], but a matching implementation was not found in the linker — the JS bundle always links every SDK module (esbuild keeps the side-effectful WIT imports), so kernels that gate host interfaces on capabilities can no longer load stock JS capsules. 0.9.0 links it unconditionally and works.