Documentation > Architecture > Agent Deployment Protocol

Agent Generic Deployment Protocol

Server-to-agent message protocol for the three open-source deployment primitives every Pro+ engine builds on: deploy_files, execute_command_sequence, and service_control.

Why these messages exist

The architectural decision made in Phase 3 split the deployment pipeline in two:

  • Server side (Pro+ Cython modules) generates fully-baked configuration — firewall rule sets, AV deployment plans, OTEL collector configs, VM definitions. This is where the proprietary value lives.
  • Agent side (open-source) just deploys files, runs commands, and controls services. It has no knowledge of what any particular file means.

The three messages documented here are the entire interface between the two halves. They are deliberately generic — adding a new Pro+ feature does not require any agent code change.

Agent implementation lives in sysmanage-agent/src/sysmanage_agent/operations/generic_deployment.py (deploy and command sequencing) and sysmanage-agent/src/sysmanage_agent/core/agent_utils.py (service control). All three return a structured result dict; none of them ever raise out of the handler.

deploy_files

Atomic file deployment. Writes one or more files to the local filesystem using the temp-file-plus-rename pattern, with optional SHA-256 verification and backup/rollback support.

Request

{
    "message_type": "command",
    "data": {
        "command_type": "deploy_files",
        "parameters": {
            "files": [
                {
                    "path": "/etc/myservice/myservice.conf",
                    "content": "key = value\n",
                    "permissions": "0644",
                    "owner_uid": 0,
                    "owner_gid": 0,
                    "expected_sha256": "5c8f2c3...",
                    "backup": true
                }
            ]
        }
    }
}

File spec fields

FieldRequiredTypeNotes
pathyesstringAbsolute destination path. Parent dirs are created (mode 0755) if missing.
contentyesstringFull file contents. The agent appends a trailing newline if the content does not end in one (POSIX text-file convention).
permissionsnooctal stringDefault "0644".
owner_uidnointegerDefault 0. Skipped on Windows.
owner_gidnointegerDefault 0. Skipped on Windows.
expected_sha256nohex stringIf supplied, the agent verifies BOTH the pre-write content (to catch transport corruption) AND the on-disk file post-rename (to catch filesystem-level mutation). Mismatch triggers rollback if a backup exists, otherwise removes the bad file.
backupnobooleanDefault false. When true, an existing target is copied to <path>.sysmanage.bak before the new content is written. The backup is left in place on success (admin can clean up); on post-write hash mismatch the backup is restored.

Response

{
    "success": true,
    "deployed_count": 1,
    "deployed_files": [
        {
            "success": true,
            "path": "/etc/myservice/myservice.conf",
            "bytes_written": 14,
            "sha256": "5c8f2c3...",
            "backup_path": "/etc/myservice/myservice.conf.sysmanage.bak"
        }
    ],
    "error_count": 0,
    "errors": []
}

success is true if at least one file was deployed. Per-file outcomes are in deployed_files (each carries its own success) and errors.

execute_command_sequence

Run a sequence of steps in order. Stops at the first failed step. A "step" is a discriminated union of three types: shell (run a command), deploy_file (one-off file deployment, same semantics as deploy_files), and wait_condition (poll a command's output until it matches a regex).

Request

{
    "message_type": "command",
    "data": {
        "command_type": "execute_command_sequence",
        "parameters": {
            "steps": [
                {
                    "type": "deploy_file",
                    "description": "Drop the service config",
                    "path": "/etc/myservice/myservice.conf",
                    "content": "key = value\n",
                    "permissions": "0644"
                },
                {
                    "type": "shell",
                    "description": "Reload the service",
                    "command": "systemctl reload myservice",
                    "timeout": 30
                },
                {
                    "type": "wait_condition",
                    "description": "Confirm it's listening",
                    "command": "ss -ltn",
                    "match": ":443 ",
                    "timeout": 60,
                    "interval": 2
                }
            ],
            "progress_message_type": "command_sequence_progress",
            "child_host_id": "00000000-0000-0000-0000-000000000abc"
        }
    }
}

Step types

typeRequired fieldsOptional fieldsBehavior
shellcommandtimeout (default 300s)Runs via subprocess. Step succeeds iff exit code is 0.
deploy_filepath, contentsame optional fields as in deploy_filesSingle-file atomic deploy with the same SHA-256 + backup semantics.
wait_conditioncommand, matchtimeout (default 60s), interval (default 1s)Re-runs command on the interval until its stdout matches the match regex, or times out. Useful for waiting on services to become ready.

Optional progress streaming

If progress_message_type is supplied, the agent emits one progress message per step before executing it. The message envelope uses the supplied type so callers can route progress without colliding with other in-flight commands. Default type is command_sequence_progress.

{
    "message_type": "command_sequence_progress",
    "data": {
        "step": 2,
        "total_steps": 3,
        "description": "Reload the service",
        "status": "running",
        "child_host_id": "00000000-0000-0000-0000-000000000abc"
    }
}

Response

{
    "success": true,
    "completed_steps": 3,
    "total_steps": 3,
    "results": [
        {"step": 1, "type": "deploy_file", "description": "Drop the service config", "success": true, "output": "..."},
        {"step": 2, "type": "shell",       "description": "Reload the service",     "success": true, "output": "..."},
        {"step": 3, "type": "wait_condition", "description": "Confirm it's listening", "success": true, "output": "..."}
    ],
    "errors": []
}

If a step fails, the sequence stops; completed_steps reflects the count of successful steps and errors contains the failure message. The remaining steps are not attempted.

service_control

Start, stop, restart, enable, or disable one or more services. The agent picks the appropriate service manager based on the host platform — systemctl on most modern Linux distros, rc-service + rc-update on Alpine/Gentoo (OpenRC), launchctl on macOS, or sc.exe on Windows. The same message works across distros without the caller knowing which manager runs the host.

Request

{
    "message_type": "command",
    "data": {
        "command_type": "service_control",
        "parameters": {
            "action": "restart",
            "services": ["myservice", "myservice-helper"]
        }
    }
}

Action values

ActionEffect
startBring the service up now. No-op if already running.
stopTake the service down now. No-op if already stopped.
restartStop, then start.
enableMark the service to start on boot. Does NOT start it now.
disableMark the service to NOT start on boot. Does NOT stop it now.

Privilege requirement

The handler refuses to operate when the agent is not running in privileged mode (is_running_privileged() returns false). In unprivileged mode the response is {"success": false, "error": "Service control requires privileged mode"} — the caller is expected to surface that to the operator and request elevated re-deployment.

Response

{
    "success": true,
    "action": "restart",
    "services": ["myservice", "myservice-helper"],
    "results": {
        "myservice":        {"success": true, "stdout": "...", "stderr": ""},
        "myservice-helper": {"success": true, "stdout": "...", "stderr": ""}
    },
    "message": "Service restart completed for 2 services"
}

success is true iff EVERY service in the list completed successfully. Per-service outcomes are in results.

Versioning & backwards compatibility

These three messages are the open-source contract every Pro+ release ships against. Backwards-incompatible changes to any of the request or response shapes documented above require:

  • A new command_type string (e.g., deploy_files_v2) — never an in-place schema change.
  • A migration path documented in the agent release notes.
  • A version-detection probe so older Pro+ engines can fall back to the previous wire format.

Adding optional new fields to existing messages is safe and does not require versioning, provided the agent treats unknown fields as no-ops.

Implementation references

  • Agent handlers: sysmanage-agent/src/sysmanage_agent/operations/generic_deployment.py (deploy_files, execute_command_sequence); sysmanage-agent/src/sysmanage_agent/core/agent_utils.py (_handle_service_control, _build_service_control_cmd).
  • Tests: sysmanage-agent/tests/test_generic_deployment.py, sysmanage-agent/tests/test_agent_utils_comprehensive.py::TestServiceControlNewActions, sysmanage-agent/tests/test_agent_utils_comprehensive.py::TestBuildServiceControlCmd.
  • Pro+ callers: av_management_engine, firewall_orchestration_engine, and container_engine all generate plans that ultimately call these three messages.

Navigation