Skip to content

Operator deploy CLI with health-checked rollback, one deploy routine - #1386

Merged
Th0rgal merged 2 commits into
masterfrom
operator-deploy-cli
Oct 28, 2026
Merged

Th0rgal merged 2 commits into
masterfrom
operator-deploy-cli

Conversation

@Th0rgal

@Th0rgal Th0rgal commented Oct 28, 2026

Copy link
Copy Markdown
Owner

What

  • sandboxed-sh deploy --service <unit> [--ref] [--repo] [--skip-build] [--force]: operator deploy run as root on the host. Standard prod deploy:
    ssh agent-core '/usr/local/bin/sandboxed-sh-prod deploy --service sandboxed-sh-prod.service --ref origin/master'
  • stream_deploy's body is now run_deploy(plan, tx) in src/api/system.rs, reporting progress over a channel. The API wraps it in SSE, and the CLI prints the lines. There is one deploy implementation and no shell script.
  • The CLI applies the same refusals through evaluate_deploy_request: the binary must serve --service, plus the live-writer and debounce checks (--force bypasses the last two). It adopts the service's environment (/proc/<MainPID>/environ, or the unit's Environment/EnvironmentFile when it is down) for port, working dir and repo path. Live writers are counted from the on-disk SQLite mission stores using the same count_live_writers helper as the API.
  • The guarded restart (stop, wait for the port, start) now polls /api/health for DEPLOY_HEALTH_TIMEOUT_SECS (default 120). If the check fails, it restores the previous binaries (backup copy, symlink retarget, or removal of a first install) and restarts again. The outcome is written to /var/lib/sandboxed-sh/last_deploy_result. The CLI waits with systemd-run --wait and exits non-zero on rollback. The API path gets the same check and rollback. Self-update keeps the plain guarded restart.
  • Docs: DEBUGGING.md "Deploying Updates" and the MCP section, AGENTS.md, and docs/PRIVILEGE_BROKER_AND_BUNKER.md. The SSH command is the operator path and the sanctioned fallback when the approval UI or broker is down.

Authorization

The API path is unchanged: missions still need an active service_deploy grant, and orb_deploy_gate_step still applies. The CLI requires root shell access on the host, which is already full trust.

Principles traded

  • The CLI does not consult Bunker Mode, because it never goes through Core. Root on the host can already bypass Core.
  • If curl is missing on the host, the health check reports unchecked instead of rolling back. A missing tool should not undo a good deploy.

Tests

cargo test --lib api::system::: 61 passed. The new tests cover argument parsing, the refusals the CLI applies, the environment parsers, the health URL, the restart-outcome decision, the rollback shell commands and the health script order. cargo clippy -D clippy::all is clean on the touched code.

Not deployed. The operator will run the command above.

…ed rollback

sandboxed-sh deploy --service <unit> runs the same run_deploy as
POST /api/system/deploy (refactored out of the SSE stream), applies the same
refusals, waits for the guarded restart and reports its health check. The
restart script now requires /api/health after start and restores the
previous binaries otherwise, for both entry points. The API grant for
missions is unchanged.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-28T10:41:47.529246Z a82fbef New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 916a1649b9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/api/system.rs
repo_path,
git_ref: args.git_ref.clone(),
skip_build: args.skip_build,
service_name: args.service.clone(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Derive install destinations from the binary's actual service name

When invoking sandboxed-sh-dev deploy --service orb-core-dev.service, the service-equivalence guard accepts the alias, but passing that alias into DeployPlan changes the install destinations: service_binary_path_for_unit selects /usr/local/bin/orb-core-dev, and service_companion_binary_path only recognizes the sandboxed-sh- prefix, so it selects the unsuffixed production companions. With the checked-in deploy/systemd/orb-core-dev.service, whose ExecStart is /usr/local/bin/sandboxed-sh-dev, this both leaves the running backend binary unchanged and overwrites production MCP/palomactl binaries. Preserve the actual binary service identity for installation rather than using the user-supplied alias. This violates the dev/prod isolation requirement in AGENTS.md.

AGENTS.md reference: AGENTS.md:L364-L367

Useful? React with 👍 / 👎.

Comment thread src/api/system.rs Outdated
std::env::set_var(key, value);
}
}
let config = match crate::config::Config::from_env() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Adopt the service working directory before loading configuration

If WORKING_DIR is unset, copying the service environment does not reproduce its configuration: Config::from_env defaults debug builds to the current directory, but this CLI retains the SSH caller's directory instead of the service's working directory. The checked-in core units use /var/lib/sandboxed-sh or /var/lib/sandboxed-sh-dev, while an SSH invocation can start in /root. Consequently, count_live_writers_on_disk scans the wrong mission directory and returns zero, allowing a non-forced deploy to interrupt running missions; settings-based repository selection also reads the wrong state directory. Resolve the live process's cwd or the unit's configured working directory before constructing the configuration.

Useful? React with 👍 / 👎.

… environment

Addresses security review: no set_var of the service environment, no
/proc/<pid>/environ; PORT/HOST/WORKING_DIR/repo path are read from the unit's
EnvironmentFile/Environment and passed explicitly. Test asserts LD_PRELOAD,
GIT_DIR, BASH_ENV, PATH, CARGO_*, RUSTC_* are never adopted.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a82fbef81d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/api/system.rs
Comment on lines +6081 to +6084
if let Some(inline) = systemctl_show(service, "Environment").await {
env.extend(parse_environment_file(
&inline.split_whitespace().collect::<Vec<_>>().join("\n"),
));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply environment-file values after inline unit values

When a unit defines the same allowlisted key in both Environment= and EnvironmentFile=, this merge selects the inline value because service_deploy_env searches the pairs in reverse. That is opposite to systemd: its documentation states, “Settings from these files override settings made with Environment=.” For example, an inline PORT=3000 overridden by PORT=3100 in the environment file makes the CLI guard and health-check port 3000 while the restarted service actually listens on 3100, potentially rolling back a healthy deployment. Collect inline assignments first, then append the environment files in their configured order. This also undermines the documented requirement that both deploy entry points health-check the restarted service.

AGENTS.md reference: AGENTS.md:L420-L421

Useful? React with 👍 / 👎.

@Th0rgal
Th0rgal merged commit db021c5 into master Oct 28, 2026
14 checks passed
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