Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Guest Contract

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

OutcomeMeaningInstance
OkSuccessReturned to the pool
Err(message)Guest-reported error, such as bad user codebegin-run: dropped. execute-batch: kept
Trap or panicUnexpected failureDropped
Deadline exceededRan too longDropped

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.