Skip to content

Releases: BySergiMM/holdcall

v0.1.3

Choose a tag to compare

@BySergiMM BySergiMM released this 01 Oct 18:32

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 console makes 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 and holdcall 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 --
    /tmp itself, 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 are 0700. One that was
    already there, is yours and is open to group or other is not changed
    (a socket = ~/holdcall.sock would otherwise take away your home's access
    from everyone else): it is warned about in the daemon's log, and
    chmod 700 closes it. Linux and macOS; nothing is checked on Windows.

Changed

  • Linux: the default socket moves when XDG_RUNTIME_DIR is 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 $TMPDIR is, and
    $XDG_RUNTIME_DIR when 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: run holdcall daemon restart after 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 arming busy_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.
    With approval_timeout running 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
    waits approval_timeout plus 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
    as rejected, 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 restart is the remedy. Where they do not (Windows), a
    relay from an earlier version does not know timed_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 began nim:, 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 every tools/call is 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 to approval_timeout; other connectors' sessions are not affected.
    Not fixed here: it needs decisions to be asynchronous, which is a redesign
    of the relay. See docs/decisions/0005-human-approval.md.

v0.1.2

Choose a tag to compare

@github-actions github-actions released this 28 Sep 20:35

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 .exe files, 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
    returns jq prints on Windows. The Windows job decides the run again.

v0.1.1

Choose a tag to compare

@github-actions github-actions released this 26 Sep 00:11

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.run replaces the workflow-level one, so the working directory
    was lost; the first run on a real runner failed on go vet at the root,
    and the Windows pre-checkout step must not use it.
  • install.sh no longer overwrites the binary in place (F-028). On
    macOS that left a holdcall the 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 run holdcall daemon restart.

v0.1.0

Choose a tag to compare

@BySergiMM BySergiMM released this 25 Sep 22:53

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.