feat(cluster): root commands over the command channel

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
This commit is contained in:
Claude
2026-09-25 14:40:01 +00:00
parent 06a74f2eea
commit 0960418307
30 changed files with 838 additions and 204 deletions
+1
View File
@@ -44,6 +44,7 @@ final class ClusterEnrolCodeTest extends TestCase {
}
$this->rDb->exec('ALTER TABLE `cluster_node_epochs` ADD COLUMN `agent_eph_pub` binary(32) DEFAULT NULL');
$this->rDb->exec('ALTER TABLE `cluster_enrol_requests` ADD COLUMN `agent_eph_pub` binary(32) DEFAULT NULL');
$this->rDb->exec('ALTER TABLE `cluster_nodes` ADD COLUMN `root_ready` tinyint(1) NOT NULL DEFAULT 0');
DatabaseFactory::set($this->rDb);
SettingsManager::set($this->rSettings);
$this->rCrypto = new FakeClusterCrypto();