4 Commits
Author SHA1 Message Date
Sycun e8a5392b37 fix: serialize installation updates and recover interrupted adoption and rollback 2026-10-09 10:58:12 +08:00
Sycun 54d8f1187d fix: preserve rollback binaries during ambiguous recovery and YAML settings 2026-10-09 10:27:17 +08:00
Sycun b645914e1f fix: a rollback keeps a recreated role, and an owed build is finished first
Three ways the installation could still end up with its source, its binary and
the operator's content describing different things.

Rollback copied aside only *tracked* protected files, so a role upstream had
deleted and the operator had recreated at the same path was untracked - and
`git reset --hard` wrote the old commit's copy over it. The stash now also
covers untracked protected paths the target commit writes to, and names them in
keptContent like any other file it saved.

The pending-build marker said only "a compile is owed". An update that found
new commits therefore merged them and built the *new* tree, leaving `.prev`
holding a binary for a commit the state file no longer named - a rollback then
paired old source with the original binary. And a marker left behind after a
successful swap rebuilt and overwrote that same `.prev`. The marker now names
its target commit and the hashes on both sides of the swap, the state records
which commit the live binary came from, an owed build is finished before the
tree moves again, and a retry that finds the swap already done clears the
marker instead of repeating it. Rollback verifies that pairing before touching
anything and refuses (binary_changed) rather than reset the source beside a
binary it cannot account for; an update whose build failed never swapped one,
so its rollback moves the source only and no longer demands a `.prev`.

The version pair is recorded before the config is written, so a crash in
between still leaves a rollback that can put the old number back.
2026-10-08 22:46:17 +08:00
Sycun cb8bc6b824 feat: update this installation's own source in one action
An operator running a fork has no honest way to upgrade: upgrade.sh downloads the
Release tarball of one hard-coded third-party repository and rsyncs it over the
tree, so "upgrade" means "overwrite yourself with somebody else's code", and the
safe alternative is to pull by hand and remember the build command.

internal/update drives the remote the directory already tracks: fetch, fast-forward,
go build into .update-staging, atomic rename of the binary (the previous one kept as
cyberstrike-ai.prev), and a .update-state.json so a rollback has a target that is
not guessed at. Refusals are the point - local source edits and a diverged branch
stop with the file names and commit counts in the error rather than being reset to
match, and operator-owned content (roles/skills/tools/agents/data/config.yaml) is put
aside before the merge and restored over it, with what was kept listed in keptContent
instead of quietly dropped.

Consumers: GET/POST /api/system/update* behind update:read / update:apply (mutations
are process-global, so they need a global-scope session); a console page at
#system-update with an apply button that names why it is blocked, a job polled from
the server and a rollback control; the same three actions as -check-update / -update
/ -update-rollback on the binary; OpenAPI entries for all five endpoints. Permission
gating no longer re-enables a control a page disabled for a reason of its own
(data-state-disabled), which every state-disabled, permission-gated button in the
console was subject to. upgrade.sh becomes a thin shell over the same implementation
and only falls back to the tarball path for installs that are not git work trees.

Docs and the zh/EN/RU dictionaries are updated. The package runs 13 tests against
real temporary git repositories; the HTTP layer, RBAC and the console page each
carry their own contract tests.
2026-10-07 12:07:26 +08:00