Skip to content

Releases

Lonca v0.2.2

Choose a tag to compare

@github-actions github-actions released this 09 Nov 20:31
cb8a7b8

lonca doctor reads like flutter doctor

The text form was one flat, colourless line per check. It now groups the
checks under headings ([✓] Mode, [✗] Daemon, [!] Hooks, Tools,
Human, Repository <repo>), each marked with its worst check, says what
to do under each problem, and ends with how many categories have issues.
Colour only on a terminal, never with NO_COLOR or TERM=dumb. --json
is unchanged; in text mode the summary line replaces check(s) failed on
stderr, and the exit code is still 1 (#227).

session start into a full directory starts nothing

lonca session start --workspace DIR with a DIR that is not empty was
refused only after the daemon had started the session: the log had an
allow, and status listed an active session whose token nobody held
until its TTL ran out (#223). The directory is now checked first, and
anything that fails after the session started (making the workspace,
writing its token) ends that session again.

Symbol engine 16: a parse within a budget

A tree-sitter grammar can take far more work and memory than a file is
long: the fuzzer found a short Kotlin file that took gigabytes, so a
proposal holding one could take the daemon down. From engine 16 a parse
stops after a budget of work that grows with the file's length (256 steps
and one per 4 bytes, at most 8192), counted in parse actions and not in
time, so the auditor gets the same answer. A file past it goes by its lines,
as one over 2 MiB does: code people write stays far below it. Entries of
older engines are recomputed as they were written.

Fuzzing

The parsers and the merge are fuzzed (docs/fuzzing.md): stored log
entries, whole logs as the auditor reads them, daemon requests, hook
events, the symbol engine, the three-way merge, and repository
configuration and policy. Every saved input runs under cargo test. A git
id in a request or a log entry is now refused unless it is 40 lowercase
hex digits, as Lonca writes them. A C or C++ file with a broken scoped name
such as a:::b no longer makes the symbol engine panic: the request that
read it got no answer, and the connection was dropped. Such a name now has
one scope per ::, read from the left.

lonca policy check, and a guide to writing policies

lonca policy check --policy policy.cedar (or --config, or both) tries
a policy before lonca policy set: it decides every entry of the log again
with it and lists the decisions that would change, writing nothing. It also
warns when the new policy would deny an enrolled human a way back, such as
policy.update, unfreeze or approve. The new
Writing policies page lists what a Cedar rule sees
(principals, actions, every context key), what @escalate and
@approvals do, why an evaluation error denies, and examples.

A lost Mac comes back with the recovery key

When the Mac is lost, the daemon's key and your Secure Enclave key go with
it. A repository restored onto a new daemon then accepted only
daemon.recover, signed with an enrolled human's key, so the recovery key
alone could not bring it back. Now lonca human recover does it: on a
restored repository it sends a daemon.recover signed with the recovery
key, which moves the repository onto the new daemon and installs your new
key in one entry (log format 27). Every session ends; the recovery key is
used up. See Recovery.

Short change ids work

lonca pending and lonca log show the first 12 letters of a change id,
but lonca diff, lonca change and lonca advise refused them
("invalid change id"). They now take any 4 or more letters that name one
change; when several changes start with them the answer lists those. The
daemon has a new read method for it, change.resolve.

undo and revert read the whole log

The daemon answers log a page at a time (1000 entries), and lonca undo
and lonca revert read only the first page. On a longer log revert
planned nothing after entry 999 and said "nothing to revert", and
undo of a policy.update past entry 1000 put back the last policy of the
first page instead of the one before it. Both now read every page.

More of the guide

New pages: Concepts,
Reviewing agents' work,
Troubleshooting and
Recovery.

Live runs for this release

Claude Code 2.1 (haiku), as docs/release-checklist.md asks: a session
leased, proposed and integrated after approval, and lonca diff listed no
generated file; two sessions at once on one file, on different symbols, both
integrated; one that stopped without proposing was sent back by the stop
hook, proposed, and left no unproposed work. lonca-audit verify passed on
the log. Not run: Codex 0.167 and OpenCode 2.2. Their free models on
Ollama's cloud are retired and the local one was not used. No harness
profile has changed since 0.2.1; Codex last ran live before 0.2.0 (Codex
0.165), with Claude Code on one file, and both integrated.

Install

curl -fsSL https://selcukgural.github.io/lonca/install.sh | sh
brew tap selcukgural/lonca && brew install lonca
cargo install --locked lonca

Check an archive: gh attestation verify lonca-<target>.tar.gz --repo selcukgural/lonca.

Lonca v0.2.1

Choose a tag to compare

@github-actions github-actions released this 09 Nov 07:23
d802fd6

Its tests ran on Ubuntu, macOS and in the bubblewrap sandbox; as for
0.2.0, the mutation run did not gate it (docs/decisions.md, "Mutation
testing"), and its survivors are fixed in a later 0.2.x.

Auditing a restore on a hardened install

lonca restore ended with Audit it: lonca-audit verify <home>/repos/demo.git, a placeholder, and on a hardened install a store
the developer can not read. Now it prints the store's real path (the
daemon's repo.restore result has a new path), or on a hardened daemon
how to verify the source it was restored from. lonca-audit names
"permission denied" for a path it can not read instead of "does not appear
to be a git repository", and points at verifying a backup bundle. The quick
start says which store path is the cooperative one.

A closed pipe no longer panics the CLI

lonca status | head, lonca log | grep -q ... and the like ended in a
panic (failed printing to stdout: Broken pipe). Now the command finishes,
drops the output no one reads, and exits as it would have (0 on success).
Another write error on stdout (a full disk) is reported and exits 3. The
hook blocks the call (exit 2) when the harness can not read its answer,
unless it runs with --fail-open. See docs/decisions.md, "A closed stdout
ends a command quietly (#207)".

A newer proposal replaces the pending one

A workspace that proposed again while its first proposal waited for a
human had two pending changes, the second holding the first's edits:
approving both landed the work twice, the second time as an empty commit.
Now the newer proposal withdraws the older in the same entry and names it
in its reasons (replaces:<change>). The older reads withdrawn, leaves
status.pending and can not be approved; its pinned leases pass to the
newer. A queued change is left to land. Log format 26; older logs replay as
they were. See docs/decisions.md, "A newer proposal replaces the pending
one (#107)".

Hardened mode, installed for real on Linux

The Linux installer ran for real with sudo (Debian 12, systemd), and what
broke is fixed. A hardened install has an import directory
(/Library/Lonca/import, /var/lib/lonca-import) where you leave a Git
bundle for lonca init --import or lonca restore: before, a hardened
daemon could read neither a project of yours nor a cooperative backup. The
daemon copies the bundle before git reads it (lonca daemon --import-dir).
Running a newer archive's installer is the upgrade: it stops the daemon,
replaces the binaries and starts it again (on Linux the old daemon kept
running before). Linux has uninstall-hardened.sh too. ping says
hardened. A hardened daemon no longer starts with --local-root. macOS has not been installed for real yet. See
docs/decisions.md, "Hardened mode in the field (#10)".

status names the policy's checks

status has policy_checks: the names of the named checks the policy in
force defines (verification.checks), sorted, null when the policy can
not be read. A runner can refuse a check name nobody defined when it is
written, not at the human's yes. Names only; a check's command never
reaches a client. The protocol number stays 1: a field was added.

Crates and Homebrew

Every published crate has a README on crates.io; lonca-client and
lonca-net show it on docs.rs as well, with an example that is compiled
as a test. Homebrew is shown as brew tap selcukgural/lonca, then
brew install lonca, so the short name works for brew upgrade lonca
too. The repository's README is a short front page; what it held is on
the site (backups in the quick start, limitations on "What Lonca is").

Install

curl -fsSL https://selcukgural.github.io/lonca/install.sh | sh
brew tap selcukgural/lonca && brew install lonca
cargo install --locked lonca

Check an archive: gh attestation verify lonca-<target>.tar.gz --repo selcukgural/lonca.

Lonca v0.2.0

Choose a tag to compare

@github-actions github-actions released this 08 Nov 19:48
5de43b8

The first release. Its tests ran on Ubuntu, macOS and in the bubblewrap
sandbox; the mutation run did not gate it (docs/decisions.md, "Mutation
testing"), and what it finds afterwards is fixed in 0.2.x.

Releases: prebuilt binaries, crates.io and Homebrew

The first release with prebuilt binaries: lonca and lonca-audit for macOS
(arm64, x86_64) and Linux (x86_64, arm64, static), with lonca-se on macOS,
checksums, a CycloneDX SBOM and a build provenance attestation per archive
(gh attestation verify lonca-<target>.tar.gz --repo selcukgural/lonca).
Install with curl -fsSL https://selcukgural.github.io/lonca/install.sh | sh,
brew install selcukgural/lonca/lonca or cargo install --locked lonca;
every crate of the workspace is on crates.io at the same version. The CLI's
package is now lonca (was lonca-cli), and the minimum Rust is 1.90. Each
archive carries the hardened install scripts. See docs/decisions.md,
"Releases and install paths (#7, #8)".

A client crate for harnesses

lonca-client is what a harness or runner needs to talk to the daemon, and nothing else: the
protocol's request and response types and PROTOCOL, the socket client with the daemon checks
(socket owner, peer uid, the hardened config), framing, an optional timeout (past it the call
fails timed_out and the connection is dropped, since the request may have run), typed errors
that tell rate_limited, busy and closed apart (is_transient), and the human session file
reader, which refuses a link, another user's file or one others can read. It depends on
lonca-core without its Git store, which is now the default feature store. A protocol number
change is a major version of the crate (0.x: minor). lonca no longer uses a session file
others can read, and a login writes an old file back as 0600. See docs/decisions.md, "A
client crate for harnesses (#192)".

Checks a change owes, named per change

[verification] takes named commands, checks = { api = "cargo test --offline -p api" },
which run only for a change that owes them: a human names them when approving
(lonca approve SEQ --also api,e2e, signed in approve, log format 25), or
checks_by_path = { "api/**" = ["api"] } does for a path. They run in the queue's sandbox
after build and test, a failure rejects with verification_failed:<name>, and a check owed
is owed even with required = false. A new version of the change keeps them.
status.changes[], status.pending[] and change.get add checks (asked) and runs
(what the queue will run). See docs/decisions.md, "Checks a change owes, chosen per
change (#191)".

A session whose hooks do not run is marked and tainted

lonca mcp now tells the daemon which tool it runs (mcp.call). On Claude Code, whose
PreToolUse fires for MCP tools too, the hook's event must come first; a call with none
means the harness is not running the session's hooks, and the session is marked
unhooked: logged once, a security event, tainted, unobserved from then on. What it
proposes waits for a person (unhooked_session) and a critical path is refused
(unhooked_critical_path). status.sessions[] adds bound_ms, last_hook_ms and
unhooked, so a runner can stop a session the daemon has not seen. Log format 24. Other
harnesses are checked once a run shows the same (events.before_tool_mcp in a profile).
See docs/decisions.md, "A session whose hooks do not run (#190)".

A clean stop is on the record

status.sessions[].unproposed was null both for a session that stopped or ended with nothing
left and for one that never reported, so a runner could not tell a workspace safe to remove
without a live token. A clean report now reads {"paths": [], "total": 0, "rounds": 0, …}, and
null means nobody said since the session began or last proposed. lonca status lists only
sessions that left work, as before. See docs/decisions.md, "Stopping with unproposed work"
(#189).

Merge: a change already in trunk, and deletions at the top

Symbol engine 15. A change whose every edit trunk already has merges into trunk unchanged; it
used to be a conflict on the file when trunk had also added something at the same place. A
function or method deleted at the top of a file or of a class no longer leaves a blank line
there, which a format check in verification would refuse. Found by the new merge property tests.
Older entries are recomputed with the engine they name. See docs/decisions.md, "Symbol engine
versions", Engine 15.

A delegated lease comes back when the child expires

A lease set handed to a child session with cap.attenuate went back to the parent only when the
child ended. Now it goes back as well when the child's time runs out or it misses its heartbeat,
with the time the set had, if the parent is still there; until now it ended, and another session
could take it first. Log format 23. See docs/decisions.md, "A delegated lease comes back (#40)".

Insert leases reserve names; a new file needs its path

An insert lease item can list the names of the members it adds ("names", lonca lease acquire --kind insert --name throttle). Two inserts into one container that reserve one name
conflict, an addition outside the reserved names is denied with lease_missing, and adding a
name another session reserved is denied with name_reserved, whatever lease covers it. An insert
without names otherwise works as before; lease_needed now offers inserts with the names the
change adds. A change that adds a file now needs an edit lease on its path#<file>, so two
agents can not create one path; until now a new file needed no lease. A path trunk created
after the change's base is created_since_base: sync first. The agent rules say all of this.
Additions at one place are merged in queue order, the last integrated nearest to its neighbour.
Log format 22. See docs/decisions.md, "Insert leases (#45)".

Shared leases

A new lease kind, shared: several agents can hold one symbol at once, where a signed
policy has [leases] shared = true, which needs verification.required = true and a build
or test command. edit and rename stay exclusive and conflict with it. It covers a body,
not a signature. When two holders change one function, the merge (symbol engine 14) joins
their lines; lines both changed are conflicted, naming the function. Log format 21. See
docs/guide/verification.md, "Shared leases".

A quota on verification

[quota] session_seconds and day_seconds cap the time verification may take for a session
(with the sessions made from it or continuing it) and for the repository in a UTC day. Past
either, a proposal is denied with verification_quota:session or :day, and what the session
queued before is rejected without a run. Both are 0, off, by default. An agent's
change.withdraw of a change whose checks are running is refused with verifying. duration_ms in the
evidence now covers every attempt of a retried check. status shows verification_ms per
session and verification_day_ms. See docs/guide/verification.md, "Quota".

Nothing of a verification outlives it

A build or test command now runs as a session whose process group is killed when it ends,
however it ends, so in a sandbox a test can no longer leave a server running, and a background
process that keeps the output open no longer holds the queue. A check out of time is an error
with evidence and the next change goes on. The log keeps the last 64 KiB of stdout and stderr as
they were written. The program a command runs directly can not make a process group of its own.

Each process of a command now has limits, in existing repositories too, since a lonca.toml
without the keys takes the defaults
: limit_processes 16384 (Linux), limit_file_bytes 8 GiB,
limit_open_files 4096. limit_cpu_seconds and limit_address_bytes (Linux) are off unless set.
path sets the commands' PATH. See docs/guide/verification.md, "Limits".

Limits on the socket

One client can no longer tie the daemon up. Connections are capped (the longest-quiet one makes
room), a quiet connection closes after 15 minutes, and a request line has 60 seconds to arrive. A
request line is at most 64 KiB, or 96 MiB for the methods that carry files, and a long line must
name method and token before params. A session and those made from it get [sessions] ops_per_minute, 600 by default; past it requests are rate_limited and the log does not grow.
queue.run now only wakes the worker, log answers at most 1 000 entries, and ping counts
what was refused. See docs/protocol.md.

A level reads the proxy's port and every hook

A harness table can now say is = "proxy_port": the value must be the port the daemon's
own proxy listens on, and with no proxy running the check does not hold. The built-in
Claude Code table uses it for httpProxyPort, so the managed settings and --proxy must
agree; the hardened installers already write the same port to both. Every Lonca hook in
every list must now have its program in root-owned directories. ping returns proxy.

A session names its harness (log format 20)

session start --harness <name> now puts the name in what you sign, and an observation
level is read only for that harness. A session started without it is unobserved on any
machine, so a hardened setup must add the flag; --no-setup names the harness and writes
nothing in the workspace. Once set, a level can not rise for the rest of the session.
status lists each session's harness.

Batches in the merge queue (log format 17)

[queue] batch_size in a signed policy, default 1, verifies the changes at the head of the queue
as one and bisects a failure: change.integrate_batch integrates them with one entry, and trunk
moves straight to the verified tip. A tree that passed is not verified again while the daemon runs.

Seven more languages (symbol engine 10)

Java, Kotlin, C, C...

Read more