Skip to content

Releases: samirhvbr/tura-notes

1.9.4

Choose a tag to compare

@samirhvbr samirhvbr released this 25 Sep 19:05

The owner asked on 25/09 whether anything is left to produce, and the sweep
measured nothing buildable here. No queue line is open. Of the parked ones,
four wait on the owner (the 1.9.0 signature, the acceptance walks, installing
the publish helper, the macOS feed that needs the MacBook), two on a device
(the Android adapter's budgeted polling and the mobile background queue), and
one on time (the Windows crash leg). There are no open pull requests and no
branch but master, no TODO or FIXME in the code, and no ADR left
PROPOSED or unbuilt. The only PROPOSED document is the Android adapter
contract, which needs a device. CI is green on the last three versions.

Three parked lines had gone stale, and this corrects them. The chat-harvested
duplicate of R7-04 still asked whether to approve ADR-096, which was approved
on 24/09 and built in 1.8.66 and 1.9.0, so it is closed. The signature line
named 1.8.0, which will never be deployed now that the line is 1.9; it names
1.9.0, with the morning's refused deploy as the measurement. The macOS feed
line compared 1.7.21 against a Linux 1.8.47 that was never published; it now
compares it with the 1.9.2 measured from outside. And R8-04 records the second
green night on the Windows crash leg, 2 of 10.


No artifacts on this release. It is a patch; packages are built for minor bumps (X.Y.0) and on request. The newest release that carries them is the one to install — or run the Build workflow by hand with 1.9.4 as the version.

1.9.3

Choose a tag to compare

@samirhvbr samirhvbr released this 25 Sep 15:01

The owner's answer to q_publish_sudo on 25/09: a passwordless rule, limited
to the publish, so the loop can publish the Linux feed at each minor as
authorized. Written as sudoers wildcards it would not have been limited. In
sudoers * matches spaces and ../ as well, so artisan files:add * ingests
any file www-data can read, a site's .env included, and
install -m 644 /tmp/* …/updates/tura-notes/* writes as www-data anywhere www-data
can write. docs/runbook.md recommended the first of those lines, and the
failure message in build-local.sh printed it.

So the rule names one program, server/cotenant/tura-publish, with no
arguments in the rule. The helper checks every argument itself. The staging
directory must have the publisher's mktemp shape, and names must be one plain
path component. The platform must be one of the known feeds, the version three
numbers, and the download-page target one of three, whose label the helper
builds. The destination is a constant in the helper, never an argument. It
reads nothing through a symlink (dd iflag=nofollow), so a link swapped in
after the checks publishes nothing, and artisan gets a private copy rather
than the staged path. It refuses to run as anyone but www-data. The
sudoers file, server/cotenant/sudoers-tura-publish, is one line.

tools/build-linux.sh, build-local.sh and tools/updater-release.py try the
helper first, when sudo -n -l -u www-data /usr/local/sbin/tura-publish check
says it is granted without a password, and fall back to the old
password-asking commands otherwise, so a server without the helper publishes
as it did. The deploy compares an installed helper with the repository's copy
and warns when they differ. It never installs the helper, because the helper
is the whole reach of a passwordless grant, and a copy installed from the
repository would change without review. docs/OWNER-ACTS.md §7 has the
one-time install and the check, the runbook's wildcard advice is replaced, and
.loop/SCOPE.md says the loop publishes when that check answers without a
password.

Tests: server/tests/cotenant.py runs the helper for real against a copy
pointed at a temporary tree. It installs the payload and the feed at 0644 and
leaves no temporary file, and it refuses twelve bad calls, among them
traversal in each argument, an unknown platform and version, a staged file
outside /tmp, and a symlink as payload and as download. artisan receives a
copy, the real helper refuses a non-www-data user, and the sudoers line has no
wildcard. test_updater_release.py checks the exact helper call and that the
fallback remains; test_build_local.py follows the new shape of the ingest.


No artifacts on this release. It is a patch; packages are built for minor bumps (X.Y.0) and on request. The newest release that carries them is the one to install — or run the Build workflow by hand with 1.9.3 as the version.

1.9.2

Choose a tag to compare

@samirhvbr samirhvbr released this 25 Sep 11:48

The same deploy stopped where it should: it targeted 1.9.0, found no
notes-server-1.9.0-x86_64-linux.tar.gz.minisig, refused, and left 1.7.0
running. docs/OWNER-ACTS.md §1 still named 1.8.0 as the next signature. It
never will be: the deploy installs the minor of the line it is on, and the line
is now 1.9. The page now says 1.9.0, that its tarball and checksum are
attached, and that the private key is not on the Linux desktop (checked for
existence only), so the signing runs wherever 1.7.0 was signed.


No artifacts on this release. It is a patch; packages are built for minor bumps (X.Y.0) and on request. The newest release that carries them is the one to install — or run the Build workflow by hand with 1.9.2 as the version.

1.9.1

Choose a tag to compare

@samirhvbr samirhvbr released this 25 Sep 01:20

1.9.0 was the first minor after the owner authorized the loop to publish the
Linux feed (1.8.65), and the publish could not be run. tools/build-linux.sh --publish runs sudo -u www-data php artisan files:add on the server over
ssh, and tools/updater-release.py runs sudo -u www-data mkdir, install
and mv into the updates directory. Measured without changing anything, with
sudo -n -u www-data true as the server's user: "a password is required". The
loop types no password, and sudo is on its list of acts to stop at, so the
authorization does not reach the step that needs it. That should have been
measured before the question was asked, and it was not.

.loop/SCOPE.md records the block under the authorization it limits, and R8-03
is parked on a new question on the owner's board, q_publish_sudo. The options:
the owner runs the publish at each minor with the loop's notice, which changes
nothing on the server; a passwordless sudoers rule limited to the publish's own
commands; or an updates directory the server's user can write without sudo,
which would leave only the download page's row to the owner. The 1.9.0 publish
is on the board as an owner act, with the command to run it from a clean
worktree.


No artifacts on this release. It is a patch; packages are built for minor bumps (X.Y.0) and on request. The newest release that carries them is the one to install — or run the Build workflow by hand with 1.9.1 as the version.

1.9.0

Choose a tag to compare

@samirhvbr samirhvbr released this 25 Sep 01:13

Why a minor. docs/versioning.md moves Y for "an ADR that reverses an earlier one or overrides a fleet convention", when it lands. ADR-096 is such an ADR: it names an exception to security.md §4.10, which this repository inherits as written from the fleet standard, keeping administrative services off public interfaces. Its first half landed in 1.8.66 as a Z. That was a mistake, and a published version is not rewritten, so the Y is taken here, on the commit that completes the ADR. Being a minor has consequences, and they are intended: the Build workflow attaches release binaries to X.Y.0, which is what the server deploy installs, so the device routes reach a server only from here (once the owner signs this release); the Linux feed is published at each minor (R8-03); and the acceptance walks repeat on the next minor (ADR-093).

The app half of ADR-096, which closes R7-04. Once paired, the device panel lists
the workspace's sync devices, each with its credential's label, its state and
its receipts. Every device that is not this one has Revoke, and the
confirmation, in place rather than in a blocking dialog, says what revoking
does: that device stops syncing until a new credential is issued for it on the
server, and nothing is deleted. A credential without devices gets no list and
no error. The server's 403 is read as "not granted", because most credentials
will never have the permission, and a refusal on every open would look like
something broken.

Underneath, Remote::devices returns no list on 403 and on 404 (a server older
than 1.8.66). Remote::revoke_device turns the server's 409 into its own error,
OwnDevice, with its own cause and sentence in both languages, because every
other 409 in this client means a revision conflict and would have said so.
Controller::devices and revoke_device take no lock, as the connection test
takes none: they write nothing on this machine, so a transfer in progress must
not turn them into "busy". Two Tauri commands carry them.

Tests: the client against a local server, checking the list, the exact method
and path of each request, the 403 and 404 read as no list, and the 409 read as
OwnDevice; and the component, checking that nothing is shown without the
permission, that this device has no Revoke, that another needs the
confirmation first, and that a refusal says why. SYNC-0.6, ADR-096, the queue
and the CLAUDE.md/AGENTS.md line say it is built. What remains is the
owner's acceptance on an installed release, as it is for every milestone.

1.8.66

Choose a tag to compare

@samirhvbr samirhvbr released this 25 Sep 01:02

ADR-096 was accepted by the owner on 24/09, as proposed. This is its server
half. A seventh permission, devices, is granted only from the host
(token create … devices) and never implied. It lets a credential do two
things over the public HTTPS API, both in its own workspace:
GET /v1/workspaces/{w}/sync/devices lists the sync devices, each with its
owning credential's label, its receipts, whether that credential is revoked and
whether it is the caller's own; and
POST /v1/workspaces/{w}/sync/devices/{device}/revoke revokes the credential
that owns another device. The caller's own device answers 409 own_device, a
device the workspace does not have answers 404, and a credential without the
permission answers 403. Revoking a revoked credential answers the same row
and rewrites nothing. The revoked device is out on its next request, with no
restart.

Every request reads the credential store under a shared administration lock,
and this is the one route that writes it. A shared holder cannot upgrade
without waiting on itself, so the revocation takes the lock exclusively for its
whole request, as token revoke does on the host. Deciding that needs only the
method and the path shape. It is audited as sync_device_revoke, with the
caller as actor, the client address, and device:<id> as the target.

The permission is a variant of the same enum local agents use. No agent tool
asks for it, so on a local agent it grants nothing. security.md §4.10, which
keeps administrative services off public interfaces, now names this as its one
exception and states what it costs. SERVER-0.5 no longer says revocation is
never a remote route, SYNC-0.6 says revocation can be done from a device while
retirement stays the operator's, and the OpenAPI document, the CLI help, ADR-096
itself (now ACCEPTED) and the lines in CLAUDE.md, AGENTS.md and the queue
that said R7-04 waited on an answer all follow.

Two tests: the full path with two credentials and their devices, including the
409, the 404, the idempotent second revocation, the revoked device's 401 and
the audit lines; and the 403 for a credential without devices, and for
another workspace. The app's screen is what R7-04 still needs.


No artifacts on this release. It is a patch; packages are built for minor bumps (X.Y.0) and on request. The newest release that carries them is the one to install — or run the Build workflow by hand with 1.8.66 as the version.

1.8.65

Choose a tag to compare

@samirhvbr samirhvbr released this 25 Sep 00:55

The owner's answer to q_publish_linux on 24/09: yes, at each minor, from a
clean worktree. .loop/SCOPE.md now lists tools/build-linux.sh --publish among
the acts the loop takes without asking, with the conditions. Only for X.Y.0,
since a patch release stays the owner's to publish. Only from a worktree on
origin/master, never from the working tree, because on 21/09 a 1.8.21 built
from a tree carrying uncommitted work reached the feed. And only followed by
reading both Linux feeds back from outside.

Without this, the in-app updater has nothing new to offer: the feed moved only
when the owner ran the publish, and on 24/09 it was 37 versions behind GitHub
until they did. R8-03 stays open in the queue because it recurs. The next time
it applies is 1.9.0.


No artifacts on this release. It is a patch; packages are built for minor bumps (X.Y.0) and on request. The newest release that carries them is the one to install — or run the Build workflow by hand with 1.8.65 as the version.

1.8.64

Choose a tag to compare

@samirhvbr samirhvbr released this 25 Sep 00:48

The owner's report of 24/09, made plain on the board: an installed 1.6.100
checks, finds the newer version, and fails when asked to install it. It was not
a request for automatic installation, and the answer to that question was to
keep the installation explicit.

Measured on the machine that reported it, which runs the 1.6.100 .deb. The
1.8.58 feed answers; the package downloads (7.1 MB); its signature verifies
with minisign -V against the public key built into 1.6.100; the plugin's own
format check (infer::archive::is_deb) accepts it; its dependencies are
satisfied; and the installed binary carries the Debian bundle marker, so the
plugin would take the .deb path. journalctl has no pkexec from Tura Notes
at all, while two other Tauri applications on the same machine went through the
same pkexec dpkg -i four times since 21/09. So the click never reached the
plugin. It stops at the editor barrier of 1.6.x, which 1.7.0 fixed: the claim was
one instant's check against the application's own polling (500 ms, 3 s, 15 s), a
coin toss the installation nearly always lost, reported as "nothing was
attempted".

A fix cannot reach the copy whose installer is the defect, so leaving 1.6.x
takes one manual install. docs/OWNER-ACTS.md §5 has the two commands, against
the package verified here. docs/updater.md says that versions 1.1.0 to 1.6.x
find updates and cannot install them, and it corrects two sentences that said
otherwise: one written this morning ("installs only when asked"), and one that
blamed "something else" holding the barrier for a message that, on 1.6.x, was
the barrier itself.


No artifacts on this release. It is a patch; packages are built for minor bumps (X.Y.0) and on request. The newest release that carries them is the one to install — or run the Build workflow by hand with 1.8.64 as the version.

1.8.63

Choose a tag to compare

@github-actions github-actions released this 24 Sep 20:51

rm -rf and curl/wget piped into a shell leave deny and now ask for confirmation.
Reading .env/.env.*, git push --force/-f, git reset --hard and git clean -fd
leave deny. Key reads (*.pem, *.key, *.p8, *.p12, *.pfx) stay blocked. The
owner's decision on 24/09/2026, replicated from repodocs 1.17.0 (ADR-028).


No artifacts on this release. It is a patch; packages are built for minor bumps (X.Y.0) and on request. The newest release that carries them is the one to install — or run the Build workflow by hand with 1.8.63 as the version.

1.8.62

Choose a tag to compare

@samirhvbr samirhvbr released this 24 Sep 18:39

The macOS recovery-test intermittent is found, and it was what the instrument
said: a holder outside the process. The holder was a process being spawned by
another test's thread. A spawned child starts with a copy of every open
descriptor and keeps it until it execs, and a flock belongs to the open file,
not to the descriptor. So a lease dropped during that window stays locked, held
by a process that is about to become something else. On macOS every note
deletion spawns one: the trash crate asks the Finder through osascript, and
the recovery tests delete notes on parallel threads. That is why it only ever
happened on macOS. On Linux trash spawns nothing, and on Windows handles are
not inherited.

Measured on Linux before changing anything, with one thread taking an exclusive
lease, dropping it and taking a shared one, while a second thread spawned
/bin/true: 274,000 refusals of a lock nobody held in 1.4 million rounds, and
none in 1.3 million without the spawner.

A refused activity lease is now tried again every 5 ms for up to 250 ms before
it is reported. The window closes in milliseconds, and a real holder is still
refused, a quarter of a second later; the workspace write lock already waits
5 s. forget's probe of every lease file goes through the same function, since
it is the same kind of lock. Two tests: one with a thread spawning processes
takes and drops the lease 400 times without a refusal, and with the window set
to zero it failed in rounds 1 to 3 on each of three runs; the other shows that a
real holder is still refused after the window.

The row in .continue/README.md keeps tracking the intermittent for a week of
macOS CI, and the Linux occurrences from before 1.7.10, which were counted by a
string that four places emitted, stay open on it.


No artifacts on this release. It is a patch; packages are built for minor bumps (X.Y.0) and on request. The newest release that carries them is the one to install — or run the Build workflow by hand with 1.8.62 as the version.