Restarting a program that runs other programs (a terminal multiplexer, an
agent's supervisor, a server with clients connected) takes everything under it
down too, because its children lose their terminals and its sockets close.
With engram's enboot, the program replaces its own image with
execve instead, so its pid and its children stay, and it takes its
terminals and sockets back from a small process that held them across the
gap.
engram also ships the builds to update to: release channels that CI
publishes to a bucket, and that a program follows and checks against a key it
pins, so a tampered build is refused and a withdrawn one is known to be
withdrawn.
Updating in place
plexos had exactly this problem. Its daemon holds every pane's terminal, so
each update cost every pane its live agent, and bringing them back was never
quite the same as keeping them. With enboot, an update looks like this in the
process table:
PID COMMAND
4120 plx daemon the resident
4121 └ plx daemon a7072af → 3c91e0b
4130 ├ zsh pty 1
4188 ├ claude pty 2
4203 └ nvim pty 3
Only the build changes, by execve. The resident holds the
listener and the three ptys across the gap, so the pids, the children and the
terminals are the ones from before the update. Here's how it goes:
enboot.Wrap makes the first process the resident (the
enboot), which starts a second copy of the same executable as the
program (the tenant).
- Each time the program opens a pty master or a listening socket, it hands
the resident a duplicate, so there's nothing to collect at update time.
- To update, it writes the new build beside the old one, stops reading, and
calls
execve on it, which keeps its pid and its children.
- The new image asks for its descriptors back, and the resident sends
duplicates and keeps its own.
- Once it has everything it needs, it says
ready, and its path
becomes the build to roll back to.
If the new image dies before it says ready, the resident starts
the last good build again, in milliseconds, with the children alive and every
descriptor still held. In Go that's four calls:
import "github.com/neuroplastio/engram/enboot"
func main() {
b := enboot.Wrap() // the first process becomes enboot and never returns
b.Hello()
b.Put("pty-3", fd) // when a descriptor is created, not at update time
b.Ready(self) // "I am up": this path is now the rollback target
// … later, to update: write the new binary beside the old one, then
b.Exec(newPath, os.Args)
}
enboot is its own module with no dependencies, so embedding it costs a
program nothing else:
go get github.com/neuroplastio/engram/enboot
What it costs. It was measured on Linux and macOS before it
was written down. Holding a duplicate descriptor costs nothing measurable,
because the bytes never pass through the resident (proxying them instead would
cost a fifth of the throughput). Nothing is lost across the gap when the old
image stops reading first, and a client that connects during the gap waits in
the listen backlog until the new image answers it.
The resident
A program can only update itself freely if the thing underneath it never
needs updating, so the resident is kept too small to change. It has no
download, no verification and no version policy (it's handed a path, it
doesn't choose one), and it never reads or writes a descriptor it holds. It
goes on running the code it started with, however many times the program
replaces itself, and since Wrap makes it from the same executable,
there's no second binary to ship or keep in step.
The protocol between the two is eight verbs over one socket pair:
hello <n>
- The first request of every image: agrees on a version, and tells the
resident a new image is running that hasn't said
ready.
put <name>
- Hold a duplicate of this descriptor under name.
get <name>
- Send back a duplicate, and keep the resident's own.
list
- Every name held.
drop <name>
- Close the resident's copy.
ready <path>
- This image is up and has everything it needs: path is the
rollback target.
next <path>
- When I exit, run path in my place rather than exiting with me.
reaped
- What became of the orphans the resident took in (on Linux, where it's a
sub-reaper).
PROTOCOL.md
is the whole protocol in a page, and it ends with the complete list of reasons
the resident could ever need to change. A build that needs a newer protocol
says so in its manifest (min_enboot), and only that update restarts
the whole program. A program that isn't written in Go runs under
cmd/enboot (enboot <program> [args…]) and speaks
the tenant's half from that page.
Programs a package manager installs
A package manager owns the files it installs, so a program that replaces its
own binary under /usr/bin breaks its package, and one that waits
for the package waits for every packager.
enlaunch
splits the two. The package installs a thin launcher under the program's name,
with the channel and its key compiled in, and the program lives in a home of
its own, where it updates itself:
/usr/bin/acme the launcher, which the package owns
~/.local/acme/
builds/<commit>/ one build: its artifacts by name
bin a symlink to builds/<commit>: the build to run
When bin is there, the launcher hands over at once with
execve (same pid, arguments and terminal) and checks nothing. When
it isn't, the launcher first fetches the channel's newest build, checks it
against the pinned key and links it. From then on the program's own update
command picks a build and installs it with enlaunch.Install, and
the home keeps that build and the one before it. A package can carry a build as
well, so the first launch needs no network.
Release channels
A program that updates itself needs somewhere to get builds, and a way to
tell a real build from a tampered one, and a withdrawn build from an old one.
An engram channel does that with a few files in an S3-compatible bucket behind
a CDN, so there's no release server to run:
<base>/<project>/<channel>/
head the newest live build — tiny, polled
journal every event since the channel began, oldest first
builds/<commit>/
manifest what one build consists of — immutable, signed
manifest.sig
<artifact files>
latest/ a copy of builds/<commit>/ for the newest live build
head and journal are rewritten on each publish and
only point. Each build's manifest never changes, and it's the only file that's
signed, with an Ed25519 key the program pins (as an OpenSSH signature, so
ssh-keygen checks a release by hand). The journal is every event
since the channel began, in lines you read with grep:
engram version=1 kind=journal
channel project=acme name=dev
publish seq=1 commit=1f3e9aa0… version=26.09.20-dev.1f3e9aa time=2026-09-20T18:02:11Z min_enboot=1
scrap seq=2 commit=1f3e9aa0… time=2026-09-21T08:50:12Z reason=broken
publish seq=3 commit=a7072af3… version=26.09.21-dev.a7072af time=2026-09-21T09:10:55Z min_enboot=1
It's a journal rather than a list of live builds, because a list can't tell
a host whether its build was withdrawn or is merely old, and the journal says
which, when, and why.
A project usually has two channels: dev gets a build for every
commit of main, and stable gets the commits the maintainer
promotes. engram releases itself this way. Every push to main is a build on
pkg.neuroplast.io/engram/dev, published by the binary built from
that commit, and a tag named for its day (26.10.15) publishes that
commit to stable. A private channel, for builds that aren't out
yet, is served only to whoever holds a token, and the program checks the
signature exactly as before
(what
the server must get right).
Publishing
A release workflow publishes with one command, to any S3-compatible bucket,
signed by an Ed25519 key in AWS KMS, which can't be exported, so there's no
secret to store:
engram publish --bucket my-releases --kms alias/release-signing \
--project acme --channel dev --retention 720h \
--commit "$(git rev-parse HEAD)" --version "$VERSION" \
--time "$(git show -s --format=%cI HEAD)" \
acme/linux/amd64=dist/acme_linux_amd64.tar.gz \
acme/darwin/arm64=dist/acme_darwin_arm64.tar.gz
engram verify ... --local dist # right after publish: no download
engram scrap --bucket my-releases --project acme --channel dev --commit <40 hex> --reason broken
engram pubkey --kms alias/release-signing --identity release@example.org # an allowed_signers line
- Safe to retry. Publishing a commit that's already in the
journal succeeds without doing anything.
- Safe to race. The journal is written with
If-Match, so a second publisher gets a 412 and starts
again instead of overwriting the first.
- Nothing points at half a build. The artifacts and the
signed manifest go up first, then the journal, then
head.
- Checked where it landed.
verify --local hashes
the files the job just built against the signed manifest, then reads the first
and last 64 KiB of each from the server, about 128 KiB an artifact instead of
all of it.
- Not only AWS.
--endpoint names another
S3-compatible service (R2, MinIO, …), and --key-file signs with an
OpenSSH key for anyone without a KMS.
A build stays live until it's scrapped or expires:
publish → live
- The newest live build never expires, and a commit is published at most
once.
scrap → scrapped
- Withdrawn, with a reason, and its files deleted, so hosts running it should
update. With
--cloudfront, the CDN's copy is purged too.
expire → expired
- Superseded for longer than the retention, and its files deleted. Nothing is
wrong with it.
Following a channel
The head says where to look, and the manifest says whether to believe it:
GET head: the newest live commit (unsigned).
GET builds/<commit>/manifest and manifest.sig.
- Check that a pinned key signed it, that the project, channel and commit are
the ones asked for, and that its
seq is higher than the last one
accepted. If any check fails, nothing is installed.
GET the artifact for this os/arch, and check its size and
sha256 against the manifest.
- Install, and remember the
seq.
Builds are addressed by commit, so “a build of my commit” skips the first
step, and a 404 on its manifest means it isn't live on this channel. In Go:
c := &engram.Client{Base: "https://pkg.example.org", Project: "acme", Channel: "dev", Keys: pinned}
m, err := c.Latest(ctx, accepted) // nil, nil: nothing newer
a, _ := m.Find("acme", runtime.GOOS, runtime.GOARCH)
data, err := c.Download(ctx, m, a) // size and sha256 checked
m, err = c.Manifest(ctx, myCommit) // "a build of my commit"; engram.ErrNotLive if there is none
On a private channel the client also sends a token (Token, and
ENGRAM_TOKEN for the command and the installer, never a flag,
which would show it in ps).
Checking by hand
You don't need engram to check engram.
install.sh
installs a binary from a channel with curl, ssh-keygen,
sha256sum and tar, from stable unless
ENGRAM_CHANNEL=dev, and the caller pins the key rather than
fetching it. Only a stable release replaces the installer, so a push to main
can't change what other projects release with.
- name: Install engram
env:
ENGRAM_SIGNERS: release@neuroplast.io namespaces="engram" ssh-ed25519 AAAA…
run: curl -fsS https://pkg.neuroplast.io/engram/install.sh | sh -s -- engram ./bin
Or with nothing but OpenSSH:
curl -sO https://pkg.example.org/acme/dev/builds/<commit>/manifest
curl -sO https://pkg.example.org/acme/dev/builds/<commit>/manifest.sig
ssh-keygen -Y verify -f allowed_signers -I release@example.org -n engram \
-s manifest.sig < manifest
sha256sum acme_linux_amd64.tar.gz # compare with the manifest's artifact line
latest/ holds a copy of the newest live build's files, its
manifest and signature included, for a plain curl -O (a program
that pins the key should still go through head).
Why it looks like this
- No JSON, no quotes. The grammar is a split on spaces and a
split on
=, because one of its readers is meant to be a binary
that is almost never updated.
- Builds are addressed by commit. A replaced release is a
new path, so nothing is ever invalidated in a cache.
- Only the manifest is signed, once, when it is built. The
files that move just point, and what they point at says whether to believe
them.
- OpenSSH signatures. The verifier is a few length-prefixed
fields around one Ed25519 check, and the tool to check a release by hand is
already on every Mac and Linux machine.
The repository
SPEC.md is
the contract for channels, and the repository holds four things built on
it:
- the library —
github.com/neuroplastio/engram
- what a client imports to follow a channel and believe only what a pinned key signed.
- the publisher —
cmd/engram
- what a release workflow calls: upload to any S3-compatible bucket, sign, record.
- enboot —
github.com/neuroplastio/engram/enboot
- what a program embeds to replace itself without losing what it is running. Its own module.
- enlaunch —
github.com/neuroplastio/engram/enlaunch
- what a program's thin launcher is built from: a binary a package manager can
own, which fetches the program from its channel and hands over to it. Its own module.
The root and enboot have no dependencies (their
go.mod files require nothing), and enlaunch requires only the root.
S3 and KMS are a handful of HTTPS requests signed by
sigv4/,
not an SDK.