The operator's verbs, run on the machine the node runs on — the node's own human door, beside the client's approval.
A worker reaches scripts through MCP. A person reaches them through here, and the two doors exist for different reasons: pushing a file from a repository, setting a secret whose value must never travel through a model's context, resetting a throttle's breaker, which no session may reset, and looking at what a node holds when no client is connected are all things the wire is the wrong shape for.
How it arrives
There is no escript and no separate binary. rel/overlays/bin/ymer-node is a
shell script the release carries, and every verb is one bin/ymer_node rpc
into the running node — so the CLI talks to the node that is already serving
rather than booting a second VM against the same database. Two VMs on one
SQLite file is the thing this arrangement exists to avoid.
The overlay is named ymer-node with a hyphen, and that is not cosmetic:
overlays are copied over the release last, so an overlay named
ymer_node would silently replace the release's own launcher.
The argv seam
rpc takes an Elixir expression as a string, so the shell's arguments have to
survive being pasted into one. They are packed by the overlay — NUL-joined,
then base64 — and main/1 unpacks them. Nothing an operator types can then
close a quote or inject an expression, which a naive interpolation would
allow for any argument carrying a double quote or an interpolation marker.
Where a pushed file is read
scripts push with no argument reads the file from stdin — the rpc'd
process inherits the caller's group leader, so docker exec -i <container> ymer-node scripts push < file crosses the container boundary with no path
inside it. scripts push <file> reads a path on the node's own filesystem
instead, which is where the shipped example lives; a host path given there is
refused with status 2, because the node cannot see it.
Nothing on stdin — docker exec without -i — is a usage error, status 2,
for push and for secrets set alike. What arrives then is an empty string,
which would otherwise be compiled as an empty script and refused with a
diagnostic about code that never came, or stored as an empty secret that a
run then presents to the remote as a credential.
Talking back
Everything goes to stdout. A process started by rpc has the calling
shell as its group leader, so IO.puts/1 reaches the operator's terminal —
but IO.puts(:stderr, …) does not: stderr belongs to the node's process,
wherever that was started, so a message written there is lost. That is why
refusals are printed rather than logged, and why the exit code carries the
verdict.
Exit status is exit({:shutdown, status}), which is the one channel rpc
propagates: 0 for a verb that did what it said, 1 for a refusal the node
made, 2 for a usage error the operator made.
One verb prints bytes rather than a line: scripts export <name> writes the
code the node holds and nothing else — not even the newline every other
answer ends with — so > file holds exactly those bytes and a scripts push
of that file on another node lands the same hash. Its refusal is a line like
any other. The release pins the logger level at :info (config/prod.exs)
so no query log rides that stdout: unpinned, a release logs at :debug, and
Ecto's query lines then print into the operator's terminal beside the answer
— a push's INSERT carrying the whole script.
Testing shape
main/1 is the only function that touches the world. run/2 takes an argv
list and a function that supplies stdin, and answers {output, status} — so
every verb is exercised without a release, a shell or a terminal, which is
what the whole rest of this module's tests do.
Summary
Functions
The entry point the overlay calls: unpacks argv, runs the verb, prints, exits.
Runs one verb, answering {output, status}.
Unpacks the overlay's argv encoding — base64 of the NUL-joined arguments.
Functions
The entry point the overlay calls: unpacks argv, runs the verb, prints, exits.
Takes the base64 of the NUL-joined arguments. Exits rather than returning, because the exit status is how a shell learns what happened.
Runs one verb, answering {output, status}.
stdin is a function so a test can supply a value without a terminal; the
default reads the operator's own stdin, which is how secrets set takes a
value that must never appear in a process list.
Unpacks the overlay's argv encoding — base64 of the NUL-joined arguments.
Examples
iex> YmerNode.Scripts.CLI.unpack(Base.encode64("scripts\0list\0"))
["scripts", "list"]
iex> YmerNode.Scripts.CLI.unpack("")
[]