Skip to content

feat(slm): node-major post-sync plan with a durable per-action record, sudo -n restarts and a node sync endpoint (#17455) - #18423

Open
mrveiss wants to merge 6 commits into
mainfrom
issue-17455-post-sync-record
Open

mrveiss wants to merge 6 commits into
mainfrom
issue-17455-post-sync-record

Conversation

@mrveiss

@mrveiss mrveiss commented Oct 12, 2026

Copy link
Copy Markdown
Owner

Thinking Path

The owner decided #17455 on 2026-10-10 (all four yes): node-major post-sync order across roles, with slm-backend last and chromadb before ai-stack, and the rule living once in _post_sync_plan; ollama restarted once per node; the orchestrator restart using sudo -n with failures recorded; and a new post_sync_action_runs table. The survey showed that nothing synced several roles on one node in one run. SyncOrchestrator.sync_node_role handled one (node, role) pair, its only caller was POST /roles/{role}/sync, and update-all's fleet stage uses Ansible, not the orchestrator. So the coordinator chose a node-level entry point (option A), which keeps the owner's ordering from being inert in the automated path. It builds on #18186's single SSH restart executor (merged as #18352, together with the #18359 command-injection fix) and is rebased onto that main.

What Changed

  • One plan (api/_post_sync_plan.py). ordered_post_sync_plan keeps install → schema → build → restart and adds, by unit name (_RESTART_PRECEDENCE, _RESTART_LAST_UNITS, both citing feat(sync): post-sync actions need a durable record and a node-level plan in the orchestrator #17455):

    • autobot-chromadb restarts before autobot-ai-stack;
    • autobot-slm-backend goes last, even when mixed into another restart group or named by a non-restart action;
    • ollama, shared by the cpu and gpu LLM roles, restarts once through the existing unit dedupe.
    • The unit names come from code_sync._COMPONENT_SERVICES and _SELF_SERVICE_NAME.
  • POST /api/nodes/{node_id}/sync (new, api/node_post_sync.py).

    • It syncs every ACTIVE role's files, then runs ONE node plan through services/post_sync_plan_runner.execute_plan.
    • A failed install, schema or build step aborts the rest. No restart runs and no role is marked synced, as sync_node_role did on a post-sync failure.
    • A role whose file sync fails is left out of the plan, and the node reports success=false.
  • POST /api/code-sync/roles/{role_name}/sync now goes through the same plan path. It uses one run_id per call, shared across nodes, and its response keeps every field and adds run_id and actions.

    • The old per-role execution path is removed: sync_node_role, _run_post_sync_command, _restart_systemd_service and _sync_nodes_for_role. sync_role_files and record_role_synced remain.
  • GET /api/code-sync/post-sync-runs/{run_id} (new) returns the run's rows in order, or 404.

  • Self-restart safety. On the SLM's own node (code_sync._get_local_slm_node_id):

    • role versions are recorded and an "issued" row (success = null) is committed first;
    • the response is returned, carrying run_id and every row;
    • only then is the restart sent, via fire_and_forget after service_restart.wait_for_response_flush(), the same flush-then-restart mechanism the Ansible self-update uses (Bug: 3 SLM frontend/backend code bugs from console log analysis #816);
    • role sync processes the SLM host last;
    • if the deferred restart returns (refused or timed out), a follow-up row records the real outcome.
  • Decision 3 (services/post_sync_runner.py). The restart runs through tech-debt(slm): restarting units over SSH is implemented three times with different sudo, timeout and failure handling #18186's run_systemctl_over_ssh with use_noninteractive_sudo=True and a BatchMode=yes ssh base (build_service_ssh_base_cmd). The returncode is checked. A non-zero exit, a timeout or a spawn error becomes a recorded failure instead of logging "Restarted". Timeouts are env-backed (AUTOBOT_SYNC_RESTART_TIMEOUT_S, default 60, registered in env_registry_slm.py and in ENV_VARS.md).

  • Decision 4. models/post_sync_action_run.py adds PostSyncActionRun (post_sync_action_runs): run_id, node_id, role_name, category, label, command, units, started_at, finished_at, exit_code, success (nullable, for "issued"), output_tail and error.

    • command, output_tail and error go through autobot_shared.secret_redaction.redact_text (REDACTION_BOUNDARY.md:125), and the tail is capped at 800 characters.
    • The migration migrations/add_post_sync_action_runs_table.py is idempotent (table_exists) and additive only: nothing is dropped or altered, so no data can be lost. It is appended last in MIGRATIONS.
    • services/database.py registers the model for create_all.
    • Recording never fails a sync: an error is caught and logged.
  • Rebase onto fix(slm): one SSH systemctl executor with quoted, validated units and gated service routes (#18359) #18352 (tech-debt(slm): restarting units over SSH is implemented three times with different sudo, timeout and failure handling #18186 + security(slm): remote command injection — service_name from the URL path is interpolated unquoted into an ssh 'sudo -n systemctl' shell command (reachable by admin/operator users) #18359): where the two overlap, main's hardening wins.

  • Test-isolation fix (found during the rebase): test_post_sync_recording_17455.py run in one process with test_colocated_managed_update_11605.py made 11605 fail at collection (a MagicMock response model). The fix that matters is in 11605: its dev-host stub now also applies when a real models package carries the conftest's MagicMock schemas. A contrast with only that change, and the helper reverted, passes. Additionally, import_modules_with_real_orm now drops modules first imported during its swap. That is defence in depth, not the cause.

Verification

  • After the rebase: plan order + node route 39; recording 12; migrations/*_test.py 66; test_ssh_restart_*_18186.py 60; injection 18359 64; code-sync-preserves-install 15; root guards (remote-command quoting, one-executor, reach ×2, size ratchet) 633. A single batched run of all 9 import_modules_with_real_orm users + 11605 + the 17455 route and plan tests: 218 passed.
  • autobot-slm-backend targeted set before the rebase: 144 passed. That covers the plan order, the node route, recording, the 18186 equivalence file, the 14275 structural test and migrations/. The implementer's wider run gave 170 passed, and repo guards (router auth, mount, duplicate-route, size ratchet, one-executor) gave 130 passed.
  • Intended test deltas:
    • test_orchestrator_argv_has_no_dash_n_one_call_all_units_and_literal_60 → …has_sudo_n_batchmode_one_call_all_units_env_backed_60_17455. tech-debt(slm): restarting units over SSH is implemented three times with different sudo, timeout and failure handling #18186 pinned the old sudo without -n, pending this decision. Decision 3 changes it on purpose.
    • test_orchestrator_swallows_a_nonzero_exit_and_logs_restarted → …records_a_nonzero_exit_as_a_failure_not_restarted_17455. The swallowed exit was the defect decision 3 removes.
    • The two test_code_sync_preserves_install_14275.py structural pins follow the code into post_sync_runner.py. The AST guard requires code != 0 (Compare against 0), and the plan-runner abort is pinned.
  • Opus gate: CHANGES REQUIRED. All findings are fixed:
    • HIGH: versions were recorded after the self-restart, losing the version and the response;
    • MEDIUM: the self-restart outcome was discarded;
    • LOW: a weaker AST pin, a 34-line migrate, stale docstrings.
    • Auth, migration, ordering, sudo -n, redaction, removed code and ratchets were confirmed correct.
  • Auth: all three routes take Depends(get_current_user) and mount on nodes_router / code_sync_router, which main.py includes with dependencies=_SM_OR_AGENT. A test asserts the dependency and the mounts.
  • Ratchets: services/sync_orchestrator.py goes 649 → 561 (under 600, entry deleted), and api/code_sync.py goes 5860 → 5829, in both baselines. No ceiling is raised, and no new noqa is added.

Risks

  • Deploy-visible. The orchestrator restart now needs passwordless sudo -n for systemctl. A host where only interactive sudo works will record a failed restart instead of restarting silently. The other two executor callers already use -n.
  • The migration adds a table only.
  • Generated types: the new routes make autobot-slm-frontend/src/types/generated/api.ts stale. The generator's exact diff is taken from the verify-generated-types-slm job log and committed as a follow-up push, per the reviewer, never hand-written.
  • Local tests/api has 4 environmental failures, identical on the base tree, filed as test(slm): four tests/api tests fail outside CI — pricing reads the deployed .env, admin-socket fails even alone #18234.

Model Used

Anthropic Claude Opus 5.5 (claude-opus-5-5), coordinating and gate review. Anthropic Claude Sonnet 5.5 (claude-sonnet-5-5) subagents for the survey and implementation.

Issue Link

Refs #17455

Not closed by this PR: the AC "the record is reachable from wherever the sync run is shown" is met at the API (the sync responses and GET /post-sync-runs/{run_id}). The SLM fleet-view UI that shows it is the child #18235.

Single-issue rationale: this is one owner decision on #17455 that spans the plan, the orchestrator, routes and the schema. Its only remaining part, the SLM UI, is its own child issue, #18235.

Changelog fragment

@coderabbitai

coderabbitai Bot commented Oct 12, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 12 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository: mrveiss/AutoBot-AI/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 025bb499-08fb-47e9-80f4-3013ab3c7a14

📥 Commits

Reviewing files that changed from the base of the PR and between df211d3 and 57cd4cd.


⛔ Files ignored due to path filters (2)
  • repo_tests/python_file_size_ratchet_baseline.py is excluded by !repo_tests/python_file_size_ratchet_baseline.py
  • scripts/python_file_size_known_large.py is excluded by !scripts/python_file_size_known_large.py

📒 Files selected for processing (27)
  • autobot-slm-backend/api/__init__.py
  • autobot-slm-backend/api/_post_sync_plan.py
  • autobot-slm-backend/api/code_sync.py
  • autobot-slm-backend/api/node_post_sync.py
  • autobot-slm-backend/conftest.py
  • autobot-slm-backend/migrations/add_post_sync_action_runs_table.py
  • autobot-slm-backend/migrations/add_post_sync_action_runs_table_test.py
  • autobot-slm-backend/migrations/runner.py
  • autobot-slm-backend/migrations/runner_exit_code_test.py
  • autobot-slm-backend/models/post_sync_action_run.py
  • autobot-slm-backend/models/post_sync_schemas.py
  • autobot-slm-backend/services/database.py
  • autobot-slm-backend/services/post_sync_plan_runner.py
  • autobot-slm-backend/services/post_sync_recorder.py
  • autobot-slm-backend/services/post_sync_runner.py
  • autobot-slm-backend/services/post_sync_timeouts.py
  • autobot-slm-backend/services/service_restart.py
  • autobot-slm-backend/services/sync_orchestrator.py
  • autobot-slm-backend/tests/api/_real_orm_import.py
  • autobot-slm-backend/tests/api/test_colocated_managed_update_11605.py
  • autobot-slm-backend/tests/api/test_node_post_sync_17455.py
  • autobot-slm-backend/tests/api/test_post_sync_plan_order.py
  • autobot-slm-backend/tests/api/test_ssh_restart_equivalence_18186.py
  • autobot-slm-backend/tests/services/test_code_sync_preserves_install_14275.py
  • autobot-slm-backend/tests/services/test_post_sync_recording_17455.py
  • autobot_shared/env_registry_slm.py
  • docs/developer/ENV_VARS.md

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@mrveiss

mrveiss commented Oct 12, 2026

Copy link
Copy Markdown
Owner Author

Security review (Opus) at 57cd4cd: approve.

LOWs are filed as #18424: retention, the missing downgrade() despite the docstring, the concept declaration, and the narrower module cleanup. Queued for the next merge train.

@github-actions

Copy link
Copy Markdown
Contributor

✅ SSOT Configuration Compliance: Passing

🎉 No new hardcoded values of either class — ssot and other both block.

Known backlog in pipeline-scripts/hardcoded_values_baseline.txt is suppressed and tracked in #14371.

@github-actions

Copy link
Copy Markdown
Contributor

Notice: 29 open PRs — past the runaway threshold (25)

This is not a request to defer this PR. The actual limit is the open-PR cap (AUTOBOT_OPEN_PR_CAP, default 40), enforced at pre-push against NEW branches only -- pushing more commits to this PR's own branch is never blocked by it.

This notice only means the count is high enough to be worth a glance for a runaway — something opening PRs in a loop, or a merge pipeline that has stalled so nothing is draining.

Currently open:

If the queue is draining normally, ignore this. Otherwise:

  1. Check whether CI is dispatching at all — see the ci-dispatch-watchdog status on these PRs
  2. Merge the ones whose CI has finished and review has passed: gh pr merge <number> --squash --delete-branch
  3. Look for a loop opening near-identical PRs

Warn-only runaway detector — .github/workflows/pr-queue-gate.yml. It never blocks a merge.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant