package near:agent@0.3.0; // 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// to prevent escape // - Message emission is rate-limited /// 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//. /// Path must be relative (no leading /) and cannot contain "..". /// Returns None if the file doesn't exist. workspace-read: func(path: string) -> option; /// 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, } /// 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>, timeout-ms: option, ) -> result; /// 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 file or media attachment on an inbound message (channel → agent). /// /// Core fields are part of the record. Extended metadata (duration, dimensions, /// codec, etc.) goes in `extras-json` to avoid WIT record changes when new /// properties are needed. Binary data (e.g., downloaded voice bytes) should be /// stored via `store-attachment-data` rather than inlined in the record. record inbound-attachment { /// Unique identifier within the channel (e.g., Telegram file_id). id: string, /// MIME type (e.g., "image/jpeg", "audio/ogg", "application/pdf"). mime-type: string, /// Original filename, if known. filename: option, /// File size in bytes, if known. size-bytes: option, /// URL to download the file from the channel's API. /// May require authentication (handled by host credential injection). source-url: option, /// Opaque key for host-side storage (e.g., after download/caching). storage-key: option, /// Extracted text content (e.g., OCR result, PDF text, audio transcript). extracted-text: option, /// Extensible metadata as JSON string. /// /// Used for properties that may be added over time without changing WIT. /// Well-known keys: /// - "duration_secs": u32 — duration in seconds (audio/video) /// - "width": u32, "height": u32 — pixel dimensions (images/video) /// - "codec": string — audio/video codec /// - "thumbnail_file_id": string — thumbnail identifier extras-json: string, } /// Store binary data for an attachment (e.g., downloaded voice note bytes). /// /// Call this before emit-message to associate raw bytes with an attachment. /// The host retrieves the data after the callback using the attachment ID. /// /// Security: /// - Maximum 20MB per attachment /// - Maximum 50MB total per callback execution /// - Data is cleared after the callback completes store-attachment-data: func(attachment-id: string, data: list) -> result<_, string>; /// 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, /// Message content. content: string, /// Optional thread ID for threaded conversations. thread-id: option, /// Channel-specific metadata as JSON string. metadata-json: string, /// File or media attachments on this message. attachments: list, } /// 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//. /// 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; /// Check if a sender is allowed (in allowFrom store). pairing-is-allowed: func( channel: string, id: string, username: option ) -> result; /// Read the allowFrom list (for merging with config allowFrom). pairing-read-allow-from: func(channel: string) -> result, 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, /// 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, /// Optional polling configuration. poll: option, } // ==================== 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, /// 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, } /// A file or image attachment on an outbound message (agent → channel). /// /// Contains raw file bytes for the channel to upload/send. record attachment { /// Original filename (e.g., "screenshot.png"). filename: string, /// MIME type (e.g., "image/png"). mime-type: string, /// Raw file bytes. data: list, } /// 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, /// Channel-specific metadata as JSON string. metadata-json: string, /// File/image attachments to send. attachments: list, } // ==================== 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; /// 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); /// Send a proactive message to a user without a prior incoming message. /// /// Used for broadcasts, alerts, and agent-initiated messages with attachments. /// The user-id identifies the target user within the channel. /// /// Arguments: /// - user-id: Target user identifier (e.g., Telegram chat_id) /// - response: The message content and attachments to send /// /// Returns: /// - Ok: Message delivered successfully /// - Err(string): Delivery failure message on-broadcast: func(user-id: string, response: agent-response) -> result<_, string>; /// 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; }