Package:
prx:host@0.1.0
WIT source:wit/host/
Version: 0.1
This document is the authoritative reference for all host functions available to PRX WASM plugins. Each interface corresponds to a WIT file in wit/host/.
log— Structured Loggingconfig— Plugin Configurationkv— Key-Value Storagehttp-outbound— Outbound HTTPmemory— Long-Term Memoryevents— Event Bus
Interface: prx:host/log
WIT file: wit/host/log.wit
Permission required: None (always available)
Emit structured log messages that integrate with PRX's tracing infrastructure. All log messages are tagged with the emitting plugin's name.
interface log {
enum level {
trace,
debug,
info,
warn,
error,
}
log: func(level: level, message: string);
}Emit a log message at the specified severity level.
| Parameter | Type | Description |
|---|---|---|
level |
level |
Severity level (see below) |
message |
string |
Log message text |
Returns: nothing
Severity levels:
| Level | Value | Use case |
|---|---|---|
trace |
0 | Extremely verbose internal steps; disabled in production by default |
debug |
1 | Development-time debugging information |
info |
2 | Normal operational messages (most common) |
warn |
3 | Unexpected but recoverable situations |
error |
4 | Failures that affect plugin functionality |
Rust:
use prx_pdk::prelude::*;
log::trace("entering hot path");
log::debug(&format!("processing item: {id}"));
log::info("operation completed successfully");
log::warn(&format!("retrying after error: {e}"));
log::error(&format!("fatal: {e}"));Python:
from prx_pdk import host
host.log.trace("entering hot path")
host.log.debug(f"processing item: {id}")
host.log.info("operation completed successfully")
host.log.warn(f"retrying after error: {e}")
host.log.error(f"fatal: {e}")TypeScript:
import { log } from "@prx/pdk";
log.trace("entering hot path");
log.debug(`processing item: ${id}`);
log.info("operation completed successfully");
log.warn(`retrying after error: ${e}`);
log.error(`fatal: ${e}`);Go:
import "github.com/openprx/prx-pdk-go/host/log"
log.Trace("entering hot path")
log.Debug("processing item: " + id)
log.Info("operation completed successfully")
log.Warn("retrying after error: " + e.Error())
log.Error("fatal: " + e.Error())Interface: prx:host/config
WIT file: wit/host/config.wit
Permission required: "config" (always granted)
Provides read-only access to plugin-specific configuration values defined in the [config] section of plugin.toml. Values are set at deploy time and cannot be changed at runtime.
interface config {
get: func(key: string) -> option<string>;
get-all: func() -> list<tuple<string, string>>;
}Get a single configuration value by key.
| Parameter | Type | Description |
|---|---|---|
key |
string |
Configuration key |
Returns: option<string> — the value, or none if the key is not set
Get all configuration key-value pairs.
Returns: list<tuple<string, string>> — all key-value pairs defined in [config]
Rust:
use prx_pdk::prelude::*;
// Get a value (returns Option<String>)
let api_key = config::get("api_key");
// Get with a default fallback
let timeout = config::get_or("timeout_ms", "5000");
// Get all key-value pairs
let all: Vec<(String, String)> = config::get_all();
for (k, v) in &all {
log::debug(&format!("config: {k} = {v}"));
}Python:
from prx_pdk import host
value = host.config.get("api_key") # Optional[str]
value = host.config.get_or("timeout_ms", "5000") # str
pairs = host.config.get_all() # list[tuple[str, str]]TypeScript:
import { config } from "@prx/pdk";
const apiKey = config.get("api_key"); // string | undefined
const timeout = config.getOr("timeout_ms", "5000"); // string
const all = config.getAll(); // [string, string][]Go:
import "github.com/openprx/prx-pdk-go/host/config"
val, ok := config.Get("api_key")
timeout := config.GetOr("timeout_ms", "5000")
pairs := config.GetAll() // [][2]string[config]
api_key = "sk-..."
base_url = "https://api.example.com/v1"
timeout_ms = "5000"
max_retries = "3"
debug = "false"Interface: prx:host/kv
WIT file: wit/host/kv.wit
Permission required: "kv"
Isolated persistent key-value storage. Each plugin has its own namespace — plugins cannot read or write each other's keys. Data persists across plugin reloads and PRX restarts.
interface kv {
get: func(key: string) -> option<list<u8>>;
set: func(key: string, value: list<u8>) -> result<_, string>;
delete: func(key: string) -> result<bool, string>;
list-keys: func(prefix: string) -> list<string>;
}Retrieve a value by key.
| Parameter | Type | Description |
|---|---|---|
key |
string |
Key to retrieve |
Returns: option<list<u8>> — the raw bytes value, or none if the key does not exist
Store a value. Overwrites any existing value for the key.
| Parameter | Type | Description |
|---|---|---|
key |
string |
Key to store |
value |
list<u8> |
Raw bytes to store |
Returns: result<_, string> — ok(()) on success, err(message) on failure (e.g., storage limit exceeded)
Delete a key.
| Parameter | Type | Description |
|---|---|---|
key |
string |
Key to delete |
Returns: result<bool, string> — ok(true) if the key existed and was deleted, ok(false) if the key did not exist, err(message) on storage error
List all keys matching a prefix.
| Parameter | Type | Description |
|---|---|---|
prefix |
string |
Key prefix to filter by; use "" to list all keys |
Returns: list<string> — all keys with the given prefix, in unspecified order
- Namespace isolation: Keys are automatically namespaced per plugin. Two plugins can both have a key named
"state"without conflict. - Storage limit: Controlled by
resources.max_kv_storage_kbinplugin.toml(default: 1024 KB). Writing data that exceeds the limit returns an error. - Value type: Raw bytes (
list<u8>). Use PDK helpers for string and JSON serialization. - Atomicity: Individual
get/set/deleteoperations are atomic. Multi-key transactions are not supported.
The PDK adds has, increment, get_str/set_str, and get_json/set_json on top of the core WIT functions:
Rust:
use prx_pdk::prelude::*;
// Raw bytes
kv::set("raw", b"hello world").unwrap();
let bytes: Option<Vec<u8>> = kv::get("raw");
let existed = kv::delete("raw").unwrap(); // bool
let keys = kv::list_keys("prefix:"); // Vec<String>
// String helpers
kv::set_str("name", "Alice").unwrap();
let name: Option<String> = kv::get_str("name");
// JSON helpers (requires serde)
kv::set_json("user", &my_struct).unwrap();
let user: MyStruct = kv::get_json("user").unwrap();
// Atomic counter (PDK wrapper using get+set)
let count: i64 = kv::increment("call_count", 1).unwrap();
// Existence check (PDK wrapper using get)
let exists = kv::has("my_key");Python:
from prx_pdk import host
host.kv.set("raw", b"hello")
data: bytes | None = host.kv.get("raw")
host.kv.set_str("name", "Alice")
name: str | None = host.kv.get_str("name")
host.kv.set_json("config", {"debug": True})
obj = host.kv.get_json("config")
existed = host.kv.delete("raw") # bool
keys = host.kv.list_keys("") # list[str]
count = host.kv.increment("calls", delta=1) # int (new value)TypeScript:
import { kv } from "@prx/pdk";
kv.set("raw", new Uint8Array([104, 101, 108, 108, 111]));
const bytes = kv.get("raw"); // Uint8Array | undefined
kv.setString("name", "Alice");
const name = kv.getString("name"); // string | undefined
kv.setJson("config", { debug: true });
const config = kv.getJson<{ debug: boolean }>("config");
kv.delete("raw"); // boolean
kv.listKeys(""); // string[]
kv.increment("calls", 1); // number (new value)Go:
import "github.com/openprx/prx-pdk-go/host/kv"
_ = kv.Set("raw", []byte("hello"))
data, ok := kv.Get("raw")
_ = kv.SetString("name", "Alice")
name, ok := kv.GetString("name")
_ = kv.SetJSON("state", jsonBytes)
data, ok = kv.GetJSON("state")
existed, err := kv.Delete("raw")
keys := kv.ListKeys("")pub fn execute_impl(args_json: &str) -> PluginResult {
// Increment call counter
let calls = kv::increment("total_calls", 1).unwrap_or(0);
log::info(&format!("Invocation #{calls}"));
// Store last invocation time
let now = clock::now_ms().to_string();
kv::set_str("last_call_ms", &now).unwrap();
// ... main logic ...
PluginResult::ok("done")
}Interface: prx:host/http-outbound
WIT file: wit/host/http.wit
Permission required: "http-outbound" + http_allowlist in plugin.toml
Make controlled outbound HTTP requests. All URLs are validated against the plugin's http_allowlist before any network connection is made.
interface http-outbound {
record http-response {
status: u16,
headers: list<tuple<string, string>>,
body: list<u8>,
}
request: func(
method: string,
url: string,
headers: list<tuple<string, string>>,
body: option<list<u8>>,
) -> result<http-response, string>;
}| Field | Type | Description |
|---|---|---|
status |
u16 |
HTTP status code (e.g., 200, 404, 500) |
headers |
list<tuple<string, string>> |
Response headers as name-value pairs |
body |
list<u8> |
Response body as raw bytes |
Make an HTTP request.
| Parameter | Type | Description |
|---|---|---|
method |
string |
HTTP method: "GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS" |
url |
string |
Full URL including scheme, host, and path |
headers |
list<tuple<string, string>> |
Request headers as name-value pairs |
body |
option<list<u8>> |
Request body bytes, or none for no body |
Returns: result<http-response, string> — the response on success, or an error message on failure
Failure reasons:
- URL not in
http_allowlist - Network error (DNS failure, connection refused, timeout)
- Request count limit reached (
max_http_requests)
[permissions]
required = ["http-outbound"]
# Required when using http-outbound: list of allowed origins
http_allowlist = [
"https://api.openweathermap.org",
"https://api.github.com",
"https://httpbin.org",
]Rust:
use prx_pdk::prelude::*;
// GET request
let resp = http::get("https://api.example.com/data", &[]).unwrap();
println!("Status: {}", resp.status);
let body_str = resp.body_text(); // String (UTF-8 decoded)
let json: serde_json::Value = resp.json().unwrap();
// POST with JSON body
let payload = serde_json::json!({ "city": "London" });
let resp = http::post_json(
"https://api.example.com/weather",
&[("Authorization", "Bearer my-token")],
&payload,
).unwrap();
// Generic request
let resp = http::request(
"DELETE",
"https://api.example.com/items/42",
&[
("Authorization", "Bearer my-token"),
("X-Request-Id", "abc-123"),
],
None, // no body
).unwrap();
// PUT with raw body
let body = b"raw body bytes";
let resp = http::request(
"PUT",
"https://api.example.com/blob/key",
&[("Content-Type", "application/octet-stream")],
Some(body),
).unwrap();Python:
from prx_pdk import host
# GET
resp = host.http.get("https://api.example.com/data")
print(resp.status) # int
print(resp.text()) # str
print(resp.json()) # any (parsed JSON)
# POST JSON
resp = host.http.post_json(
"https://api.example.com/weather",
{"city": "London"},
headers=[("Authorization", "Bearer token")],
)
# Generic
resp = host.http.request(
"DELETE",
"https://api.example.com/items/42",
headers=[("Authorization", "Bearer token")],
body=None,
)TypeScript:
import { http } from "@prx/pdk";
// GET
const resp = http.get("https://api.example.com/data");
const text = http.bodyText(resp); // string
const json = http.bodyJson<MyType>(resp); // MyType
// POST JSON
const resp2 = http.postJson(
"https://api.example.com/submit",
{ key: "value" },
[["Authorization", "Bearer token"]],
);
// Generic
const resp3 = http.request(
"DELETE",
"https://api.example.com/items/42",
[["Authorization", "Bearer token"]],
undefined,
);Go:
import "github.com/openprx/prx-pdk-go/host/http"
headers := [][2]string{{"Authorization", "Bearer token"}}
// GET
resp, err := http.Get("https://api.example.com/data", headers)
fmt.Println(resp.Status, resp.BodyText())
// POST JSON
resp, err = http.PostJSON("https://api.example.com/submit", headers, jsonBody)
// Generic
resp, err = http.Request("DELETE", url, headers, nil)match http::get("https://api.example.com/data", &[]) {
Ok(resp) if resp.status == 200 => {
let json = resp.json().unwrap();
// handle success
}
Ok(resp) => {
log::warn(&format!("API returned status {}", resp.status));
}
Err(e) => {
// URL not in allowlist, network error, or limit exceeded
log::error(&format!("HTTP error: {e}"));
return PluginResult::err(format!("Request failed: {e}"));
}
}Interface: prx:host/memory
WIT file: wit/host/memory.wit
Permission required: "memory"
Access PRX's semantic memory system. Memories are indexed for similarity search, enabling recall by meaning rather than exact key match.
interface memory {
record memory-entry {
id: string,
text: string,
category: string,
importance: f64,
}
store: func(text: string, category: string) -> result<string, string>;
recall: func(query: string, limit: u32) -> result<list<memory-entry>, string>;
}| Field | Type | Description |
|---|---|---|
id |
string |
Unique entry identifier (UUID) |
text |
string |
The stored text |
category |
string |
Category label for filtering |
importance |
f64 |
Relevance score (0.0–1.0), higher = more relevant to query |
Store text in memory.
| Parameter | Type | Description |
|---|---|---|
text |
string |
Text content to store |
category |
string |
Category label (e.g., "fact", "preference", "entity") |
Returns: result<string, string> — the new entry's ID on success, error message on failure
Category conventions:
| Category | Use for |
|---|---|
"fact" |
Objective facts |
"preference" |
User preferences and settings |
"decision" |
Decisions made |
"entity" |
People, places, organizations |
"other" |
Miscellaneous |
Search memories by semantic similarity.
| Parameter | Type | Description |
|---|---|---|
query |
string |
Natural language search query |
limit |
u32 |
Maximum number of results to return |
Returns: result<list<memory-entry>, string> — matching entries sorted by relevance descending, or error
Rust:
use prx_pdk::prelude::*;
// Store a memory
let id = memory::store("User prefers dark mode", "preference")
.map_err(|e| PluginResult::err(e))?;
log::info(&format!("Stored memory: {id}"));
// Recall by semantic query
let entries = memory::recall("user interface preferences", 5)
.unwrap_or_default();
for entry in &entries {
log::debug(&format!(
"[{:.2}] {}: {}",
entry.importance, entry.id, entry.text
));
}
// Use top result
if let Some(top) = entries.first() {
log::info(&format!("Best match: {}", top.text));
}Python:
from prx_pdk import host
# Store
entry_id = host.memory.store("User prefers dark mode", category="preference")
# Recall
entries = host.memory.recall("user interface preferences", limit=5)
for e in entries:
print(f"[{e.importance:.2f}] {e.id}: {e.text}")TypeScript:
import { memory } from "@prx/pdk";
import type { MemoryEntry } from "@prx/pdk";
// Store
const id = memory.store("User prefers dark mode", "preference");
// Recall
const entries: MemoryEntry[] = memory.recall("user interface preferences", 5);
entries.forEach(e => console.log(`[${e.importance.toFixed(2)}] ${e.text}`));Go:
import "github.com/openprx/prx-pdk-go/host/memory"
// Store
id, err := memory.Store("User prefers dark mode", "preference")
// Recall
entries, err := memory.Recall("user interface preferences", 5)
for _, e := range entries {
// e.ID, e.Text, e.Category, e.Importance
}Interface: prx:host/events
WIT file: wit/host/event.wit
Permission required: "events"
Fire-and-forget publish/subscribe event bus for inter-plugin communication and integration with PRX lifecycle events. All events flow through the host for auditing and access control.
interface events {
publish: func(topic: string, payload: string) -> result<_, string>;
subscribe: func(topic-pattern: string) -> result<u64, string>;
unsubscribe: func(subscription-id: u64) -> result<_, string>;
}Publish an event to a topic.
| Parameter | Type | Description |
|---|---|---|
topic |
string |
Event topic (dot-separated, e.g., "weather.update") |
payload |
string |
JSON-encoded event payload (max 64 KB) |
Returns: result<_, string> — ok(()) on success, err(message) on failure
Failure reasons:
- Payload is not valid JSON
- Payload exceeds 64 KB
- Recursion limit reached (plugin publishing to itself)
Subscribe to a topic pattern.
| Parameter | Type | Description |
|---|---|---|
topic-pattern |
string |
Topic pattern: exact ("tool.call") or wildcard ("tool.*") |
Returns: result<u64, string> — subscription ID on success (use with unsubscribe), error message on failure
Subscriptions are active for the lifetime of the plugin invocation or until unsubscribe is called.
Cancel a subscription.
| Parameter | Type | Description |
|---|---|---|
subscription-id |
u64 |
Subscription ID returned by subscribe |
Returns: result<_, string> — ok(()) on success, err(message) if the ID is invalid
| Pattern | Matches |
|---|---|
"weather.update" |
Exactly weather.update only |
"weather.*" |
weather.update, weather.alert, weather.clear, etc. |
"prx.lifecycle.*" |
All PRX lifecycle events |
"*" |
All events (use with care) |
| Topic | Description | Payload fields |
|---|---|---|
prx.lifecycle.agent_start |
Agent loop started | agent_id: string |
prx.lifecycle.agent_stop |
Agent loop stopped | agent_id: string, reason: string |
tool.call |
Tool invoked by LLM | tool_name: string, args: object, session_id: string |
tool.result |
Tool completed | tool_name: string, success: bool, duration_ms: number |
llm.request |
Request sent to LLM | model: string, message_count: number |
llm.response |
Response from LLM | model: string, tokens_used: number |
error |
Error occurred | message: string, context: string |
Rust:
use prx_pdk::prelude::*;
// Publish
events::publish("my.plugin.result", r#"{"status":"ok","count":42}"#).unwrap();
// Publish with auto-serialized JSON
events::publish_json("my.plugin.result", &serde_json::json!({
"status": "ok",
"count": 42,
})).unwrap();
// Subscribe and track subscription
let sub_id = events::subscribe("prx.lifecycle.*").unwrap();
// ... plugin logic ...
events::unsubscribe(sub_id).unwrap();Python:
from prx_pdk import host
# Publish
host.events.publish("my.plugin.result", '{"status":"ok","count":42}')
host.events.publish_json("my.plugin.result", {"status": "ok", "count": 42})
# Subscribe
sub_id = host.events.subscribe("prx.lifecycle.*")
# ... plugin logic ...
host.events.unsubscribe(sub_id)TypeScript:
import { events } from "@prx/pdk";
// Publish
events.publish("my.plugin.result", JSON.stringify({ status: "ok", count: 42 }));
events.publishJson("my.plugin.result", { status: "ok", count: 42 });
// Subscribe (returns bigint subscription ID)
const subId = events.subscribe("prx.lifecycle.*");
// ...
events.unsubscribe(subId);Go:
import "github.com/openprx/prx-pdk-go/host/events"
// Publish
err := events.Publish("my.plugin.result", `{"status":"ok","count":42}`)
err = events.PublishJSON("my.plugin.result", jsonPayload)
// Subscribe
id, err := events.Subscribe("prx.lifecycle.*")
// ...
err = events.Unsubscribe(id)Hook plugins receive events via the on-event export rather than subscribe. Use subscribe for in-plugin dynamic subscriptions during a single invocation.
// Hook plugin: event patterns are declared in plugin.toml
// The host calls on-event for matching events
pub fn on_event_impl(event: &str, payload_json: &str) -> Result<(), String> {
match event {
"tool.call" => {
let payload: serde_json::Value = serde_json::from_str(payload_json)
.unwrap_or_default();
let tool = payload["tool_name"].as_str().unwrap_or("unknown");
let _ = kv::increment(&format!("count:{tool}"), 1);
}
"error" => {
// Forward errors as a new event for monitoring plugins
events::publish("audit.error", payload_json).ok();
}
_ => {}
}
Ok(())
}| Constraint | Value | Notes |
|---|---|---|
| Max payload | 64 KB | Per event; JSON must be valid |
| Delivery | Async, fire-and-forget | No delivery confirmation |
| Ordering | Best-effort | Events may be reordered under load |
| Recursion | Blocked | Plugin cannot receive events it published to itself |
| Permission | Interface | Worlds available in | Default |
|---|---|---|---|
| (none) | prx:host/log |
all | ✅ always granted |
config |
prx:host/config |
all | ✅ always granted |
kv |
prx:host/kv |
tool, middleware, hook, cron | ❌ must declare |
events |
prx:host/events |
all | ❌ must declare |
http-outbound |
prx:host/http-outbound |
tool, provider, storage | ❌ must declare |
memory |
prx:host/memory |
tool | ❌ must declare |
| Host function | tool |
middleware |
hook |
cron |
provider |
storage |
|---|---|---|---|---|---|---|
log |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
config |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
kv |
✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
http-outbound |
✅ | ❌ | ❌ | ❌ | ✅ | ✅ |
memory |
✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
events |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
"✅" = interface is imported in the world (may still require permissions.required declaration).
"❌" = not available in this world.
All WIT definitions are in wit/host/ relative to the PRX project root:
| File | Interface | Description |
|---|---|---|
wit/host/log.wit |
prx:host/log |
Structured logging |
wit/host/config.wit |
prx:host/config |
Plugin configuration |
wit/host/kv.wit |
prx:host/kv |
Key-value storage |
wit/host/http.wit |
prx:host/http-outbound |
Outbound HTTP |
wit/host/memory.wit |
prx:host/memory |
Long-term memory |
wit/host/event.wit |
prx:host/events |
Event bus |
World definitions (which interfaces are imported/exported per plugin type) are in wit/worlds.wit.
Plugin export interfaces (what plugins must implement) are in wit/plugin/:
| File | Interface | Capability |
|---|---|---|
wit/plugin/tool.wit |
prx:plugin/tool-exports |
tool |
wit/plugin/hook.wit |
prx:plugin/hook-exports |
hook |
wit/plugin/middleware.wit |
prx:plugin/middleware-exports |
middleware |
wit/plugin/cron.wit |
prx:plugin/cron-exports |
cron |
wit/plugin/provider.wit |
prx:plugin/provider-exports |
provider |
wit/plugin/storage.wit |
prx:plugin/storage-exports |
storage |