Repository navigation
Releases: BySergiMM/holdcall
Release list
v0.1.3
The approval-timeout fix and a hardening pass. After upgrading, run
holdcall daemon restart: a 0.1.2 relay and a 0.1.3 daemon refuse each other.
Security
- The console answers only to whoever was handed its address.
holdcall consolemakes a 256-bit token when it starts and prints it in the
address it asks you to open (http://127.0.0.1:7717/#token=...; the token is
in the fragment, which a browser keeps to itself). Every request but the page
itself must carry it. Until now any process that could open a connection to
the port could read the journal and, from/api/pending, the real arguments
of the calls held for a human. Upgrading: a bookmarked plain address opens a
page that says it has no token and shows nothing; the token lasts until that
console stops, so a console started again prints a new one. - A peer is checked for the user it runs as, not only for the file it runs.
The daemon and the relay compared the other side's executable and nothing
else, so another user running the same binary passed the check. Both now
compare the effective uid as well, on Linux and macOS. Not on Windows, which
has no peer credentials to ask (see below). - Windows no longer releases credentials or connectors. The daemon served
a connection the platform could not verify, which is every connection on
Windows, like a verified one, so a process of the same user able to open the
socket could ask for a connector's secret and receive it. A secret now goes
only to a peer that was verified, and the relay takes no connector or
credential from a daemon it could not confirm. On Windows, which stays
experimental, that means connectors do not work andholdcall connector set
refuses to send a secret; decisions, rules and the journal are as before. - A socket or data directory another user owns is refused. The daemon
bound its socket wherever the path said, so a directory another user owns --
/tmpitself, say -- let that user put or replace the socket. The home, the
data directory and the socket's directory must now belong to the user running
Holdcall, or the daemon does not start and says which directory and which
setting to change. Directories Holdcall creates are0700. One that was
already there, is yours and is open to group or other is not changed
(asocket = ~/holdcall.sockwould otherwise take away your home's access
from everyone else): it is warned about in the daemon's log, and
chmod 700closes it. Linux and macOS; nothing is checked on Windows.
Changed
- Linux: the default socket moves when
XDG_RUNTIME_DIRis not set. It
used to go directly in/tmp, where any local user can pre-create a path.
It goes in/tmp/holdcall-<uid>/now (the temp directory itself is still
used where it is already private to you, as macOS's$TMPDIRis, and
$XDG_RUNTIME_DIRwhen set is as before). A daemon an earlier version
started on the old path is not found by the new one and keeps running
orphaned: runholdcall daemon restartafter upgrading, or stop it by hand.
Fixed
-
Opening the journal waits for a lock instead of failing at once. The
journal switched the database to WAL before armingbusy_timeout, so a lock
another connection held at that moment made the open fail with "database is
locked" instead of waiting the five seconds it is owed. A daemon starting
while another handle was mid-write failed intermittently. -
A held call nobody decides no longer ends the session, and is no longer
reported as a human's rejection. Withapproval_timeoutrunning out, the
relay and the daemon's own timer raced, and either result was wrong. When
the relay's clock won, it hung up on the daemon for good and every later
call in that session was denied with "could not reach a decision". When the
daemon's answer won, the model was told "A human reviewing this call's real
arguments rejected it" about a call nobody had looked at. The relay now
waitsapproval_timeoutplus a five-second grace, the daemon answers a
timeout with a value of its own (timed_out), and the model is told
"nobody decided within the approval timeout". The next call in the session
is decided as usual. The journal is unchanged: the call is still recorded
asrejected, so existing journals verify as before, and the daemon's log
now says, for each one, that nobody decided.
Upgrading: where the daemon and the relay check each other (macOS, Linux)
a daemon and a relay from different builds already refuse each other, and
holdcall daemon restartis the remedy. Where they do not (Windows), a
relay from an earlier version does not knowtimed_out, denies the call
and drops its connection to the daemon, so the rest of that session is
denied. -
A daemon that goes away while it holds a call is no longer reported as an
approval timeout. The relay counted any failed read as the wait running
out, so a daemon killed a second into a one-minute hold was reported as
"nobody decided" at that moment. The client is told "could not reach a
decision", and the relay's stderr says the daemon stopped answering. -
The relay's two-line notices say
holdcall:on both lines. The second
line of five of them still begannim:, the product's name before the
rename. -
A configuration the daemon refuses is reported for what it does. A
socket path past the limit made the relay print "relaying anyway; calls
will not be recorded". It relays nothing unrecorded: the daemon it then
tries to start runs the same check and exits, so no daemon is ever
reachable and everytools/callis denied, while every other message is
still relayed. The notice now says that. -
The console says what a client is told when nobody decides a held call.
The note under "Held for a human" still said the client is told a human
said no.
Changed
- Head-of-line blocking is documented as a known limitation. While a call
is held for a human, every other message in the same session waits behind
it, up toapproval_timeout; other connectors' sessions are not affected.
Not fixed here: it needs decisions to be asynchronous, which is a redesign
of the relay. Seedocs/decisions/0005-human-approval.md.
v0.1.2
Built by the release workflow from a commit that passed the whole suite on
ubuntu-latest, macos-latest and windows-latest, the first to do so. The
binaries differ from 0.1.1 only in the daemon's stoppable run loop, which
tests use; what changed is what is known about them.
Added
- A refusal names what the peer runs. Every "not Holdcall" refusal now
carries the peer's pid, the file it runs and the file this binary is, or
that the path could not be read. Wording only, never a decision. This
shipped unlisted in 0.1.1's binaries.
Changed
- Windows runs the suite and passes (F-029). Every failure on the first
Windows runs had one of a handful of causes, and each was fixed as a
cause: in-process daemons now stop before their temporary directory is
removed, the end-to-end tests build.exefiles, the faked home sets
USERPROFILE, the no-daemon test blocks the socket under a regular file,
and the tests that need an executable identity skip there by design and
are named in the allow-list. The evidence script strips the carriage
returnsjqprints on Windows. The Windows job decides the run again.
v0.1.1
Built and published by .github/workflows/release.yml on GitHub's runners,
the first release that was: the repository is public since 2026-09-26 and
the runners can start. The suite ran on ubuntu-latest before the build; the
same commit passed on macos-latest in CI. Every archive comes from the same
commit; darwin/arm64 is also the build the maintainer runs. Windows archives
are still cross-compiled from a tree whose tests fail there (F-029).
Changed
- Linux is executed, not just compiled. The repository went public on
2026-09-26, GitHub's runners started, and the whole suite, end-to-end
relay tests included, passes on ubuntu-latest and macos-latest. Windows
ran for the first time the same day and fails 52 tests (F-029); its job
no longer decides the run until those are fixed.
Fixed
- A daemon upgraded before its first peer check no longer accepts the
new build as itself (F-026). On macOS the daemon resolved its own
image lazily, on the first checkable connection; rebuilt in place before
that, it fell back to the path comparison for good and served the new
build's relays silently, with no F-001 diagnosis. It resolves its image
at startup now, before listening. Found through a test that flaked for
exactly this reason; the test also keeps each probe alive until the old
daemon has looked at it. - The daemon's tests create their own machine-id (F-027, second part).
Tests that enrol through the journal before the daemon has created the
identifier lost that race on CI; the helpers now create it first. - CI's test job runs in
engine/on every platform. A job-level
defaults.runreplaces the workflow-level one, so the working directory
was lost; the first run on a real runner failed ongo vetat the root,
and the Windows pre-checkout step must not use it. install.shno longer overwrites the binary in place (F-028). On
macOS that left aholdcallthe kernel killed on every exec once a daemon
from the previous build was running. The installer now copies beside the
target and renames over it, and says to runholdcall daemon restart.
v0.1.0
The first tagged version of Holdcall: a local relay that decides every MCP tool call, holds the dangerous ones for a human with their real arguments, and keeps a hash-chained journal you can verify offline. CHANGELOG.md is the status report; docs/milestones.md holds the evidence behind each claim.
Built by hand on macOS. GitHub Actions could not run on this account this week, so these archives were produced from the tagged commit with the release workflow's own flags (-trimpath -ldflags "-s -w -X main.version=v0.1.0 -X main.commit=… -X main.builtAt=…", CGO_ENABLED=0) and layout. Only darwin/arm64 was executed: it is the binary the whole engine suite ran against. darwin/amd64, linux/amd64, linux/arm64 and the windows/amd64 zip are cross-compiled and were never run; nothing in this release changes the platform claims in the docs.
Install. Extract the archive for your platform and put holdcall on your PATH, then holdcall doctor. install.sh verifies archives against SHA256SUMS but cannot fetch from a private repository anonymously.
If you had it installed as nim: the product was renamed on 2026-09-25 and nothing is migrated on its own. Stop the daemon, move ~/Library/Application Support/nim to .../holdcall, rename data/nim.db to data/holdcall.db, run holdcall init --write, and set each connector's credential again with holdcall connector set. The journal format is unchanged, so the chain keeps verifying.