Skip to content

feat: Cross-Language SDK — Phases 2-4 + gRPC debt fix + dogfooding#39

Merged
bkrabach merged 2 commits intomainfrom
dev/cross-language-sdk-v2
Mar 8, 2026
Merged

feat: Cross-Language SDK — Phases 2-4 + gRPC debt fix + dogfooding#39
bkrabach merged 2 commits intomainfrom
dev/cross-language-sdk-v2

Conversation

@bkrabach
Copy link
Collaborator

@bkrabach bkrabach commented Mar 7, 2026

Summary

Single squash of all Cross-Language SDK work (Phases 2-4), gRPC Phase 2 debt resolution, and integration dogfooding. 124 files changed, ~32,800 additions.

Phase 2: TypeScript/Napi-RS Bindings

  • bindings/node/ — 4 kernel classes (AmplifierSession, Coordinator, HookRegistry, CancellationToken)
  • JsToolBridge for TypeScript module authoring
  • 6 enums + 5 typed struct interfaces + .d.ts auto-generation
  • 64 Vitest tests across 9 test files
  • CI: separate node-tests job with actions/setup-node@v4
  • Dependency upgrades: pyo3 0.28.1→0.28.2 (HIGH severity fix), wasmtime 29→42 (8 Dependabot alerts cleared)

gRPC Phase 2 Debt Fix

  • 5 proto optional field additions (backward-compatible wire change)
  • Full bidirectional conversions: Role, Message (all 7 ContentBlock variants), ChatRequest, ChatResponse, HookResult
  • All bridge fixes: GrpcContextBridge (full message fidelity + provider_name), GrpcProviderBridge::complete(), GrpcOrchestratorBridge (session_id routing)
  • Session holds Arc<Coordinator> with coordinator_shared() for KernelService
  • All 9 KernelService RPCs implemented (was 1 of 9): capabilities, module query, messages, hooks, provider complete, streaming
  • Zero TODO(grpc-v2) markers remain in source

Phase 3: WASM Module Loading

  • wit/amplifier-modules.wit — WIT interfaces for all 6 module types + kernel-service host imports
  • crates/amplifier-guest/ — Rust guest SDK with traits, types, export_*! macros
  • 6 WASM bridges: WasmToolBridge, WasmHookBridge, WasmContextBridge, WasmApprovalBridge, WasmProviderBridge, WasmOrchestratorBridge
  • Shared WasmEngine with Arc<wasmtime::Engine>
  • 6 compiled .wasm test fixtures (echo-tool, deny-hook, memory-context, auto-approve, echo-provider, passthrough-orchestrator)
  • Transport dispatch: load_wasm_* for all 6 module types

Phase 4: Cross-Language Module Resolver

  • module_resolver.rs (1,036 lines) — auto-detect transport from filesystem path
  • Detection pipeline: amplifier.toml.wasm scan + component metadata → Python package → error
  • WASM component metadata parsing for module type detection (self-describing, zero config)
  • PyO3 binding: resolve_module() + load_wasm_from_path()
  • Napi-RS binding: resolveModule() + loadWasmFromPath()
  • loader_dispatch.py WASM/gRPC branches wired to Rust

Dogfooding / Integration

  • _session_init.py routes through loader_dispatch.load_module() (backward compat preserved)
  • load_and_mount_wasm + PyWasmTool for real coordinator WASM mounting
  • Mixed-transport E2E test: native Rust + WASM modules in same session — coordinator is transport-blind
  • Examples: python-wasm-session.py, node-wasm-session.ts, calculator-tool WASM module

Design docs (in docs/plans/)

  • Phase 2 design + implementation plan
  • gRPC debt fix design + implementation plan
  • Phase 3 WASM design + implementation plan
  • Phase 4 module resolver design + implementation plan
  • Dogfooding integration plan

Test Plan

  • 396 Rust lib tests (non-WASM) — 0 failures
  • 34 WASM + resolver lib tests — 0 failures
  • 8 WASM E2E tests (all 6 module types including orchestrator→kernel→tool callback chain)
  • 14 module resolver E2E tests
  • 2 mixed-transport E2E tests (native + WASM in same session)
  • cargo clippy (core + wasm) — 0 warnings
  • cargo check --features wasm — builds clean
  • CI workflow runs

Notes

  • This is a squash of the dev/cross-language-sdk integration branch onto a clean branch from current main
  • All WASM work is behind #[cfg(feature = "wasm")] — zero impact when feature is off
  • Backward compatible: all existing Python module loading works identically

@bkrabach bkrabach force-pushed the dev/cross-language-sdk-v2 branch 4 times, most recently from 38ba778 to 8b290c3 Compare March 7, 2026 23:32
…ening

TypeScript/Napi-RS bindings (Phase 2):
- 4 kernel classes: AmplifierSession, Coordinator, HookRegistry, CancellationToken
- JsToolBridge for TypeScript module authoring
- 71 Vitest tests, CI split into separate node-tests job

gRPC Phase 2 debt fix:
- 5 proto optional fields, full bidirectional conversions
- All 9 KernelService RPCs implemented
- Session holds Arc<Coordinator> with coordinator_shared()

WASM module loading (Phase 3):
- 6 WASM bridges via Component Model
- amplifier-guest crate with export macros
- WIT interface definitions for all 6 module types
- 6 test fixture .wasm binaries

Cross-language module resolver (Phase 4):
- Auto-detect transport from filesystem path
- WASM component metadata parsing for module type detection
- PyO3 + Napi-RS bindings for resolver

Security hardening (from code review):
- C-01: gRPC shared-secret auth interceptor
- C-02: WASM epoch interruption + memory limits
- C-03: Hook parse failure fails closed (Deny)
- C-04: Node.js detached getters renamed to create methods
- C-05: Streaming endpoint logs send failures
- C-06: Guest SDK kernel stubs have compile_error! gate
- H-01: WASI null I/O on all bridges
- H-02: gRPC error messages sanitized (12 sites)
- H-03: Path traversal check in module resolver
- H-04: Session ID routing documented
- H-05: Unsupported HTTP import removed from WIT
- H-06: TypeScript type documentation improved
- H-07: JSON payload size limits (64KB)

Dependency upgrades:
- pyo3 0.28.1 → 0.28.2 (HIGH severity fix)
- wasmtime 29 → 42 (8 Dependabot alerts cleared)
@bkrabach bkrabach force-pushed the dev/cross-language-sdk-v2 branch from 8b290c3 to 404d99e Compare March 7, 2026 23:49
@bkrabach bkrabach merged commit 168a570 into main Mar 8, 2026
8 checks passed
bkrabach added a commit that referenced this pull request Mar 9, 2026
…-09) (#43)

* feat: clamp timeout_seconds to max 300s in EmitHookAndCollect

Previously NaN/Infinity would panic in Duration::from_secs_f64 and
arbitrarily large values were uncapped. Now:
- finite positive values are clamped to MAX_HOOK_COLLECT_TIMEOUT_SECS (300s)
- non-finite or non-positive values fall back to DEFAULT (30s)

* test: add NEG_INFINITY coverage for emit_hook_and_collect timeout

* refactor: replace unwrap() with swap_remove(0) in detect_wasm_module_type

* feat: add debug_assert! before block_on calls in orchestrator bridge

adds 5 debug_assert! guards verifying tokio::runtime::Handle::try_current().is_ok() before each block_on call in register_kernel_service_imports(). These fire only in debug builds and document the invariant that WASM host import closures must execute inside spawn_blocking. Includes an invariant test.

🤖 Generated with Amplifier

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>

* fix(node): remove unused reason parameter from cancellation methods

* refactor(conversions): extract to_json_or_warn and from_json_or_default helpers

Replace 18 serde_json::to_string and 8 serde_json::from_str inline
unwrap_or_else+warn patterns with two shared private helpers:

  fn to_json_or_warn(value, label) -> String
  fn from_json_or_default<T>(json, label) -> T

No behavior change — identical serialization paths and identical warn
messages, just deduplicated. All 49 conversion tests pass.

* refactor(wasm): extract shared get_typed_func to bridges/mod.rs

All 6 WASM bridge files contained a nearly identical function for resolving
a typed WASM Component export (try root-level first, then try nested inside
an interface-exported instance). The only differences were the local function
name, the INTERFACE_NAME constant used, and whether the types were generic or
hardcoded.

Consolidate into a single pub(crate) get_typed_func<Params, Results> in
bridges/mod.rs, parameterized by func_name and interface_name.

Removed functions:
- wasm_tool:     get_typed_func_from_instance<P, R>
- wasm_provider: get_provider_func<P, R>
- wasm_context:  get_context_func<P, R>
- wasm_hook:     get_handle_func (hardcoded types)
- wasm_orchestrator: get_execute_func (hardcoded types)
- wasm_approval: get_request_approval_func (hardcoded types)

Also removed now-unused type aliases (HandleFunc, OrchestratorExecuteFunc,
RequestApprovalFunc, WasmResult<T> in context/hook/approval) and unused
imports (Store, WasmState in hook/approval/provider/orchestrator).

Net: -233 lines / +76 lines across 7 files. All 41 bridge tests pass.

* fix(node): remove unused JsToolResult, JsToolSpec, JsSessionConfig, Role exports

* refactor(resolver): add WasmPath variant to distinguish pre-load from loaded WASM artifacts

- Add WasmPath(PathBuf) variant to ModuleArtifact enum with doc comment
  explaining it is the pre-load state returned by parse_amplifier_toml
- Change parse_amplifier_toml to return WasmPath(wasm_path) instead of
  WasmBytes { bytes: Vec::new(), path: wasm_path } — the invisible empty-bytes
  contract is now made explicit via the type system
- Update load_module (wasm feature) to handle WasmPath by reading bytes from
  disk via std::fs::read before dispatching to the appropriate wasm transport;
  WasmBytes continues to work as before for already-loaded bytes
- Update all match arms on ModuleArtifact throughout the codebase:
  - bindings/node/src/lib.rs: add WasmPath arm (returns path string, same as WasmBytes)
  - bindings/python/src/lib.rs: add WasmPath arm (same artifact_path behavior)
- Update existing parse_toml_wasm_transport test to expect WasmPath
- Add three new tests:
  - parse_toml_wasm_transport_returns_wasm_path: verifies parse_amplifier_toml
    returns WasmPath, not WasmBytes with empty bytes
  - wasm_path_variant_basic: WasmPath can be constructed, cloned, and compared
  - load_module_wasm_path_loads_bytes_from_disk: WasmPath artifact in a manifest
    causes load_module to read bytes from disk and load successfully

WasmBytes is preserved for backward compatibility (used by resolve_module
wasm auto-detection path and direct-bytes test scenarios).

* feat(resolver): add optional sha256 integrity verification for WASM modules

- Add sha2 = { version = "0.10", optional = true } dependency, feature-gated
  under the 'wasm' feature flag to avoid pulling in sha2 for non-WASM builds
- Add sha256: Option<String> field to ModuleManifest for optional integrity hash
- Parse optional 'sha256' field from [module] section in amplifier.toml
  (available for all transport types, primarily useful for WASM)
- Add ModuleResolverError::IntegrityMismatch variant with descriptive message
  showing path, expected hash, and actual hash
- Add verify_wasm_integrity() function (cfg(feature = "wasm")) using sha2 crate
  to compute and compare SHA-256 digests; logs debug message on success
- Wire verification into load_module() for both WasmPath and WasmBytes artifacts,
  executing before bytes are passed to wasmtime for compilation
- Update all ModuleManifest construction sites to include sha256: None

Tests added (TDD - written before implementation):
- parse_toml_with_sha256_field: parses sha256 from amplifier.toml into manifest
- parse_toml_without_sha256_field: missing sha256 field yields None (no verification)
- sha256_missing_field_skips_verification: None sha256 skips check, load succeeds
- sha256_matching_hash_passes: correct hash passes verification, load succeeds
- sha256_mismatched_hash_returns_error: wrong hash returns IntegrityMismatch error

---------

Co-authored-by: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant