NodeActions (reboot, update, service restarts) become signed node.root
commands for nodes with the COMMANDS flow whose root-owned pin of the
panel key is in place; others keep the signals table.
- RootPin: /etc/xc_vm/cluster/{main_sign.pub,node,root.seq}, trusted only
when root-owned and not group/other writable
- cluster:exec hands node.root to root through config/cluster/root-inbox
- cluster:root (root cron, 58 s watch) verifies against the pin, raises
its own high-water before running, runs RootSignalsCronJob actions and
writes results without following planted links
- cluster:pin-root <panel_fp> pins a code-enrolled node; LbInstallFlow
pins over SSH at install
- cluster_nodes.root_ready (migration 039) from the agent's heartbeat;
the Cluster Nodes page shows it and the panel fingerprint
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DcNdxHieZuUh3EnMUKt7zf
When MAIN's broadcast port changes and the API has no port of its own,
ClusterEndpoint::recordChange() runs before the ports are applied: it
bumps cluster_policy_ver (agents refetch the policy within a heartbeat)
and keeps the old port for 7 days (migration 038). The policy lists it
after the new URLs; MAIN's nginx gets bin/nginx/conf/cluster_legacy.conf
from the set_port handler, one server block per kept port serving
/cluster/v1/ and 404 for everything else (included by glob). cron:cluster
drops expired ports, bumps the policy and re-applies MAIN's ports.
nginx -t passes with and without the file; a run confirmed only
/cluster/v1/ reaches PHP on the old port.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DcNdxHieZuUh3EnMUKt7zf
Servers → Cluster Nodes (permission `servers`) is the admin side of the
cluster CLI, via Domain\Cluster\ClusterAdmin: enrolled nodes with state,
liveness, epoch, token expiry, last heartbeat and agent version; code
enrolments waiting for a SAS (approve / reject); issuing an enrolment
code (shown once); and revocation. The deciding admin is recorded.
Strings in all eight languages.
cron:cluster (enabled by migration 037) deletes expired epochs (erasing
their z), the replay cache and used challenges, unused expired codes and
decided requests after a day. It returns at once on load balancers and
while the API is disabled.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DcNdxHieZuUh3EnMUKt7zf
For an LB MAIN cannot reach over SSH. `cluster:enrol-code <id>` issues a
one-time code (MAIN URL, panel key hash, 128-bit secret; 30 min). MAIN
keeps only K_req/K_res, sealed to the machine under a row MAC.
The node sends enrol_code (sid:<n>, epoch 0; MAC'd with K_req, signed by
the key it asks to enrol, body SEALed to the panel box key). Nothing is
minted until `cluster:enrol-approve <id> <SAS>`: the admin types the SAS
the node shows, which binds its keys; five wrong entries reject. The
node then collects epoch 1 via enrol_code_status (MAC'd with K_res,
panel-signed `pre`) and finishes with enrol_complete.
A wrong code MAC records nothing; other keys under a used code are a
signed ENROL_CONFLICT, the same keys again are accepted. Migration 036
keeps the request's per-epoch key until approval. Both commands are
MAIN-only (stripped from the LB archive).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DcNdxHieZuUh3EnMUKt7zf
Migrations 028–034 (the plan's 026–032; 026 and 027 are taken by the SSH
host key and the fanout switch), each with a down file, mirrored in
database.sql:
- 028: the 19 cluster settings with the plan's defaults. They stay inert
until cluster_api_enabled and cluster:init.
- 029: cluster_nodes (mode, flow bits, gen, node keys, state, sequence
high-waters, row MAC), cluster_node_epochs (the extension's sealed epoch
records; deleting a row erases z) and cluster_meta.
- 030: cluster_commands, FIFO per node with UNIQUE(server_id, seq),
UNIQUE(server_id, dedupe_key) and UNIQUE(cmd_id).
- 031: cluster_enrol_codes (sealed K_req/K_res under a row MAC, looked up
by SHA-256 of the secret) and cluster_enrol_requests (UNIQUE(server_id):
no bulk approve).
- 032: cluster_audit, cluster_nonces (replay cache) and
cluster_reservations (MySQL-mode admission).
- 033: crontab.role (all/main/legacy). The four crons Phase 0 gated to
MAIN become 'main', and a disabled 'cluster' row is added for
cron:cluster (Phase 2).
- 034: cluster_changes (blocklist delta log) and cluster_stream_ver (R2
versions).
Checked on MariaDB 10.11:
- a fresh database.sql and the previous database.sql plus these
migrations produce identical columns, indexes, cron rows and defaults;
- the migrations are idempotent;
- the down files remove everything and up re-applies.
ClusterSchemaTest keeps the two copies in step in CI.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Es8UhyPS8MNjuYTagqfGWi