heliograph

The station

The far side. A directory of plain bash, planted into a private transport repo, that watches for a request, runs one step, and sends the log back.

It is the half of heliograph you cannot reach, so it is the half most worth reading before you run it. Everything here is text you can open in an editor on the machine it will run on.

You push a step to the transport. The station pulls it, runs it, and pushes the captured log back to the same place, where you read it. youthe CLI, or an agent transportgit, share, relay, store stationthe far side step step log log Nothing reaches in. Every command runs because somebody with access chose to run it.
The operator starts it once. Everything after that is the transport.

What it depends on

Bash 4+ and GNU coreutils, plus whatever the transport needs: git for the git transport, curl for blob and relay. That is the whole list, and it is the entire proposition - on a locked-down box, installing anything is its own change request. Nothing is ever installed on the far side.

CI enforces it. No Go, no binary, no interpreter and no package may appear under station/. The one Go file permitted is station/embed.go, which never ships anywhere.

There is exactly one exception, argued for explicitly rather than smuggled in: the relay transport needs heliograph-seal, because its construction is X25519, HKDF-SHA256, ChaCha20-Poly1305 and Ed25519, and hand-assembling those in shell across openssl versions is where crypto bugs live and where they are silent. Every other transport stays pure bash.

The files

start.shthe preflight, then hand over. The one command the operator types
station.shthe loop: poll, decide, dispatch, publish
run.shthe step runner. Owns the log, the timestamps and the delivery
caprun.shthe same capture around an arbitrary command, for ad-hoc use
caplib.shthe capture itself, and the only implementation of it
transports/git.sh, blob.sh, relay.sh - one file per channel
steps/one file per question. _template.sh to start from
lib/helpers a step can source: probes, ansible, terraform, remote hosts
ops-logs/where captured logs land, and are committed from
station/requestwhat to run. station/status - what happened

Details of each: the runner, writing a step.

The loop, precisely

1. Poll the transport for a request. Default every 5 seconds 2. The trigger is the id, never a new commit. Documentation and step edits land constantly; if any change fired a run, the station would fire on all of them. A run is always something somebody asked for on purpose 3. Ask run.sh --mode what the step declares itself to be, and apply the gates 4. Publish running, then dispatch the step in its own process group 5. Keep polling while it works, so a cancel is heard and an hour-long step does not make the station deaf for an hour 6. Push a partial log every PROGRESS_EVERY seconds, so a long run can be watched rather than waited out 7. Deliver the finished log, then publish idle - or undelivered, if the log was captured and the transport would not take it

The states it publishes

state
startingthe loop is up and its credential works. Nothing asked yet
runninga step is in flight
idlethe run finished and the log was delivered. Only a confirmed delivery earns it
undeliveredthe run finished and the log did not arrive - refused by the transport, skipped, or the runner never reached delivery. The reason is published with it
refuseda gate said no, and says which one
cancelledsignalled mid-run. The partial log is kept
stoppedthe loop ended, by stop: yes or by Ctrl-C

undelivered matters more than it looks. Without it, "the log exists and cannot be shipped" and "the step is still running" are the same silence from your side, and only one of them is worth waiting on.

Two properties everything else rests on

Every captured line carries a UTC timestamp, applied by a pure-bash read loop reading straight from the command, before any other stage. That ordering is load-bearing. It used to be applied last, which made the property depend on sed -u; a sed without it gave every line in a block the same time while the log still read perfectly. After the fact, in an untimed log, a hang and slow progress are indistinguishable.

A failed run still ships, and a failed delivery never loses the log. The log is delivered whether the step passed or failed, and the real exit code survives the pipeline via PIPESTATUS. Each round trip through an operator is expensive; none may be wasted by tooling that only reports success.

Both are asserted by the conformance suite, which is the executable form of the contract rather than a description of it.

Stopping, cancelling, and surviving a logout

stop: yes in the request ends the loop from your side, which matters because nobody is sitting at that terminal. cancel: yes kills the step running right now; cancel: <id> kills it only if that id is the one running, so a stale cancel cannot reap a later run. The partial log is always kept on the station, and on the git transport it is delivered with the cancellation; on blob and relay it currently is not, and stays local.

station.sh deliberately does not trap HUP. Its cleanup signals the running step's process group, so trapping HUP would kill an in-flight step every time a connection dropped. To survive a logout properly, see running it as a service.

Getting one onto the far side

Planting a station. The operator's whole job is: clone the repo, run ./start.sh, walk away.