Plan #6618 — bms-4 worker-fabric autonomy readiness
Status: design / awaiting
/review-plan(min 3 cycles) before any Class-B/C implementation. Issue: radieu/p24-infra#6618 · Role authoring this plan:dev-coder. Goal: bring the bms-4 worker fabric to a state where it needs no interactive dev machine for the operations that failed on it recently (bms-1 OS upgrade 6602,IAM_ENC_KEYrotation #6561, future CF Worker deploys). The target boundary is “one-time paste of one token per external service” — not “zero dev-access”, which is impossible (see Class A).
1. Problem framing — three classes of barrier (verified live 2026-08-23)
The key enabling fact, confirmed against .sops.yaml: the bms-4 worker age key
age1qrccz84pj8huj8hmx6452te2zugane0slgxs09h9s05m4zpgpetq8p9up0 is already a recipient of the
default 6-recipient .sops.yaml rule, so the bms-4 infra-task worker can already sops -d every
default-rule file directly. It is also the second recipient (with the developer key) on the two
existing narrow worker files pinbox24-gitlab.env.sops and wasabi-admin-worker.env.sops. This
means new scoped SOPS files can be encrypted to the worker from public keys alone — no developer
age private key is required to create them.
| Class | Definition | Autonomous? | Examples (this issue) |
|---|---|---|---|
| A | Value origination behind a vendor panel gated by human 2FA. No API to mint. | ❌ never — one-time human paste | Valid SoYouStart/OVH-budget dedicated-server API consumer-key for bms-1 (SYS_INFRA_* currently returns INVALID_KEY); a CF Workers Scripts:Edit token on the whatsup CF account; Wasabi root key rotation (no self-rotation API). |
| B | Distribution of an originated value into scoped SOPS + secrets-sync wiring + re-key. | ✅ after origination — secret-manager on bms-4 already does this (proof: rotations 6593). | SOPS scaffold precreation (§3), secrets-sync jobs. |
| C | Tooling + sanction gates entirely inside the repo. | ✅ now | destructive-gate override token (§4), wrangler availability (§5), wa-app-api deploy pipeline (§5). |
The residual human step this plan is aiming for is exactly: sops edit <scoped-file> → paste one
freshly-minted token → git commit. Zero recipient-surgery, zero re-key, because the scoped files
are pre-created (Class B, §3) with the worker already a recipient.
2. Scope, ownership, and gating (what ships where)
| # | Item | Class | Owner role | Gate | Ships in |
|---|---|---|---|---|---|
| §3 | SOPS scaffold precreation (ovh-sys, cloudflare-workers-bms4) + .sops.yaml rules + secrets-sync jobs | B | secret-manager (SOPS write) | after this plan approved | follow-up secret-manager issue |
| §4 | Destructive-gate authorization-token convention in infra-task-request-worker.md Step 4 | C (security-sensitive) | dev-coder | hard-gated on green /review-plan (risk+security+compliance) | follow-up dev-issue |
| §5 | wrangler globally on bms-4 | C | sys-admin/infra-task (server op / Ansible) | after this plan approved | follow-up infra-task issue |
| §5 | wa-app-api (repo whatsup-android-chat-puller) deploy pipeline | C | dev-coder in that repo | after this plan approved | follow-up cross-repo issue |
| §6 | Class-A human-origination checklist | A (doc) | dev-coder | none — doc | this PR |
| §7 | bms1-ubuntu-2204-upgrade.md runbook safety fix (wrong server-ID) | — (doc) | dev-coder | none — doc | this PR |
This PR (dev-coder, plan issue) ships only §6 + §7 + this document. Everything in Classes B/C is
out of dev-coder scope or hard-gated, so it is deferred to the follow-up issues listed above and
tracked from the issue. Rationale: dev-coder capabilities.cannot forbids writing/decrypting any
secrets/*.env.sops (§3 → secret-manager) and SSH/Ansible against a server (§5 wrangler →
sys-admin); §4 is dev-coder-editable but the issue mandates it not be implemented without a
green /review-plan.
3. Class B — SOPS scaffold precreation (follow-up: secret-manager)
Objective: collapse the human step for each Class-A credential to sops edit → paste → commit.
3.1 New scoped files (placeholders only, 2 recipients)
Model exactly on pinbox24-gitlab.env.sops / wasabi-admin-worker.env.sops — 2 recipients:
developer key age1utmdm6cj2ray70g5nnupza8uvyuren26xk6a6nd4a58arax5xa7qaax7al + bms-4 worker key
age1qrccz84pj8huj8hmx6452te2zugane0slgxs09h9s05m4zpgpetq8p9up0.
| New file | Placeholder keys | Consumer | Notes |
|---|---|---|---|
secrets/ovh-sys.env.sops | SYS_INFRA_APP_KEY, SYS_INFRA_SECRET, SYS_INFRA_CONSUMER_KEY | bms-1 dedicated-server API ops (rescue/reboot/reinstall) | See decision 3.3 — new file vs re-key of ovh-api.env.sops. |
secrets/cloudflare-workers-bms4.env.sops | CF_WORKERS_EDIT_TOKEN | wrangler deploy of wa-app-api (p24-wa-app-api) on the whatsup CF account | scope = Workers Scripts:Edit for wa-app-api only. |
Placeholder discipline (issue gate): the files must contain placeholder strings only, never real values (e.g.
SYS_INFRA_APP_KEY=PLACEHOLDER_PENDING_HUMAN_PASTE). Canary-decrypt before commit. Per-keygrep|cut, neversourcethe whole.env(footgun class of 6600).
3.2 .sops.yaml rules
Add two path_regex rules above the default rule, byte-identical in shape to the existing
pinbox24-gitlab / wasabi-admin-worker rules (developer + bms-4 worker key). These rules must be
added before the files are encrypted (SOPS reads the rule to pick recipients at encrypt time).
3.3 Decision — new ovh-sys.env.sops vs re-key ovh-api.env.sops
Recommendation: create a new secrets/ovh-sys.env.sops; do not re-key ovh-api.env.sops.
- The bms-4 worker can already decrypt
ovh-api.env.sops(default-rule file) — so this is not about decryptability. It is about (a) a clean one-credential paste surface for the human, and (b) blast-radius isolation of the SoYouStart/budget-brand API family from the OVH-account API family that shareovh-api.env.sopstoday. ovh-api.env.sopsis excluded from secrets-sync.yml (!secrets/ovh-api.env.sops, line 30) and consumed by direct workersops -d. A new isolated file keeps that “direct-decrypt, no CI push” property while giving a dedicated paste target.- Migration of the existing
OVH_INFRA_*keys out ofovh-api.env.sopsis out of scope — this plan only adds theSYS_INFRA_*scoped home. (Open question Q1 for review: shouldSYS_INFRA_*be removed fromovh-api.env.sopsto avoid a two-copy drift, mirroring the GITLAB_ADMIN_PAT atomic-rotation policy? Default answer: yes, remove fromovh-api.env.sopsin the same secret-manager PR so there is exactly one home.)
3.4 secrets-sync.yml wiring (deploy-guarded, fail-closed)
For any new file that must reach a server, add a dedicated sync-* job modeled on sync-traccar
(.github/workflows/secrets-sync.yml), whose guard step fails closed while the value is still a
placeholder:
# Guard — refuse deploy while the credential is still a placeholder (mirror of sync-traccar #5510)
VAL=$(grep -m1 '^SYS_INFRA_CONSUMER_KEY=' "$RUNNER_TEMP/ovh-sys.env" | cut -d= -f2-)
case "$VAL" in ""|PLACEHOLDER_*) echo "placeholder — refusing to deploy"; exit 1;; esacovh-sys.env.sops: no server push needed — the worker decrypts it directly on bms-4. So it is excluded from the push trigger (add!secrets/ovh-sys.env.sopstoon.push.paths, same asovh-api.env.sops). Nosync-*job required.cloudflare-workers-bms4.env.sops: same — the worker readsCF_WORKERS_EDIT_TOKENdirectly at deploy time. Exclude from push trigger; nosync-*job. (Open question Q2: confirm the worker reads it via directsops -d, not a CI-deployed plaintext, before deciding no job is needed.)detect-changespath lists must be updated only if async-*job is actually added.
Note on workflow-file pushes: editing
.github/workflows/secrets-sync.ymlrequires theGH_TOKENPAT push path (docs/playbooks/worker-push-workflow-files.md) — the worker’s defaultghOAuth login lacksworkflowscope. The secret-manager follow-up must budget for this.
3.5 Compliance obligation (CLAUDE.md §Compliance)
§3 introduces two new scoped credential families (ovh-sys, cloudflare-workers-bms4) and,
conditionally, new secrets-sync.yml jobs. Per CLAUDE.md §Compliance (“New … credential/workflow →
update dev_r_services + ops doc”), the secret-manager follow-up must, as part of the same PR:
- add the two files to the CLAUDE.md §SOPS file map table (canonical secret registry);
- record any new
secrets-sync.ymljob / new credential family indev_r_serviceswhere applicable, and reference thedocs/policies/credential-isolation-policy.mdtwo-copy/atomic-rotation policy if Q1 leavesSYS_INFRA_*duplicated.
This line exists so the delegated follow-up does not drop the compliance step.
4. Class C — destructive-gate authorization-token (follow-up dev-issue, review-gated)
Current behaviour (infra/agent-prompts/infra-task-request-worker.md Step 4): any of
delete|drop|truncate|rm -rf|remove credential|revoke|wipe|purge|destroy in the issue title/body →
hard block → human-action label → STOP. There is no override path, so a legitimately
pre-authorized destructive op (e.g. do-release-upgrade on bms-1, dd a rescue image) can never run
autonomously even when a human has explicitly sanctioned it.
Proposed convention (design only — do not implement without a green /review-plan):
- Override token line in the issue body:
# DESTRUCTIVE-AUTHORIZED: <github-user> <issue#> <ISO-8601-timestamp> - Mandatory rollback-proof requirement: the same issue must contain a
Rollback:/ rescue-path / backup reference (e.g. “OVH rescue image captured”, “mongodump at s3://…”). Gate refuses if absent. - Step 4 lets the destructive payload through only when both the token line and the rollback-proof are present, and writes an audit-trail row (dev_r priorities/session log) recording who authorized what.
- Anti-self-authorization (review Q3): require
issue-author == <github-user>on the token line and that the author is on an allow-list of authorized humans — so a worker cannot write its own override. The allow-list source is an open question (hard-coded vs adev_r_*table vs GH team).
Why review-gated: this deliberately relaxes the single gate that protects production from
autonomous destructive actions. It must pass /review-plan on the risk, security, and compliance
perspectives before any code lands. Do not blind-ship. The infra-task-request-worker.md file is
not protected: true, so dev-coder may edit it — but the review gate is a hard precondition per the
issue.
5. Class C — tooling gaps
5.1 wrangler on bms-4 (follow-up: sys-admin/infra-task via Ansible)
Today CF Worker deploys in this repo run npx wrangler deploy (see deploy-meta-dispatcher.yml,
deploy-p24-auth-worker.yml). For an infra-task worker deploying directly on bms-4, npx wrangler also works but re-downloads per invocation. Options:
- (recommended) add
npm install -g wranglerto theclaude-runnerAnsible role /bootstrap-new-vps.shPhase 2 (next to the existing global@anthropic-ai/claude-codeinstall), so every runner host has it. Server op → sys-admin/infra-task. - (fallback) keep
npx wranglerand accept per-invocation download. No change needed.
This plan recommends the Ansible install for determinism, delegated to sys-admin.
5.2 wa-app-api deploy pipeline (follow-up: cross-repo, dev-coder in whatsup-android-chat-puller)
p24-wa-app-api is a Cloudflare Worker living in the separate repo
radieu/whatsup-android-chat-puller — its deploy workflow cannot be created in p24-infra. Spec
(model on .github/workflows/deploy-p24-auth-worker.yml):
- Trigger: push to the worker’s source path on
main(+workflow_dispatch). - Auth:
CLOUDFLARE_API_TOKEN= the scopedCF_WORKERS_EDIT_TOKENfrom §3, delivered as a GH Actions secret in that repo (or decrypted from the scoped SOPS file by the bms-4 worker for a worker-driven deploy). - Step:
npx wrangler deployin the worker directory. - Standard error notification (Discord + GH issue) on failure.
Deliverable: a follow-up issue in radieu/whatsup-android-chat-puller referencing this spec.
6. Class A — human-origination checklist (ships in this PR)
One-time, human-with-panel-access steps. After each paste, the named issue becomes fully autonomous.
| Credential | Where to originate | Exact action | Paste target | Unblocks |
|---|---|---|---|---|
SoYouStart/budget dedicated-server API consumer-key (SYS_INFRA_*) | the budget-brand control panel that actually owns bms-1 (SoYouStart or Kimsufi — verify which; not the standard OVH Manager) — createToken endpoint, e.g. https://eu.api.soyoustart.com/createToken/ | mint token with scope GET/POST/PUT /dedicated/server/*; capture APP_KEY/SECRET/CONSUMER_KEY | sops edit secrets/ovh-sys.env.sops (after §3) | bms-1 rescue/reboot/reinstall automation (#1756/#6149/#6602) |
CF Workers Scripts:Edit token (whatsup account) | Cloudflare dashboard of the whatsup account → My Profile → API Tokens | create token, template “Edit Cloudflare Workers” scoped to wa-app-api | sops edit secrets/cloudflare-workers-bms4.env.sops (after §3) | autonomous wa-app-api deploys |
| Wasabi root key rotation | Wasabi console (no self-rotation API) | rotate per docs/playbooks/wasabi-key-rotation.md §Human-action | existing wasabi-admin-worker.env.sops (secret-manager) | Wasabi root rotation |
7. Runbook safety fix (ships in this PR)
docs/playbooks/bms1-ubuntu-2204-upgrade.md currently instructs the day-of operator to open
“OVH Manager / IPMI-KVM access (server ID 1823494)”. #6618 flagged (live 2026-08-23) that
1823494 is a different server (bms-2, a MongoDB rs0 PRIMARY) and that bms-1’s console lives in
a budget-brand panel, not the standard OVH Manager. Opening rescue/KVM on the wrong server during a
production maintenance window is a real outage risk.
Conservative fix applied here (I cannot verify bms-1’s exact server-ID or exact panel brand without panel/API access — that is a sys-admin/human step, so I do not invent a replacement number):
- Remove the hard-coded server ID
1823494and replace it with a ⚠️ verify-before-KVM step that explicitly warns1823494was found to belong to a different server (bms-2) and must not be used. - Soften “OVH Manager / IPMI-KVM” to “the budget-brand control panel that owns bms-1 (verify: Kimsufi vs SoYouStart — not the standard OVH Manager)”.
- Cross-reference #6618 so the authoritative server-ID/panel confirmation is tracked.
Provenance caveat. The
1823494 == bms-2and “SoYouStart not OVH Manager” claims come from the #6618 live finding (2026-08-23) and were not independently re-verified by this dev-coder worker (no panel/API access in role). The runbook edit is therefore written as a conservative hedge (“do not use1823494; confirm in the panel first”) rather than asserting a specific replacement ID/brand — the authoritative confirmation is a sys-admin/human step tracked on #6618.
8. Regression risks
- §7 runbook edit — docs-only; risk is under-correcting (leaving ambiguity). Mitigated by the explicit “verify in panel, do not use 1823494” warning, which is strictly safer than the current wrong-ID instruction.
- §4 (deferred) — highest-risk item; relaxes prod-protection. Mitigated by the hard
/review-plangate and the anti-self-authorization requirement. - §3 (deferred) — SOPS write; risk of committing a real value or a corrupt SOPS file. Mitigated by placeholder-only + canary-decrypt discipline, owned by secret-manager.
9. Manual test checklist
-
docs/plans/plan-6618-bms4-autonomy-readiness.mdrenders (tables, headings). -
docs/playbooks/bms1-ubuntu-2204-upgrade.mdno longer contains the string1823494as a bms-1 instruction; contains the verify-before-KVM warning. -
/review-planrun ≥3 cycles; risk/security/compliance perspectives addressed for §4. - Follow-up issues created: secret-manager (§3), dev-issue (§4, review-gated), infra-task (§5.1), cross-repo (§5.2).
10. Open questions for /review-plan
- Q1 — remove
SYS_INFRA_*fromovh-api.env.sopswhen addingovh-sys.env.sops(single-home) vs leave both (two-copy drift)? Default: single-home. - Q2 — does the bms-4 worker read the new scoped CF/OVH files via direct
sops -d(nosync-*job) or via CI-deployed plaintext (job needed)? - Q3 — anti-self-authorization allow-list source for §4 (hard-coded /
dev_r_*table / GH team)?
files_to_change (this PR): docs/plans/plan-6618-bms4-autonomy-readiness.md, docs/playbooks/bms1-ubuntu-2204-upgrade.md