Files
optimclaw/wit/channel.wit
T
04c5c3fe9f feat: WASM extension versioning with WIT compat checks (#592)
* feat: add WASM extension versioning with WIT compat checks and CI enforcement

Phase 1 — WIT Versioning & Compatibility Checks:
- Version WIT packages as `package near:[email protected];`
- Add `semver` crate for version parsing and comparison
- Add `WIT_TOOL_VERSION` / `WIT_CHANNEL_VERSION` host constants
- Add `version` and `wit_version` fields to capabilities schemas
- Add `wit_version` column to `wasm_tools` DB table (both backends)
- Add load-time `check_wit_version_compat()` with semver rules
- Add `IncompatibleWitVersion` error variants for tools and channels
- Enhance instantiation errors with WIT version mismatch hints
- Update all 14 capabilities JSON and 14 registry JSON files

Phase 2 — Upgrade-in-Place & Channel DB Storage:
- Change tool store to DELETE-before-INSERT (one version per extension)
- Create `wasm_channels` table (PostgreSQL migration + libSQL schema)
- Add `WasmChannelStore` trait with PostgreSQL and libSQL backends
- Add `extension_info` tool showing version, WIT version, and status
- Wire `ExtensionInfoTool` into tool registry (7 extension tools)

Phase 3 — CI Version-Bump Enforcement:
- Add `scripts/check-version-bumps.sh` checking WIT/tool/channel versions
- Add `version-check` CI job (PR-only) to `.github/workflows/test.yml`
- Support `[skip-version-check]` label/commit message bypass

Includes 7 regression tests for WIT version compatibility checking
and 2 integration tests for WIT version annotation verification.

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: address PR review feedback for WASM extension versioning

- Wrap PostgreSQL DELETE+INSERT in transactions for both tool and channel
  store() methods to prevent data loss on partial failure (Gemini, Copilot)
- Rename StoredWasmChannelWithBinary.tool → .channel (copy-paste fix)
- Remove unused WasmError::IncompatibleWitVersion variant (dead code)
- Map channel loader WIT mismatch to IncompatibleWitVersion instead of
  generic Config error, simplify variant to single String message
- Fix extension_info description to match actual returned fields
- Add schema test for ExtensionInfoTool matching existing test pattern
- Fix CI script to fail fast on git errors instead of silent bypass

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-03-06 04:38:07 +00:00

356 lines
14 KiB
Plaintext

// WASM Channel Sandbox Interface
//
// Defines the contract between sandboxed channels and the host runtime.
// Channels export the `channel` interface; the host provides the `channel-host` interface.
//
// Architecture: Host-Managed Event Loop
// ┌─────────────────────────────────────────────────────────────────────────────────┐
// │ Host-Managed Event Loop │
// │ │
// │ ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ │
// │ │ HTTP │ │ Polling │ │ Timer │ │
// │ │ Router │ │ Scheduler │ │ Scheduler │ │
// │ └──────┬──────┘ └──────┬───────┘ └──────┬───────┘ │
// │ └───────────────────┴────────────────────┘ │
// │ │ │
// │ ▼ │
// │ ┌──────────────────┬──────────────────┐ │
// │ ▼ ▼ ▼ │
// │ ┌───────────┐ ┌────────┐ ┌──────────┐ ┌──────────┐ │
// │ │on-http-req│ │on-poll │ │on-respond│ │on-status │ WASM Exports │
// │ └───────────┘ └────────┘ └──────────┘ └──────────┘ │
// │ │ │ │ │
// │ └──────────────────┴──────────────────┘ │
// │ │ │
// │ ▼ │
// │ ┌─────────────────┐ │
// │ │ Host Imports │ │
// │ │ emit-message │──────────▶ MessageStream │
// │ │ http-request │ │
// │ └─────────────────┘ │
// └─────────────────────────────────────────────────────────────────────────────────┘
//
// Security Model:
// - WASM channels are untrusted and run in a sandbox
// - Fresh instance per callback (no shared mutable state)
// - All capabilities are opt-in (default: no access)
// - Secrets are NEVER exposed to WASM; credentials are injected at host boundary
// - Workspace writes are prefixed with channels/<name>/ to prevent escape
// - Message emission is rate-limited
package near:[email protected];
/// Host-provided capabilities for sandboxed channels.
///
/// Extends base tool capabilities with channel-specific functions:
/// - emit-message: Queue messages for delivery to the agent
/// - workspace-write: Write to channel-namespaced workspace
interface channel-host {
// ==================== Base Capabilities (from tool host) ====================
/// Log levels for structured logging.
enum log-level {
trace,
debug,
info,
warn,
error,
}
/// Emit a log message.
///
/// Messages are collected and emitted after execution completes.
/// Rate-limited to 1000 entries per execution, 4KB per message.
log: func(level: log-level, message: string);
/// Get the current timestamp in milliseconds since Unix epoch.
now-millis: func() -> u64;
/// Read a file from the workspace.
///
/// Path is automatically prefixed with channels/<name>/.
/// Path must be relative (no leading /) and cannot contain "..".
/// Returns None if the file doesn't exist.
workspace-read: func(path: string) -> option<string>;
/// Response from an HTTP request.
record http-response {
/// HTTP status code.
status: u16,
/// Response headers as JSON object string.
headers-json: string,
/// Response body bytes.
body: list<u8>,
}
/// Make an HTTP request (if capability granted).
///
/// Security:
/// - Only allowed endpoints (host/path patterns) can be accessed
/// - Credentials are injected by the host; WASM never sees them
/// - Response is scanned for leaked secrets before returning
/// - Rate-limited per channel
///
/// The optional timeout-ms parameter controls the HTTP client timeout
/// in milliseconds. Defaults to 30000 (30s) when not provided. Use a
/// longer timeout for long-polling requests (e.g., Telegram getUpdates).
/// Capped at the channel's callback_timeout to prevent hangs.
http-request: func(
method: string,
url: string,
headers-json: string,
body: option<list<u8>>,
timeout-ms: option<u32>,
) -> result<http-response, string>;
/// Check if a secret exists (if capability granted).
///
/// Security:
/// - WASM can only check existence, NEVER read values
/// - Only allowed secret names can be checked
/// - Actual credentials are injected by host during HTTP requests
secret-exists: func(name: string) -> bool;
// ==================== Channel-Specific Capabilities ====================
/// A message to emit to the agent.
record emitted-message {
/// User identifier within the channel (e.g., Slack user ID).
user-id: string,
/// Optional human-readable user name.
user-name: option<string>,
/// Message content.
content: string,
/// Optional thread ID for threaded conversations.
thread-id: option<string>,
/// Channel-specific metadata as JSON string.
metadata-json: string,
}
/// Emit a message to the agent.
///
/// Messages are queued during callback execution and delivered after
/// the callback completes successfully.
///
/// Security:
/// - Rate-limited per execution (max 100 messages)
/// - Rate-limited globally per channel (configurable)
/// - Content size limited to 64KB
emit-message: func(msg: emitted-message);
/// Write a file to the workspace.
///
/// Path is automatically prefixed with channels/<name>/.
/// Path must be relative (no leading /) and cannot contain "..".
///
/// Returns Err if:
/// - Path validation fails (traversal attempt, absolute path)
/// - Write operation fails
workspace-write: func(path: string, content: string) -> result<_, string>;
// ==================== DM Pairing ====================
/// Result of upserting a pairing request.
record pairing-upsert-result {
code: string,
created: bool,
}
/// Upsert a pairing request for an unknown sender.
/// Returns (code, created). When created is true, the channel should send a pairing reply.
pairing-upsert-request: func(
channel: string,
id: string,
meta-json: string
) -> result<pairing-upsert-result, string>;
/// Check if a sender is allowed (in allowFrom store).
pairing-is-allowed: func(
channel: string,
id: string,
username: option<string>
) -> result<bool, string>;
/// Read the allowFrom list (for merging with config allowFrom).
pairing-read-allow-from: func(channel: string) -> result<list<string>, string>;
}
/// Channel interface that sandboxed channels must implement.
interface channel {
// ==================== Configuration Types ====================
/// Configuration for an HTTP endpoint.
record http-endpoint-config {
/// Path to register (e.g., "/webhook/slack").
path: string,
/// Allowed HTTP methods (e.g., ["POST"]).
methods: list<string>,
/// Whether the endpoint requires secret validation.
require-secret: bool,
}
/// Configuration for polling behavior.
record poll-config {
/// Polling interval in milliseconds (minimum 30000).
interval-ms: u32,
/// Whether polling is enabled.
enabled: bool,
}
/// Channel configuration returned by on-start.
record channel-config {
/// Human-readable display name.
display-name: string,
/// HTTP endpoints to register.
http-endpoints: list<http-endpoint-config>,
/// Optional polling configuration.
poll: option<poll-config>,
}
// ==================== Request/Response Types ====================
/// Incoming HTTP request from a webhook.
record incoming-http-request {
/// HTTP method (GET, POST, etc.).
method: string,
/// Request path.
path: string,
/// Request headers as JSON object string.
headers-json: string,
/// Query parameters as JSON object string.
query-json: string,
/// Request body bytes.
body: list<u8>,
/// Whether the webhook secret was validated by the host.
secret-validated: bool,
}
/// HTTP response to return to the webhook caller.
record outgoing-http-response {
/// HTTP status code.
status: u16,
/// Response headers as JSON object string.
headers-json: string,
/// Response body bytes.
body: list<u8>,
}
/// Agent response to be sent back to the channel.
record agent-response {
/// Unique message ID for correlation.
message-id: string,
/// Response content from the agent.
content: string,
/// Optional thread ID for threaded replies.
thread-id: option<string>,
/// Channel-specific metadata as JSON string.
metadata-json: string,
}
// ==================== Status Types ====================
/// Types of status updates the agent can send to channels.
enum status-type {
/// Agent is thinking/processing a response.
thinking,
/// Agent finished processing (response sent or about to be sent).
done,
/// Agent processing was interrupted.
interrupted,
/// A tool execution started.
tool-started,
/// A tool execution completed.
tool-completed,
/// A tool execution produced a preview/result status.
tool-result,
/// A tool call is waiting for user approval.
approval-needed,
/// Generic status text that should be shown to the user.
status,
/// A background/sandbox job was started.
job-started,
/// An extension/tool requires user authentication.
auth-required,
/// Authentication flow completed.
auth-completed,
}
/// A status update from the agent.
record status-update {
/// The type of status change.
status: status-type,
/// Human-readable description of the status.
message: string,
/// Channel-specific metadata as JSON string (e.g., contains chat_id for routing).
metadata-json: string,
}
// ==================== Lifecycle Callbacks ====================
/// Initialize the channel.
///
/// Called once when the channel is loaded. Returns configuration
/// describing HTTP endpoints and polling behavior.
///
/// Arguments:
/// - config-json: Channel configuration from the capabilities file.
///
/// Returns:
/// - Ok(channel-config): Configuration for the host to set up routing
/// - Err(string): Initialization failure message
on-start: func(config-json: string) -> result<channel-config, string>;
/// Handle an incoming HTTP request.
///
/// Called for each HTTP request to a registered endpoint.
/// Use emit-message to queue messages for the agent.
///
/// Arguments:
/// - req: The incoming HTTP request
///
/// Returns:
/// - HTTP response to send back to the caller
on-http-request: func(req: incoming-http-request) -> outgoing-http-response;
/// Handle a polling tick.
///
/// Called periodically if polling is configured.
/// Use emit-message to queue messages discovered during polling.
on-poll: func();
/// Deliver an agent response to the channel.
///
/// Called when the agent has generated a response to a message
/// that was emitted by this channel.
///
/// Arguments:
/// - response: The agent's response
///
/// Returns:
/// - Ok: Response delivered successfully
/// - Err(string): Delivery failure message
on-respond: func(response: agent-response) -> result<_, string>;
/// Notify the channel of agent status changes.
///
/// Called when the agent starts thinking, finishes, or changes state.
/// Channels can use this to show typing indicators or status messages.
///
/// Arguments:
/// - update: The status update
on-status: func(update: status-update);
/// Clean up channel resources.
///
/// Called when the channel is being unloaded.
on-shutdown: func();
}
/// World definition for sandboxed channels.
///
/// Channels import host capabilities and export the channel interface.
world sandboxed-channel {
import channel-host;
export channel;
}