Documentation
¶
Overview ¶
Package detecthost is the wazero-based HOST runtime for wasip1 WASM detector sidecars. It lives OUTSIDE core/ on purpose, mirroring connect/exec: core/detect (the detection seam) is pure stdlib + pkg/event/pkg/finding/pkg/baseline and forbids exec/runtime dependencies via core/lint — this package imports github.com/tetratelabs/wazero, which the core purity lint would reject if it lived under core/. The seam is honored by the process/runtime boundary itself: detecthost wraps one compiled wasm module and exposes it as an ordinary core/detect.Detector; core/detect never links wazero, it only ever sees the Detector interface.
GUEST MODEL (ruled, see mallcoppro-2fd / mallcoppro-f70): the guest is a wasip1 COMMAND — a Go program compiled with GOOS=wasip1 GOARCH=wasm whose main is `os.Exit(detectorhost.Run(myDetector{}))` (package github.com/mallcop-app/mallcop/pkg/detectorhost). It reads ONE JSON document {"events":[...], "baseline":{...}} on stdin and writes a JSON array of findings to stdout; a nonzero exit is a detector/harness error, surfaced loudly by this package (see Detector.Detect below), never silently dropped.
SAFETY: every sidecar instantiation is ZERO-CAPABILITY. NewRuntime wires ONLY wasi_snapshot_preview1 (stdio + the base WASI surface every wasip1 Go binary needs to start up) — no preopened directories (no ModuleConfig.WithFS / WithFSConfig call anywhere in this package), no environment variables (no WithEnv call), and no network (wasi_snapshot_preview1 predates BSD sockets; there is no network syscall for a guest to reach even if it tried). The guest's ONLY channel to the outside world is the stdin/stdout/stderr pipes Detect wires per call.
TIMEOUT: NewRuntime enables wazero's RuntimeConfig.WithCloseOnContextDone, and Detect calls context.WithTimeout per invocation — wazero checks the context at every function call AND every loop back-edge when this option is set, so a guest spinning in a pure CPU infinite loop (not just one blocked on I/O) is forcibly terminated when the deadline fires. See detecthost_test.go's TestDetectKillsHungGuest for the proof (a deliberately hanging module, run through the real host).
Index ¶
Constants ¶
const DefaultTimeout = 8 * time.Second
DefaultTimeout bounds a single sidecar invocation's wall-clock runtime. It is deliberately generous relative to core/detect's own 5s detectorTimeout — a sidecar wrapped by detecthost is itself a detect.Detector, so it is ALSO subject to that outer quarantine deadline; this inner timeout exists so a hung guest is torn down (wazero module.Close) rather than merely abandoned (which is all the outer goroutine-based timeout can do to a misbehaving Go detector).
Variables ¶
This section is empty.
Functions ¶
func CacheDir ¶
CacheDir returns the on-disk wazero compilation cache directory for a sidecar dir at sidecarsDir: sidecarsDir's PARENT joined with ".mallcop/wasmcache". This mirrors the existing store-adjacent-state convention connect/exec.defaultCursorDir uses for kind:cloud connector cursors (<store>/.mallcop/cursors/<id>) — reusing the ".mallcop" reserved subdirectory name for another piece of mallcop-owned local state — but is rooted at the sidecar directory's parent rather than the git-backed findings store, because `mallcop detect` / `mallcop exam-detect` load sidecars with no store path available at all (only `mallcop scan` takes --store). Under the config default (detectors.sidecars.dir: ./detectors/bin), the parent IS learning.dir's default value ("detectors"), so the cache also lands naturally alongside the loop-owned learning overlay for the common case where sidecars ship inside the same learning tree.
func NewRuntime ¶
NewRuntime constructs the shared wazero Runtime every sidecar Detector in a process is instantiated from. It is zero-capability by construction (see the package doc) and wires an on-disk CompilationCache at cacheDir so a module already compiled once is loaded from cache rather than recompiled — this matters because mallcop scan/detect/exam-detect are one-shot CLI invocations that would otherwise pay full wasm compilation cost on every run. cacheDir is created if missing; an empty cacheDir disables the on-disk cache (compilation is still cached IN-MEMORY for the lifetime of the returned Runtime, just not persisted across process invocations).
Callers own the returned Runtime's lifecycle (Close it when done); every Detector Loaded from it shares the same compilation cache and WASI instance.
Types ¶
type Detector ¶
type Detector struct {
// Timeout bounds a single Detect invocation's wall-clock runtime (see the
// package doc). Load sets it to DefaultTimeout; callers/tests may override
// it afterward (e.g. to prove termination without waiting out the full
// default).
Timeout time.Duration
// contains filtered or unexported fields
}
Detector wraps one compiled wasip1 WASM module as a core/detect.Detector. Construct one via Load. The zero value is not usable.
func Load ¶
Load reads and compiles the wasm module at path against the given shared Runtime (see NewRuntime) and returns a Detector wrapping it. Compilation happens HERE, at load time — a malformed/invalid wasm module is a loud error before any scan runs, never a failure silently discovered mid-scan. Name() is the file's basename with the .wasm extension trimmed (e.g. "my-rule.wasm" -> "my-rule") — the simplest of the two naming schemes the design allows (filename stem vs. a handshake), chosen because it needs no extra guest protocol and keeps discovery a pure filesystem operation.
func (*Detector) Detect ¶
Detect implements detect.Detector by running the wrapped wasm module as a fresh, zero-capability instance: it marshals {events, bl} to the detectorhost.Input wire document, feeds it on the guest's stdin, waits (under a d.Timeout deadline enforced by wazero's WithCloseOnContextDone — see the package doc) for the guest to run to completion, and unmarshals its stdout as the findings array.
core/detect.Detector has no error return, so a sidecar failure (nonzero exit, a killed-on-timeout guest, or malformed stdout) is reported via PANIC — this is the only channel available under the frozen interface, and it is not a silent drop: core/detect's own per-detector runDetectSafely recovers the panic, QUARANTINES this detector (dropping only its output), and logs the reason via quarantineReporter, so the failure is loud (stderr/log) while every other detector's findings still come back. This composes with the existing framework machinery unchanged — detecthost adds no new isolation primitive of its own.