kamaji-procctl 0.8.30

Producer helper for the yah process-control channel: bind $YAH_CONTROL_SOCK and answer `status` with a state document in kamaji's WorkloadState vocabulary. Dependency-light by design — see W315.
Documentation

Producer helper for the yah process-control channel — the two lines a workload writes so its supervisor stops having to guess at it.

What the channel is

A supervisor watching a process from outside can ask exactly two questions: is the pid alive, and is the port open. Both are proxies. A process that has finished booting, one still replaying a WAL, and one wedged on a lock answer them identically — so the real answer has always been somewhere in stdout, and reading it means grepping a log tail for a sentence nobody agreed on.

W315's rule: any process built to run under a yah camp SHOULD expose a control channel. One verb, status, answering with a status document:

{"state":"running","pid":71455,"uptime_secs":41,"detail":"3 windows open"}

state is the only required field, and its vocabulary is exactly [kamaji_proto::WorkloadState] — pending | starting | running | draining | exited | failed. That is the whole compatibility story: the supervisor already answers a Probe verb in these words, so a workload reporting in the same words is believed verbatim instead of run through a translation table that rots the first time either side gains a state. Build with the kamaji feature and the From impls between the two enums are exhaustive matches — adding a state to either side stops compiling until both agree.

Using it

use procctl::{ProcState, ProcStatus};

# fn windows_open() -> usize { 3 }
# fn booted() -> bool { true }
// Hold the guard for as long as the process should answer. Dropping it
// stops the listener and unlinks the socket.
let _control = procctl::serve_env(|| {
    if booted() {
        ProcStatus::new(ProcState::Running)
            .with_detail(format!("{} windows open", windows_open()))
    } else {
        ProcStatus::new(ProcState::Starting)
    }
})?;
# Ok::<(), std::io::Error>(())

[serve_env] returns Ok(None) when YAH_CONTROL_SOCK is unset, which is the contract: the same binary runs unchanged outside a camp, and a process MUST NOT fail for the variable's absence.

The closure runs on the listener thread, on demand, once per request. It must not block for long and must not panic — a panic there takes the listener down with it, which reads to the supervisor as a process that stopped answering.

What this crate deliberately is not

It is not required. The protocol is one newline-delimited JSON verb precisely so the producer side is implementable in twenty lines, with no dependency, in any language, by someone whose actual job that day is their own app. A Bun script or a Python daemon emitting the same line is a first-class conforming producer. This crate is ergonomics for the Rust case, not a gate — and that is also why it does not reach for kamaji-proto's postcard wire, which would make conforming mean linking a Rust crate (W315 §"Why newline-JSON").

The client feature adds the consumer half (async, tokio) for supervisors. Producers should leave it off; the default build is std-only.

@arch:see(.yah/docs/working/W315-process-control-channel.md)