Skip to content

VM_CONTACT: upgrade path for VMs that already have vm-mcp - #85

Merged
erinepshovel-code merged 2 commits into
mainfrom
runbook/vm-mcp-existing-install
Oct 14, 2026
Merged

erinepshovel-code merged 2 commits into
mainfrom
runbook/vm-mcp-existing-install

Conversation

@erinepshovel-code

Copy link
Copy Markdown
Contributor

Adds an upgrade section (U1–U6) to backend/deploy/VM_CONTACT.md for VMs with an existing vm-mcp install:

  • U1 inventory + root-only backup (env with token values blanked, version, vmmcp home, VM_MCP_ROOT, profile, legacy SHELL_ENABLED, units, listeners)
  • U2 derive a preserved target (root, profile resolved like profiles.resolve_profile, port); stops on ambiguity; changes only by explicit edit of target.env
  • U3 token; U4 stop units right before install (fixes usermod exit 8), brief-downtime note, failure/restart path
  • U5 checks a–f adapted per profile, preservation checks, and #127 non-destructive checks incl. fixed-install-root / overlap source checks against a 023ae12 checkout when using --skip-install
  • U6 rollback that restores the inventoried env/units/code
  • Worked example from a real upgrade attempt

Goal: preserve the existing profile and privileged components by default and token-protect all local endpoints. Docs only; installer follow-ups go to skill-lib separately. Not for merge without Erin.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 14, 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-14T03:42:51.416796Z 57e0ca6 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: 030bab3d81

ℹ️ 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".

B=$(sudo -n cat /root/vm-mcp-upgrade.latest)
eval "$(sudo -n cat "$B/target.env")"
sudo -n test -d "$TARGET_ROOT"
TOKEN_FILE=${TOKEN_FILE:-/root/vm-mcp.token}

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 Honor the documented credential-file override

When an existing installation reuses /etc/credstore/vm-mcp-token as instructed, U4 ignores VM_MCP_AUTH_TOKEN_FILE: it initializes the separate TOKEN_FILE variable to /root/vm-mcp.token and then overwrites VM_MCP_AUTH_TOKEN_FILE with that value. If step 1 was skipped, the installer exits 3 only after both services have been stopped; even if the operator discovers and exports TOKEN_FILE instead, check e still reads /root/vm-mcp.token. Carry one selected token path through both installation and verification.

Useful? React with 👍 / 👎.

Comment on lines +355 to +357
# start what was running before
if [ "$(sudo -n cat "$B/broker-was" 2>/dev/null)" = active ]; then sudo -n systemctl start vm-mcp-admin.service; fi
sudo -n systemctl start vm-mcp.service

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 Restore the units' prior active and enabled states

When either service was inactive or disabled before the upgrade, U6 does not restore that state: the pinned installer enables the main unit and conditionally the broker, U1 records only whether the broker was active, and this block always starts vm-mcp.service without restoring either unit's enablement. The local systemctl --help confirms that start activates a unit and enable changes its unit-file state, so rollback can activate and persist a legacy, potentially unauthenticated endpoint that was deliberately off; snapshot and restore both active and enabled states for both units.

Useful? React with 👍 / 👎.

port = env.get("VM_MCP_PORT", "8765")
with open(f"{B}/target.env", "w") as f:
os.fchmod(f.fileno(), 0o600)
f.write(f"TARGET_ROOT={root}\nTARGET_PROFILE={prof}\nTARGET_PORT={port}\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 Quote preserved values before evaluating target.env

For an existing valid VM_MCP_ROOT containing whitespace or shell metacharacters, this emits an unquoted shell assignment and U4 later evaluates it with eval. A root such as /srv/my workspace therefore tries to execute workspace and aborts, while command substitutions embedded in a path are executed; the pinned installer itself permits every absolute path without newlines. Serialize these values with shell-safe quoting or consume the file without eval.

Useful? React with 👍 / 👎.

Comment on lines +268 to +270
| b | as written | broker unit `inactive`, `/run/vm-mcp/admin.sock` absent; all other lines as written |
| c, d, e | as written | as written (bearer auth is required in every profile) |
| f | as written | the call in f must be refused by policy (`ok: false`) and no broker receipt may appear |

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 Run upgrade checks against TARGET_PORT

When the existing installation uses a non-default VM_MCP_PORT, U4 correctly preserves it as TARGET_PORT, but running check b and checks e/f “as written” still polls and curls port 8765. The upgraded service can therefore be healthy on its preserved port while the required acceptance sequence waits, fails, and probes an unrelated listener; parameterize those checks with the preserved target port.

Useful? React with 👍 / 👎.

Comment on lines +345 to +346
if [ -f '$B/opt-vm-mcp.tar' ]; then rm -rf /opt/vm-mcp.rollback-old; mv /opt/vm-mcp /opt/vm-mcp.rollback-old 2>/dev/null || true; tar -C /opt -xpf '$B/opt-vm-mcp.tar'; fi
[ -f '$B/vm-mcp.env' ] && install -o root -g root -m 0600 '$B/vm-mcp.env' /etc/vm-mcp.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 Remove artifacts that were absent before rollback

The upgrade path is explicitly selected when either /opt/vm-mcp or /etc/vm-mcp.env exists, but U6 only restores these paths when their corresponding backup exists and never removes a newly created counterpart. Thus, for a partial pre-upgrade installation, rollback can leave the new /opt/vm-mcp tree or generated environment file behind instead of returning to the inventoried state; record absence and delete the artifact on rollback when it was originally absent.

Useful? React with 👍 / 👎.

cp -a /etc/systemd/system/vm-mcp.service.d '$B/vm-mcp.service.d' 2>/dev/null || true
cp -a /etc/credstore/vm-mcp-token '$B/credstore-token' 2>/dev/null || true
cp -a /etc/sysctl.d/99-zz-vm-mcp-ptrace.conf '$B/' 2>/dev/null || true
tar -C /opt -cpf '$B/opt-vm-mcp.tar' vm-mcp 2>/dev/null || true

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 Fail U1 when the code backup is incomplete

If tar fails after creating a partial archive—for example because the filesystem fills—|| true lets U1 report success and U6 later treats the mere existence of that archive as a valid backup. U6 then moves the working /opt/vm-mcp tree aside before extracting the truncated archive, leaving rollback and the service broken; ignore only a confirmed absent source and otherwise require the archive operation and an integrity check to succeed.

Useful? React with 👍 / 👎.

Comment on lines +268 to +270
| b | as written | broker unit `inactive`, `/run/vm-mcp/admin.sock` absent; all other lines as written |
| c, d, e | as written | as written (bearer auth is required in every profile) |
| f | as written | the call in f must be refused by policy (`ok: false`) and no broker receipt may appear |

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 Make non-personal-console checks report their expected result

For the explicitly supported workspace and read-only upgrades, the table changes the prose expectation but not the commands: check b still expects the broker to be active and its socket present, while check f still expects a successful broker receipt. Correctly disabled broker behavior therefore prints FAIL, contradicting the runbook's statement that check blocks identify success with PASS and making the required a–f acceptance sequence unusable for these preserved profiles; provide profile-aware expected values or separate executable blocks.

Useful? React with 👍 / 👎.

Comment on lines +333 to +336
Restores the env, units, drop-ins, sysctl file and code exactly as backed up in U1.
**This returns the VM to its pre-upgrade code, so endpoints that were not
token-protected before are unprotected again.** Prefer fixing forward; use this only
to recover service. For a profile-only rollback, use the `sed` rollback below instead.

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 Restore the service user's home during rollback

When the operator uses the explicitly sanctioned target.env edit to change TARGET_ROOT, the installer runs usermod --home for vmmcp, but U6 restores only files and never restores the passwd entry recorded during inventory. Rolling back after that upgrade leaves the old service running with the new home and can redirect its HOME-dependent state away from the restored workspace; save the original home in machine-readable form and restore it before restarting the old unit.

Useful? React with 👍 / 👎.

Comment on lines +187 to +190
port = env.get("VM_MCP_PORT", "8765")
with open(f"{B}/target.env", "w") as f:
os.fchmod(f.fileno(), 0o600)
f.write(f"TARGET_ROOT={root}\nTARGET_PROFILE={prof}\nTARGET_PORT={port}\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 Preserve the configured admin socket during upgrade

When an existing installation has a non-default VM_MCP_ADMIN_SOCKET, U2 omits it from target.env and U4 passes no corresponding variable, so the installer rewrites the environment with /run/vm-mcp/admin.sock. Earlier or locally customized units and drop-ins can therefore stop agreeing with the main service, breaking admin_exec despite the stated promise to preserve privileged components; inventory and pass the existing socket alongside the root, profile, and port.

Useful? React with 👍 / 👎.

@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: 57e0ca6600

ℹ️ 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".

echo
SHA=$(printf %s 'id -u' | sha256sum | cut -d' ' -f1)
ok "broker receipt for id -u since $START" 1 "$(sudo -n journalctl -u vm-mcp-admin.service --since "$START" --no-pager | grep -F '"command_sha256":"'"$SHA"'"' | grep -F '"mode":"admin"' | grep -F '"run_as":"root"' | grep -cF '"exit_code":0' | sed 's/^[1-9][0-9]*$/1/' || true)"
ok "broker receipt for id -u since $START" 1 "$(sudo -n journalctl -u vm-mcp-admin.service --since "$START" --no-pager | grep -F '"command_sha256":"'"$SHA"'"' | grep -cF '"exit_code":0' | sed 's/^[1-9][0-9]*$/1/' || true)"

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 Require the receipt to prove privileged execution

When a concurrent or same-second user_exec runs id -u, this pipeline accepts its receipt because the broker records the same command digest and exit code for both user and admin modes, while the preceding curl response is never asserted. Step f can therefore report every check as PASS even if the selected privileged call failed or never ran as root; retain the mode/run_as predicates or correlate the returned request ID with the receipt.

AGENTS.md reference: AGENTS.md:L110-L112

Useful? React with 👍 / 👎.

# printed inventory (no secrets)
sudo -n sh -c "{
echo '--- /etc/vm-mcp.env (token values blanked)'
sed -E 's/^([A-Z_]*TOKEN[A-Z_]*)=.*/\1=<blanked>/' /etc/vm-mcp.env 2>/dev/null || echo 'absent'

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 Redact indented token assignments before printing

When an existing environment file contains an indented assignment such as VM_MCP_AUTH_TOKEN=..., this expression does not match because it requires the variable name at column zero, even though U2 explicitly parses assignments with leading whitespace. U1 then prints the live bearer credential into the SSH terminal and any captured run log, contrary to the runbook's secret-handling guarantees; allow leading whitespace when identifying token keys before emitting the inventory.

Useful? React with 👍 / 👎.

sudo -n env -u VM_MCP_AUTH_TOKEN -u VM_MCP_AUTH_TOKEN_STDIN \
VM_MCP_PROFILE="$TARGET_PROFILE" VM_MCP_ROOT="$TARGET_ROOT" VM_MCP_PORT="$TARGET_PORT" \
VM_MCP_AUTH_TOKEN_FILE="$TOKEN_FILE" \
bash "$ROOT_SRC/skill-lib/vm-mcp/install.sh" || rc=$?

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 Initialize rc before invoking the installer

When the interactive shell already has a nonzero rc variable, the subshell inherits it and a successful installer invocation leaves it unchanged because this assignment runs only on failure. The following status check then reports the stale value and exits even though the upgrade and service restart succeeded, potentially prompting an unnecessary rollback; initialize rc=0 before the invocation or assign the success status explicitly.

Useful? React with 👍 / 👎.

@erinepshovel-code
erinepshovel-code merged commit ec90644 into main Oct 14, 2026
4 of 5 checks passed
erinepshovel-code added a commit that referenced this pull request Oct 14, 2026
…-preservation

Repair VM upgrade preservation and verification after #85
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