Guest Contract
- The World
- Exports
- Imports
- Lifecycle
- Error Handling
- No WASI CLI Imports
- Preview 1 Is Not Supported
- Reference Implementation
Every component the daemon loads, the bundled Python interpreter and every custom function alike, implements one WIT world.
The World
package babysoarus:exec@0.1.0;
interface host {
record http-response {
status: u16,
headers: list<tuple<string, string>>,
body: list<u8>,
}
splunk-request: func(
method: string,
path: string,
headers: list<tuple<string, string>>,
body: list<u8>,
timeout-ms: u32,
) -> result<http-response, string>;
get-lookups-dirs: func() -> list<string>;
get-run-context: func() -> string;
}
world exec {
import host;
import wasi:http/types@0.2.12;
import wasi:http/outgoing-handler@0.2.12;
export begin-run: func(code: option<string>) -> result<_, string>;
export execute-batch: func(batch-json: string) -> result<string, string>;
export end-run: func() -> result<_, string>;
}
Exports
Begin-run(code) -> result<_, string>
Called once per search or alert-action invocation, even when the underlying instance has served many previous runs.
code is the inline source, if any. The Python interpreter compiles it here,
once, and caches the code object. A fixed custom function ignores it.
Returning the error arm is a normal, reportable condition, such as a syntax error in a user's snippet. The message reaches the search.
Reset per-run state here. Anything the previous run left behind must not be visible.
Execute-batch(batch-json) -> result<string, string>
Called once per micro-batch. The argument is a JSON array of objects; the return value is a JSON array of objects.
Returning fewer objects filters. Returning more expands. Returning an empty array drops the batch.
Reordering is safe too, but only ever sorts within the current micro-batch,
since that is all one call sees. To sort a whole result set, use a trailing SPL
| sort after the command, as every detection in the shipped library does.
End-run() -> result<_, string>
Called once when the run finishes, before the instance returns to the pool. Clear run-scoped state here.
If this traps or reports an error, the host drops the instance rather than pooling it.
Imports
Wasi:http/outgoing-handler
General outbound HTTP. Guest code has no sockets, so this is the only way to reach anything that is not Splunk itself.
It is a standard WASI interface rather than an babysoarus-specific import, which means two things. Any component-model toolchain can produce a client for it without knowing anything about BabySOARus. And every request passes through the daemon's network policy, so what a function may reach is configuration rather than code.
Splunk-request(...) -> result<http-response, string>
Calls splunkd's management API using this run's credentials.
path names an endpoint, not a URL: it is resolved against the base_url
splunkd handed the run, and a value that parses as an absolute URL is
rejected. The host attaches the credentials after resolving, so guest code
never holds a Splunk session token and cannot send one anywhere else.
This is separate from wasi:http because Splunk's management port normally
presents a self-signed certificate. The host owns that one narrow exception;
the general path keeps full verification.
timeout-ms of 0 means the default of 30 seconds. The host caps it at 300
seconds and caps the response body at 64 MB.
Get-lookups-dirs() -> list<string>
Returns the guest paths where Splunk lookup directories are mounted, typically
/lookups/system and /lookups/apps/<app>. The directories themselves are
reachable as ordinary WASI preopened directories.
Get-run-context() -> string
Returns the current run's context as a JSON object:
{
"auth_headers": [["Authorization", "Splunk <token>"]],
"base_url": "https://127.0.0.1:8089",
"app": "search",
"sid": "1754000000.42",
"owner": "admin",
"platform": "linux",
"generating": false
}
Provided as an import rather than as a begin-run parameter so that custom
functions can reach the context without every one of them having to accept and
parse it.
auth_headers is on its way out. splunk-request no longer needs it, and it
will be removed once the host injects secrets for every destination rather
than just for Splunk. Do not build anything new on it.
Lifecycle
instantiate once per pooled instance
|
+-- begin-run(code) once per search
| |
| +-- execute-batch(...) once per micro-batch
| +-- execute-batch(...)
| +-- ...
| |
| +-- end-run() once per search
|
+-- (returned to the pool, reused by an unrelated search)
State that survives instantiation is where the performance is: caches, compiled patterns, parsed tables. Build them lazily on first use and every later search benefits.
State that must not survive a run belongs behind begin-run and end-run.
Error Handling
| Outcome | Meaning | Instance |
|---|---|---|
Ok | Success | Returned to the pool |
Err(message) | Guest-reported error, such as bad user code | begin-run: dropped. execute-batch: kept |
| Trap or panic | Unexpected failure | Dropped |
| Deadline exceeded | Ran too long | Dropped |
A trap is treated as poisoning because guest globals could be in any state. A
reported error during execute-batch is not, because the guest chose to
report it and its invariants presumably hold.
No WASI CLI Imports
The world deliberately does not import wasi:cli/{stdin,stdout,stderr}.
The contract is JSON in, JSON out, with explicit error channels, so guest stdio is not load-bearing.
That is a statement about the contract, not about the component. The bundled
Python interpreter's own build imports the whole of wasi:cli for CPython's
benefit, and the daemon's linker provides full WASI Preview 2, so filesystem
access to lookup directories works normally.
Preview 1 Is Not Supported
Only genuine WASI Preview 2 components load. Core modules built for
wasm32-wasip1, including anything exporting a raw process(ptr) -> ptr C
ABI, are rejected.
Supporting Preview 1 would mean carrying an adapter module in every instance, which costs memory in the pool and adds a compatibility surface. Every loaded component here is backed by a single core instance.
Reference Implementation
The app ships a compiled reference component, installed as
functions/public/example.wasm and callable as exec function=example. Its
complete Rust source, with the decisions explained, is walked through in
Custom WebAssembly Functions.
It is also the fixture BabySOARus's own test suite runs against, so it is
guaranteed to stay correct.