feat(platformctl): implement approved apply path #160

Closed
codex wants to merge 4 commits from codex/issues/142-phase3-apply into codex/issues/142-phase3-plan
Collaborator

Canary status: iterating — canary rerun requested after Codex fixes for PR #160 blocker findings

Canary Context Pack

Product story

Packet 3.3 turns platformctl apply from a skeleton into the guarded apply path needed before RS2000 cutover. The owner needs runtime mutation to require a merged PR SHA, sacred-path enforcement, and durable observed-state recording outside module.yaml.

What changed

  • Implemented apply_target for module IDs and existing plan-file artifacts.
  • Added Forgejo merged-PR SHA verification for --approved, accepting both PR head SHA and merge commit SHA for current Actions compatibility.
  • Built docker compose apply command from module.yaml and runs it through TailscaleTransport after sacred path/command checks.
  • Wrote post-success observed state to .platform/state/modules/<id>.status.json, not source manifests.
  • Kept platformctl apply <plan-file> --approved <sha> compatible, while allowing platformctl apply --approved <sha> <module>.
  • Passed FORGEJO_TOKEN into the existing apply workflow's Apply step and set checkout persist-credentials: false.

Why it changed

Phase 4 and Phase 5 depend on platformctl apply being real enough to deploy through the control-plane path instead of manual RS2000 edits.

Files touched

  • control-plane/platformctl/apply.py
  • control-plane/platformctl/cli.py
  • control-plane/platformctl/tests/test_apply_phase3.py
  • control-plane/platformctl/tests/test_forgejo_ci_scripts_contract.py
  • control-plane/forgejo-actions/apply.yaml

Relevant context

  • prompts/codex-cutover-flight/dispatch.md
  • prompts/codex-cutover-flight/phase-3-operational.md Packet 3.3
  • PLATFORM_CHARTER.md observed state layout and apply flow
  • schema/module.schema.md note that last_apply_sha belongs in .platform/state/modules/<id>.status.json
  • docs/forgejo-agent-operations.md

Runtime evidence

No production mutation performed. Tests use fake transport only. Manual refusal smoke:
PYTHONPATH=control-plane python3 -m platformctl.cli apply --approved fake-sha honcho-redis --json returns exit 6 before runtime access.

Known constraints

  • Real apply still requires a valid merged PR SHA and Forgejo token.
  • Full workflow lint across all control-plane/forgejo-actions/ still reports pre-existing findings in plan.yaml/validate.yaml; this PR only reduced apply.yaml to medium findings.
  • apply.yaml still has medium fully-qualified-action lint findings; not expanded here to avoid changing action source semantics inside Packet 3.3.

Explicit out-of-scope

  • No production deploy.
  • No Phase 4 observability deployment.
  • No full CI workflow hardening sweep.
  • No operator merge or approval.
  • No migration of legacy compose paths.

Requested decision

Review whether Packet 3.3 is safe to merge after official canary and external review: approved SHA gate, sacred-path enforcement, status update location, and workflow compatibility.

Merge blockers

  • Any path that allows runtime mutation without merged PR SHA verification.
  • Any sacred path bypass in plan-file or compose command handling.
  • Any secret exposure in logs, argv, PR text, or status files.
  • Any real SSH in tests.

Spec sources read

  • prompts/codex-cutover-flight/dispatch.md — mission and sequencing
  • prompts/codex-cutover-flight/phase-3-operational.md — Packet 3.3 contract
  • docs/forgejo-agent-operations.md — Forgejo identity and PR protocol
  • PLATFORM_CHARTER.md — observed-state and apply flow constraints
  • schema/module.schema.mdlast_apply_sha observed-state location
  • control-plane/platformctl/apply.py — implementation target
  • control-plane/platformctl/safety.py — sacred path contract
  • control-plane/platformctl/cli.py — command wiring
  • control-plane/platformctl/transport/tailscale.py — transport contract
  • control-plane/forgejo-actions/apply.yaml — existing apply workflow compatibility

Tests

  • PYTHONPATH=control-plane python3 -m pytest control-plane/platformctl/tests/test_apply_phase3.py -q — 13 passed
  • PYTHONPATH=control-plane python3 -m pytest control-plane/platformctl/tests/test_apply_phase3.py control-plane/platformctl/tests/test_forgejo_ci_scripts_contract.py -q — 23 passed
  • PYTHONPATH=control-plane python3 -m pytest control-plane/platformctl/tests control-plane/platformctl/transport/tests -q — 339 passed
  • PYTHONPATH=control-plane python3 -m pytest tests -q — 316 passed, 15 skipped
  • PYTHONPATH=control-plane python3 -m platformctl.cli validate modules/honcho-redis --strict-v2 --json — exitCode 0
  • PYTHONPATH=control-plane python3 -m platformctl.cli apply --approved fake-sha honcho-redis --json — exits 6 before runtime access
  • python3 control-plane/platformctl/ci/lint_workflows.py scoped to copied apply.yaml — exit 0, 5 medium findings
  • git diff --check — clean

Refs #142

Canary status: iterating — canary rerun requested after Codex fixes for PR #160 blocker findings ## Canary Context Pack ### Product story Packet 3.3 turns `platformctl apply` from a skeleton into the guarded apply path needed before RS2000 cutover. The owner needs runtime mutation to require a merged PR SHA, sacred-path enforcement, and durable observed-state recording outside `module.yaml`. ### What changed - Implemented `apply_target` for module IDs and existing plan-file artifacts. - Added Forgejo merged-PR SHA verification for `--approved`, accepting both PR head SHA and merge commit SHA for current Actions compatibility. - Built docker compose apply command from `module.yaml` and runs it through `TailscaleTransport` after sacred path/command checks. - Wrote post-success observed state to `.platform/state/modules/<id>.status.json`, not source manifests. - Kept `platformctl apply <plan-file> --approved <sha>` compatible, while allowing `platformctl apply --approved <sha> <module>`. - Passed `FORGEJO_TOKEN` into the existing apply workflow's Apply step and set checkout `persist-credentials: false`. ### Why it changed Phase 4 and Phase 5 depend on `platformctl apply` being real enough to deploy through the control-plane path instead of manual RS2000 edits. ### Files touched - `control-plane/platformctl/apply.py` - `control-plane/platformctl/cli.py` - `control-plane/platformctl/tests/test_apply_phase3.py` - `control-plane/platformctl/tests/test_forgejo_ci_scripts_contract.py` - `control-plane/forgejo-actions/apply.yaml` ### Relevant context - `prompts/codex-cutover-flight/dispatch.md` - `prompts/codex-cutover-flight/phase-3-operational.md` Packet 3.3 - `PLATFORM_CHARTER.md` observed state layout and apply flow - `schema/module.schema.md` note that `last_apply_sha` belongs in `.platform/state/modules/<id>.status.json` - `docs/forgejo-agent-operations.md` ### Runtime evidence No production mutation performed. Tests use fake transport only. Manual refusal smoke: `PYTHONPATH=control-plane python3 -m platformctl.cli apply --approved fake-sha honcho-redis --json` returns exit 6 before runtime access. ### Known constraints - Real apply still requires a valid merged PR SHA and Forgejo token. - Full workflow lint across all `control-plane/forgejo-actions/` still reports pre-existing findings in `plan.yaml`/`validate.yaml`; this PR only reduced `apply.yaml` to medium findings. - `apply.yaml` still has medium fully-qualified-action lint findings; not expanded here to avoid changing action source semantics inside Packet 3.3. ### Explicit out-of-scope - No production deploy. - No Phase 4 observability deployment. - No full CI workflow hardening sweep. - No operator merge or approval. - No migration of legacy compose paths. ### Requested decision Review whether Packet 3.3 is safe to merge after official canary and external review: approved SHA gate, sacred-path enforcement, status update location, and workflow compatibility. ### Merge blockers - Any path that allows runtime mutation without merged PR SHA verification. - Any sacred path bypass in plan-file or compose command handling. - Any secret exposure in logs, argv, PR text, or status files. - Any real SSH in tests. ## Spec sources read - `prompts/codex-cutover-flight/dispatch.md` — mission and sequencing - `prompts/codex-cutover-flight/phase-3-operational.md` — Packet 3.3 contract - `docs/forgejo-agent-operations.md` — Forgejo identity and PR protocol - `PLATFORM_CHARTER.md` — observed-state and apply flow constraints - `schema/module.schema.md` — `last_apply_sha` observed-state location - `control-plane/platformctl/apply.py` — implementation target - `control-plane/platformctl/safety.py` — sacred path contract - `control-plane/platformctl/cli.py` — command wiring - `control-plane/platformctl/transport/tailscale.py` — transport contract - `control-plane/forgejo-actions/apply.yaml` — existing apply workflow compatibility ## Tests - `PYTHONPATH=control-plane python3 -m pytest control-plane/platformctl/tests/test_apply_phase3.py -q` — 13 passed - `PYTHONPATH=control-plane python3 -m pytest control-plane/platformctl/tests/test_apply_phase3.py control-plane/platformctl/tests/test_forgejo_ci_scripts_contract.py -q` — 23 passed - `PYTHONPATH=control-plane python3 -m pytest control-plane/platformctl/tests control-plane/platformctl/transport/tests -q` — 339 passed - `PYTHONPATH=control-plane python3 -m pytest tests -q` — 316 passed, 15 skipped - `PYTHONPATH=control-plane python3 -m platformctl.cli validate modules/honcho-redis --strict-v2 --json` — exitCode 0 - `PYTHONPATH=control-plane python3 -m platformctl.cli apply --approved fake-sha honcho-redis --json` — exits 6 before runtime access - `python3 control-plane/platformctl/ci/lint_workflows.py` scoped to copied `apply.yaml` — exit 0, 5 medium findings - `git diff --check` — clean Refs #142
feat(platformctl): implement approved apply path
All checks were successful
canary-required / collect-diff (pull_request) Successful in 3s
pyfallow / Pyfallow gate (control-plane) (pull_request) Successful in 14s
python-ci / Python 3.11 (pull_request) Successful in 42s
python-ci / Python 3.12 (pull_request) Successful in 42s
python-ci / Python 3.13 (pull_request) Successful in 42s
canary-required / canary (pull_request) Successful in 12s
9951c75785
Author
Collaborator

Packet 3.3 external review checkpoint

Ollama Cloud expensive-review gate: 3/3 approve, no blockers.

Reviewers:

  • deepseek-v4-pro:cloud: APPROVE; blockers none.
  • kimi-k2.6:cloud: APPROVE; blockers none.
  • minimax-m2.7:cloud: APPROVE; blockers none.

Reviewer non-blockers captured:

  • Workflow/code env drift: workflow exports FORGEJO_REPO, while code reads PLATFORMCTL_FORGEJO_REPO; current hardcoded default is pdurlej/platform, so this is not blocking today but should be hardened.
  • Plan file existence is checked before approved-SHA verification; no mutation path, but ordering can be tightened.
  • break_glass_reason is required but not persisted in status/result; audit hardening follow-up.
  • Plan artifact exitCode is recorded but not gated on; manifest-driven execution from the approved checkout mitigates this, but an explicit plan-health gate would harden the contract.
  • Existing workflow lint findings around fully-qualified action pinning remain medium hardening items.

Local verification already recorded in the PR body:

  • 13/13 Packet 3.3 apply tests passed.
  • 23/23 apply + Forgejo CI contract tests passed.
  • 339/339 control-plane + transport tests passed.
  • 316 passed / 15 skipped for root tests suite.
  • honcho-redis strict-v2 validate passed.
  • fake approved SHA apply exits before runtime access.
  • scoped apply workflow lint: 5 medium findings, no high/critical.

Official platform canary remains missing and should still be fired before merge per repo policy.

## Packet 3.3 external review checkpoint Ollama Cloud expensive-review gate: **3/3 approve, no blockers**. Reviewers: - `deepseek-v4-pro:cloud`: APPROVE; blockers none. - `kimi-k2.6:cloud`: APPROVE; blockers none. - `minimax-m2.7:cloud`: APPROVE; blockers none. Reviewer non-blockers captured: - Workflow/code env drift: workflow exports `FORGEJO_REPO`, while code reads `PLATFORMCTL_FORGEJO_REPO`; current hardcoded default is `pdurlej/platform`, so this is not blocking today but should be hardened. - Plan file existence is checked before approved-SHA verification; no mutation path, but ordering can be tightened. - `break_glass_reason` is required but not persisted in status/result; audit hardening follow-up. - Plan artifact `exitCode` is recorded but not gated on; manifest-driven execution from the approved checkout mitigates this, but an explicit plan-health gate would harden the contract. - Existing workflow lint findings around fully-qualified action pinning remain medium hardening items. Local verification already recorded in the PR body: - 13/13 Packet 3.3 apply tests passed. - 23/23 apply + Forgejo CI contract tests passed. - 339/339 control-plane + transport tests passed. - 316 passed / 15 skipped for root `tests` suite. - honcho-redis strict-v2 validate passed. - fake approved SHA apply exits before runtime access. - scoped apply workflow lint: 5 medium findings, no high/critical. Official platform canary remains **missing** and should still be fired before merge per repo policy.
Collaborator

3+3 ensemble review by claude — tech + product hats

One of three independent AI reviewers (claude / codex / glm). See state/reviews/PR-160/decision_packet.md for consolidated risk across all 6 outputs.

Tech hat: OK (confidence 0.70)

Risks

  • medium — Forgejo SHA verification scans only ~500 most-recent closed PRs
    • Evidence: control-plane/platformctl/apply.py:170-194 verify_approved_sha paginates state=closed&limit=50 with default pages=10. Once a repo accumulates >500 closed PRs (state=closed includes merged AND rejected), legitimate older merged SHAs silently fail with the misleading message 'SHA did not match any rec
    • Recommendation: Replace the page-walk with a direct lookup: GET /api/v1/repos/{owner}/{repo}/git/commits/{sha} to confirm the commit exists, then GET /api/v1/repos/{owner}/{repo}/commits/{sha}/pull (Forgejo supports SHA->PR lookup) or fall back to checking reachability from the default branch. Alternatively, surface the pagination cap in the failure message ('scanned last N closed PRs, none matched') so operators

Opportunities

  • Atomic status file write — control-plane/platformctl/apply.py _write_status uses path.write_text directly. A crash mid-write leaves a corrupt JSON status file, which the next apply will fail to parse (caught as EXIT_TOOL_ERROR but still wedges the module). Writing to .tmp + os.replace is a one-line fix that makes status updates crash-safe.
  • Add error-path coverage for verify_approved_sha — control-plane/platformctl/tests/test_apply_phase3.py only tests the happy path of verify_approved_sha (one PR, head + merge SHA match). No coverage for: pagination exhaustion, non-list response from Forgejo, urllib.HTTPError, JSONDecodeError, missing token. These branches all return ApprovalCheck(False, ...) but a regression that flips one to True would not be caught.

Product hat: OK (confidence 0.70)

Risks

  • high — Self-declared canary gap — PR description says 'missing — fire canary 3+3 manually before merge'
    • Evidence: PR description: 'Canary status: missing — fire canary 3+3 manually before merge'. This is the first PR where platformctl applywill execute realdocker compose up on rs2000 if the merged-SHA gate passes, so canary evidence is the only signal you have that the gate behaves as designed end-to-end.
    • Recommendation: Do NOT merge from mobile until canary 3+3 has run and posted evidence. The author flagged this themselves; treat the missing evidence as a hard pre-merge condition rather than a follow-up.
  • medium — Forgejo token env var precedence is a 4-name maze (PLATFORMCTL_FORGEJO_TOKENFORGEJO_TOKENFORGEJO_TOKEN_CODEXCANARY_FORGEJO_TOKEN)
    • Evidence: control-plane/platformctl/apply.py _forgejo_token() checks four env names in order and returns the first hit. If you ever set a token under one name during debugging and forget it, a fresher token elsewhere is silently shadowed — and the failure surfaces as the generic 'approved SHA invalid or not
    • Recommendation: Either (a) collapse to one canonical name with the others as documented fallbacks logged on use, or (b) make the headline error message echo approval.detail so a tired operator sees 'missing Forgejo token' instead of 'SHA invalid or not merged'. Option (b) is the smaller change and higher-value for ADHD debugging.

Opportunities

  • Surface 'already applied on ' in the noop path — When apply returns the idempotent noop because last_apply_sha matches, the result currently says approved SHA already applied. Including last_apply_at and last_apply_pr from the loaded status in the human output would let the operator confirm 'yes I already did this last Tuesday via PR #142' without opening the status file. Cheap win — data is already loaded.
  • Confirm .platform/state/modules/*.status.json is gitignored — Status files are written under repo root (_repo_root_from_modules resolves to the parent of modules/). If .platform/state/ is not in .gitignore, every successful apply will leave a tracked-but-uncommitted file that pollutes git status and tempts the operator into committing runtime state into the source tree. Worth a one-line verification before merge.
<!-- platform-review:claude:pdurlej/platform:PR-160:9951c757 --> # 3+3 ensemble review by `claude` — tech + product hats > One of three independent AI reviewers (claude / codex / glm). See `state/reviews/PR-160/decision_packet.md` for consolidated risk across all 6 outputs. ## Tech hat: ✅ **OK** (confidence 0.70) ### Risks - **`medium`** — Forgejo SHA verification scans only ~500 most-recent closed PRs - Evidence: `control-plane/platformctl/apply.py:170-194 verify_approved_sha paginates state=closed&limit=50 with default pages=10. Once a repo accumulates >500 closed PRs (state=closed includes merged AND rejected), legitimate older merged SHAs silently fail with the misleading message 'SHA did not match any rec` - Recommendation: Replace the page-walk with a direct lookup: GET /api/v1/repos/{owner}/{repo}/git/commits/{sha} to confirm the commit exists, then GET /api/v1/repos/{owner}/{repo}/commits/{sha}/pull (Forgejo supports SHA->PR lookup) or fall back to checking reachability from the default branch. Alternatively, surface the pagination cap in the failure message ('scanned last N closed PRs, none matched') so operators ### Opportunities - **Atomic status file write** — control-plane/platformctl/apply.py _write_status uses path.write_text directly. A crash mid-write leaves a corrupt JSON status file, which the next apply will fail to parse (caught as EXIT_TOOL_ERROR but still wedges the module). Writing to <path>.tmp + os.replace is a one-line fix that makes status updates crash-safe. - **Add error-path coverage for verify_approved_sha** — control-plane/platformctl/tests/test_apply_phase3.py only tests the happy path of verify_approved_sha (one PR, head + merge SHA match). No coverage for: pagination exhaustion, non-list response from Forgejo, urllib.HTTPError, JSONDecodeError, missing token. These branches all return ApprovalCheck(False, ...) but a regression that flips one to True would not be caught. ## Product hat: ✅ **OK** (confidence 0.70) ### Risks - **`high`** — Self-declared canary gap — PR description says 'missing — fire canary 3+3 manually before merge' - Evidence: `PR description: 'Canary status: missing — fire canary 3+3 manually before merge'. This is the first PR where `platformctl apply` will execute real `docker compose up` on rs2000 if the merged-SHA gate passes, so canary evidence is the only signal you have that the gate behaves as designed end-to-end.` - Recommendation: Do NOT merge from mobile until canary 3+3 has run and posted evidence. The author flagged this themselves; treat the missing evidence as a hard pre-merge condition rather than a follow-up. - **`medium`** — Forgejo token env var precedence is a 4-name maze (`PLATFORMCTL_FORGEJO_TOKEN` → `FORGEJO_TOKEN` → `FORGEJO_TOKEN_CODEX` → `CANARY_FORGEJO_TOKEN`) - Evidence: `control-plane/platformctl/apply.py `_forgejo_token()` checks four env names in order and returns the first hit. If you ever set a token under one name during debugging and forget it, a fresher token elsewhere is silently shadowed — and the failure surfaces as the generic 'approved SHA invalid or not` - Recommendation: Either (a) collapse to one canonical name with the others as documented fallbacks logged on use, or (b) make the headline error message echo `approval.detail` so a tired operator sees 'missing Forgejo token' instead of 'SHA invalid or not merged'. Option (b) is the smaller change and higher-value for ADHD debugging. ### Opportunities - **Surface 'already applied on <date>' in the noop path** — When `apply` returns the idempotent noop because `last_apply_sha` matches, the result currently says `approved SHA already applied`. Including `last_apply_at` and `last_apply_pr` from the loaded status in the human output would let the operator confirm 'yes I already did this last Tuesday via PR #142' without opening the status file. Cheap win — data is already loaded. - **Confirm `.platform/state/modules/*.status.json` is gitignored** — Status files are written under repo root (`_repo_root_from_modules` resolves to the parent of `modules/`). If `.platform/state/` is not in `.gitignore`, every successful apply will leave a tracked-but-uncommitted file that pollutes `git status` and tempts the operator into committing runtime state into the source tree. Worth a one-line verification before merge.
Author
Collaborator

3+3 ensemble review by codex — tech + product hats

One of three independent AI reviewers (claude / codex / glm). See state/reviews/PR-160/decision_packet.md for consolidated risk across all 6 outputs.

Tech hat: NOT_OK (confidence 0.88)

Risks

  • blocker — Approved SHA is not bound to the checkout or plan being applied
    • Evidence: control-plane/platformctl/apply.py:123-171 verifies that the supplied SHA belongs to any recent merged PR, but apply_target later loads module.yaml from the current working tree and mutates runtime without checking that current HEAD or the plan artifact was produced by that SHA.
    • Recommendation: Before runtime mutation, require the working tree commit or plan artifact commit metadata to match the approved SHA, or explicitly checkout/read manifests from the approved commit. Refuse dirty/unmatched state before transport.run.
  • high — Plan artifact exitCode is ignored and can still apply
    • Evidence: control-plane/platformctl/apply.py:364-376 records plan_exitCode but does not reject non-zero plan artifacts; a plan with exitCode 1 and no sacred/destructive changes proceeds to compose_up_command and transport.run.
    • Recommendation: Reject plan files whose exitCode is not the expected applyable changes code, and add a regression test proving non-applyable/failed/noisy plan artifacts do not reach transport.
  • medium — Break-glass allows destructive plan changes but then ignores the plan content
    • Evidence: control-plane/platformctl/apply.py:233-259 validates destructive changes only for approval, but apply_target never executes the listed plan changes; it always runs docker compose up for the module at lines 376-388.
    • Recommendation: Either keep plan-file apply non-destructive compose-only and reject destructive plan changes unconditionally in this packet, or implement explicit per-change execution semantics before accepting break-glass.

Product hat: NOT_OK (confidence 0.88)

Risks

  • blocker — Approved SHA is not bound to the module or plan being applied
    • Evidence: control-plane/platformctl/apply.py:103 verifies that the SHA belongs to any recent merged PR; control-plane/platformctl/apply.py:300 then allows that approved_sha to apply any target module.
    • Recommendation: Require the approved SHA to match the specific PR/plan/module being applied, or require plan-file apply only for the workflow path so an unrelated merged PR SHA cannot authorize arbitrary runtime mutation.
  • high — Observed state may be written only to ephemeral runner storage
    • Evidence: control-plane/platformctl/apply.py:56 writes .platform/state/modules/<id>.status.json locally; control-plane/forgejo-actions/apply.yaml:113 only runs Apply and does not persist, upload, commit, or otherwise durably store that status file.
    • Recommendation: Make the durability path explicit before merge: either persist the status artifact in the workflow, commit/push it under the agent identity, or document and test the intended durable storage location.
  • medium — PR exceeds the operator's review-size norm for a runtime mutation path
    • Evidence: Diff stats: +809/-116 across 5 files; platform context sets a <=300 LOC norm and says larger PRs should be flagged for split.
    • Recommendation: Split the approval gate, compose execution path, workflow-token wiring, and observed-state write into smaller PRs unless the orchestrator explicitly records why this must remain one security-sensitive packet.

Opportunities

  • Operator-facing status should name the approving PR — The implementation records last_apply_pr when available. The same PR number should be surfaced prominently in successful JSON/workflow output so Piotr can verify on mobile which approval authorized the runtime change.
<!-- platform-review:codex:pdurlej/platform:PR-160:9951c757 --> # 3+3 ensemble review by `codex` — tech + product hats > One of three independent AI reviewers (claude / codex / glm). See `state/reviews/PR-160/decision_packet.md` for consolidated risk across all 6 outputs. ## Tech hat: ❌ **NOT_OK** (confidence 0.88) ### Risks - **`blocker`** — Approved SHA is not bound to the checkout or plan being applied - Evidence: `control-plane/platformctl/apply.py:123-171 verifies that the supplied SHA belongs to any recent merged PR, but apply_target later loads module.yaml from the current working tree and mutates runtime without checking that current HEAD or the plan artifact was produced by that SHA.` - Recommendation: Before runtime mutation, require the working tree commit or plan artifact commit metadata to match the approved SHA, or explicitly checkout/read manifests from the approved commit. Refuse dirty/unmatched state before transport.run. - **`high`** — Plan artifact exitCode is ignored and can still apply - Evidence: `control-plane/platformctl/apply.py:364-376 records plan_exitCode but does not reject non-zero plan artifacts; a plan with exitCode 1 and no sacred/destructive changes proceeds to compose_up_command and transport.run.` - Recommendation: Reject plan files whose exitCode is not the expected applyable changes code, and add a regression test proving non-applyable/failed/noisy plan artifacts do not reach transport. - **`medium`** — Break-glass allows destructive plan changes but then ignores the plan content - Evidence: `control-plane/platformctl/apply.py:233-259 validates destructive changes only for approval, but apply_target never executes the listed plan changes; it always runs docker compose up for the module at lines 376-388.` - Recommendation: Either keep plan-file apply non-destructive compose-only and reject destructive plan changes unconditionally in this packet, or implement explicit per-change execution semantics before accepting break-glass. ## Product hat: ❌ **NOT_OK** (confidence 0.88) ### Risks - **`blocker`** — Approved SHA is not bound to the module or plan being applied - Evidence: `control-plane/platformctl/apply.py:103 verifies that the SHA belongs to any recent merged PR; control-plane/platformctl/apply.py:300 then allows that approved_sha to apply any target module.` - Recommendation: Require the approved SHA to match the specific PR/plan/module being applied, or require plan-file apply only for the workflow path so an unrelated merged PR SHA cannot authorize arbitrary runtime mutation. - **`high`** — Observed state may be written only to ephemeral runner storage - Evidence: `control-plane/platformctl/apply.py:56 writes .platform/state/modules/<id>.status.json locally; control-plane/forgejo-actions/apply.yaml:113 only runs Apply and does not persist, upload, commit, or otherwise durably store that status file.` - Recommendation: Make the durability path explicit before merge: either persist the status artifact in the workflow, commit/push it under the agent identity, or document and test the intended durable storage location. - **`medium`** — PR exceeds the operator's review-size norm for a runtime mutation path - Evidence: `Diff stats: +809/-116 across 5 files; platform context sets a <=300 LOC norm and says larger PRs should be flagged for split.` - Recommendation: Split the approval gate, compose execution path, workflow-token wiring, and observed-state write into smaller PRs unless the orchestrator explicitly records why this must remain one security-sensitive packet. ### Opportunities - **Operator-facing status should name the approving PR** — The implementation records last_apply_pr when available. The same PR number should be surfaced prominently in successful JSON/workflow output so Piotr can verify on mobile which approval authorized the runtime change.
First-time contributor

3+3 ensemble review by glm — tech + product hats

One of three independent AI reviewers (claude / codex / glm). See state/reviews/PR-160/decision_packet.md for consolidated risk across all 6 outputs.

Tech hat: OK (confidence 0.95)

Risks

  • low — Forgejo token environment variable precedence
    • Evidence: apply.py:115-120 (_forgejo_token)
    • Recommendation: Document the precedence order (CANARY_FORGEJO_TOKEN > FORGEJO_TOKEN > PLATFORMCTL_FORGEJO_TOKEN) to avoid confusion during manual canary runs.
  • medium — Potential token leakage in remote command result
    • Evidence: apply.py:330 (result["remote"]["stdout"])
    • Recommendation: Ensure manifest.host execution context does not echo environment variables. Consider adding a post-transport sanitization pass or documented assumption that remote agents do not echo sensitive env vars.

Opportunities

  • Plan-file mode backward compatibility — Successfully preserved platformctl apply behavior while adding module ID support. The apply_plan wrapper ensures existing integrations continue to function without modification.
  • Comprehensive test coverage for new gates — Tests verify the approval check, sacred path enforcement, and status update idempotency. The use of FakeTransport ensures no real SSH execution occurs during the test suite, satisfying the 'no real SSH in tests' constraint.

Product hat: NOT_OK (confidence 0.95)

Risks

  • high — Secret exposure risk in apply.py helper
    • Evidence: control-plane/platformctl/apply.py:127 - _forgejo_token() checks 'CANARY_FORGEJO_TOKEN' environment variable
    • Recommendation: Remove 'CANARY_FORGEJO_TOKEN' from the fallback list in _forgejo_token() to prevent accidental credential leakage during local testing with a canary token that might be logged or committed.
  • medium — Hardcoded runtime path reduces platform portability
    • Evidence: control-plane/platformctl/apply.py:52 - DEFAULT_RUNTIME_ROOT = '/opt/vps-home-platform-infra'
    • Recommendation: While acceptable for Phase 3, document this as a technical debt item to be externalized to config before Phase 5/RS2000 cutover to avoid breaking if the VPS path changes.
  • medium — Forgejo API pagination limit mismatch risk
    • Evidence: control-plane/platformctl/apply.py:106 - verify_approved_sha() defaults pages=10
    • Recommendation: Add an explicit comment explaining that 10 pages (500 PRs) covers the current operational history. If PR velocity increases, this limit could fail verification for old SHAs, requiring a bump.

Opportunities

  • Clarify break-glass requirement in error message — In apply.py:388, the error 'destructive changes require --break-glass with reason' could explicitly mention that --reason is mandatory. Currently, the check at line 371 enforces it, but the error message at 388 doesn't reiterate the 'with reason' part, potentially confusing an operator who forgot the flag.
<!-- platform-review:glm:pdurlej/platform:PR-160:9951c757 --> # 3+3 ensemble review by `glm` — tech + product hats > One of three independent AI reviewers (claude / codex / glm). See `state/reviews/PR-160/decision_packet.md` for consolidated risk across all 6 outputs. ## Tech hat: ✅ **OK** (confidence 0.95) ### Risks - **`low`** — Forgejo token environment variable precedence - Evidence: `apply.py:115-120 (_forgejo_token)` - Recommendation: Document the precedence order (CANARY_FORGEJO_TOKEN > FORGEJO_TOKEN > PLATFORMCTL_FORGEJO_TOKEN) to avoid confusion during manual canary runs. - **`medium`** — Potential token leakage in remote command result - Evidence: `apply.py:330 (result["remote"]["stdout"])` - Recommendation: Ensure manifest.host execution context does not echo environment variables. Consider adding a post-transport sanitization pass or documented assumption that remote agents do not echo sensitive env vars. ### Opportunities - **Plan-file mode backward compatibility** — Successfully preserved platformctl apply <plan-file> behavior while adding module ID support. The apply_plan wrapper ensures existing integrations continue to function without modification. - **Comprehensive test coverage for new gates** — Tests verify the approval check, sacred path enforcement, and status update idempotency. The use of FakeTransport ensures no real SSH execution occurs during the test suite, satisfying the 'no real SSH in tests' constraint. ## Product hat: ❌ **NOT_OK** (confidence 0.95) ### Risks - **`high`** — Secret exposure risk in apply.py helper - Evidence: `control-plane/platformctl/apply.py:127 - _forgejo_token() checks 'CANARY_FORGEJO_TOKEN' environment variable` - Recommendation: Remove 'CANARY_FORGEJO_TOKEN' from the fallback list in _forgejo_token() to prevent accidental credential leakage during local testing with a canary token that might be logged or committed. - **`medium`** — Hardcoded runtime path reduces platform portability - Evidence: `control-plane/platformctl/apply.py:52 - DEFAULT_RUNTIME_ROOT = '/opt/vps-home-platform-infra'` - Recommendation: While acceptable for Phase 3, document this as a technical debt item to be externalized to config before Phase 5/RS2000 cutover to avoid breaking if the VPS path changes. - **`medium`** — Forgejo API pagination limit mismatch risk - Evidence: `control-plane/platformctl/apply.py:106 - verify_approved_sha() defaults pages=10` - Recommendation: Add an explicit comment explaining that 10 pages (500 PRs) covers the current operational history. If PR velocity increases, this limit could fail verification for old SHAs, requiring a bump. ### Opportunities - **Clarify break-glass requirement in error message** — In apply.py:388, the error 'destructive changes require --break-glass with reason' could explicitly mention that --reason is mandatory. Currently, the check at line 371 enforces it, but the error message at 388 doesn't reiterate the 'with reason' part, potentially confusing an operator who forgot the flag.
Collaborator

Review decision

Status: BLOCKER — recommended action: defer

Single-reviewer blockers

  • Approved SHA is not bound to the checkout or plan being applied
    • Seen by: tech-gpt
    • Decision: Before runtime mutation, require the working tree commit or plan artifact commit metadata to match the approved SHA, or explicitly checkout/read manifests from the approved commit. Refuse dirty/unmatched state before transport.run.
  • Approved SHA is not bound to the module or plan being applied
    • Seen by: product-gpt
    • Decision: Require the approved SHA to match the specific PR/plan/module being applied, or require plan-file apply only for the workflow path so an unrelated merged PR SHA cannot authorize arbitrary runtime mutation.

Single-reviewer high-risk findings

  • Secret exposure risk in apply.py helper
    • Seen by: product-glm
    • Decision: Remove 'CANARY_FORGEJO_TOKEN' from the fallback list in _forgejo_token() to prevent accidental credential leakage during local testing with a canary token that might be logged or committed.
  • Plan artifact exitCode is ignored and can still apply
    • Seen by: tech-gpt
    • Decision: Reject plan files whose exitCode is not the expected applyable changes code, and add a regression test proving non-applyable/failed/noisy plan artifacts do not reach transport.
  • Observed state may be written only to ephemeral runner storage
    • Seen by: product-gpt
    • Decision: Make the durability path explicit before merge: either persist the status artifact in the workflow, commit/push it under the agent identity, or document and test the intended durable storage location.
  • Self-declared canary gap — PR description says 'missing — fire canary 3+3 manually before merge'
    • Seen by: product-claude
    • Decision: Do NOT merge from mobile until canary 3+3 has run and posted evidence. The author flagged this themselves; treat the missing evidence as a hard pre-merge condition rather than a follow-up.

Reviewer dissents

  • product-glm voted NOT_OK (confidence 0.95)
  • tech-gpt voted NOT_OK (confidence 0.88)
  • product-gpt voted NOT_OK (confidence 0.88)

Operator decisions (yes/no)

  1. Risk 'Approved SHA is not bound to the checkout or plan being applied' raised by 1/6: do you accept this risk, or should this PR be deferred until it's mitigated?
  2. Risk 'Approved SHA is not bound to the module or plan being applied' raised by 1/6: do you accept this risk, or should this PR be deferred until it's mitigated?
  3. Risk 'Secret exposure risk in apply.py helper' raised by 1/6: do you accept this risk, or should this PR be deferred until it's mitigated?

Per-actor evidence: see comments by claude, codex, glm above. Tech: 2/3 OK · Product: 1/3 OK.

<!-- platform-review-decision:pdurlej/platform:PR-160:9951c757 --> # Review decision **Status:** BLOCKER — recommended action: `defer` ### Single-reviewer blockers - **Approved SHA is not bound to the checkout or plan being applied** - Seen by: tech-gpt - Decision: Before runtime mutation, require the working tree commit or plan artifact commit metadata to match the approved SHA, or explicitly checkout/read manifests from the approved commit. Refuse dirty/unmatched state before transport.run. - **Approved SHA is not bound to the module or plan being applied** - Seen by: product-gpt - Decision: Require the approved SHA to match the specific PR/plan/module being applied, or require plan-file apply only for the workflow path so an unrelated merged PR SHA cannot authorize arbitrary runtime mutation. ### Single-reviewer high-risk findings - **Secret exposure risk in apply.py helper** - Seen by: product-glm - Decision: Remove 'CANARY_FORGEJO_TOKEN' from the fallback list in _forgejo_token() to prevent accidental credential leakage during local testing with a canary token that might be logged or committed. - **Plan artifact exitCode is ignored and can still apply** - Seen by: tech-gpt - Decision: Reject plan files whose exitCode is not the expected applyable changes code, and add a regression test proving non-applyable/failed/noisy plan artifacts do not reach transport. - **Observed state may be written only to ephemeral runner storage** - Seen by: product-gpt - Decision: Make the durability path explicit before merge: either persist the status artifact in the workflow, commit/push it under the agent identity, or document and test the intended durable storage location. - **Self-declared canary gap — PR description says 'missing — fire canary 3+3 manually before merge'** - Seen by: product-claude - Decision: Do NOT merge from mobile until canary 3+3 has run and posted evidence. The author flagged this themselves; treat the missing evidence as a hard pre-merge condition rather than a follow-up. ### Reviewer dissents - `product-glm` voted **NOT_OK** (confidence 0.95) - `tech-gpt` voted **NOT_OK** (confidence 0.88) - `product-gpt` voted **NOT_OK** (confidence 0.88) ### Operator decisions (yes/no) 1. Risk 'Approved SHA is not bound to the checkout or plan being applied' raised by 1/6: do you accept this risk, or should this PR be deferred until it's mitigated? 2. Risk 'Approved SHA is not bound to the module or plan being applied' raised by 1/6: do you accept this risk, or should this PR be deferred until it's mitigated? 3. Risk 'Secret exposure risk in apply.py helper' raised by 1/6: do you accept this risk, or should this PR be deferred until it's mitigated? --- _Per-actor evidence: see comments by `claude`, `codex`, `glm` above. Tech: 2/3 OK · Product: 1/3 OK._
fix(platformctl): bind apply approval to plan scope
All checks were successful
canary-required / collect-diff (pull_request) Successful in 3s
pyfallow / Pyfallow gate (control-plane) (pull_request) Successful in 16s
python-ci / Python 3.11 (pull_request) Successful in 43s
python-ci / Python 3.12 (pull_request) Successful in 45s
python-ci / Python 3.13 (pull_request) Successful in 43s
canary-required / canary (pull_request) Successful in 11s
cf29e510ac
Collaborator

3+3 ensemble review by claude — tech + product hats

One of three independent AI reviewers (claude / codex / glm). See state/reviews/PR-160/decision_packet.md for consolidated risk across all 6 outputs.

Tech hat: OK (confidence 0.72)

Risks

  • medium — Broken regex in _safe_excerpt makes password redaction unreliable
    • Evidence: control-plane/platformctl/apply.py: _TOKENISH_RE pattern uses (password=)[^\s]+— the raw stringr"\s"produces regex\swhich in a character class means literal backslash and literal letters, not whitespace. Intended pattern is [^\s]+(non-whitespace), written asr"\S"orr"[^\s]+"`
    • Recommendation: Change to r"(?i)(bearer\s+)[a-z0-9._=-]{12,}|(token=)[a-z0-9._=-]{12,}|(password=)\S+" and add a unit test for password=secret123password=<redacted>.
  • medium — Workflow sets FORGEJO_REPO but apply.py reads PLATFORMCTL_FORGEJO_REPO
    • Evidence: control-plane/forgejo-actions/apply.yaml sets FORGEJO_REPO: ${{ github.repository }}on the Apply step. control-plane/platformctl/apply.py:verify_approved_sha readsos.environ.get("PLATFORMCTL_FORGEJO_REPO")only —FORGEJO_REPOis dead. Today this hides becauseDEFAULT_FORGEJO_REPO = "pdurlej`
    • Recommendation: Either rename the workflow env to PLATFORMCTL_FORGEJO_REPO (matching the host knob convention), or also accept FORGEJO_REPO in the env-var fallback chain in apply.py. Same goes for documenting PLATFORMCTL_FORGEJO_HOST in the workflow if you ever move off the default host.
  • low — PR-files pagination cap of 500 can wrongly reject scope-binding for large PRs
    • Evidence: control-plane/platformctl/apply.py:_forgejo_pr_files iterates pages=10×limit=50= 500 files maximum. _validate_approval_binding uses this list to enforcemodules// prefix. If a legitimate merged PR touches 500+ files (rare but possible — schema/CI sweeps, mass renames), the module's mani
    • Recommendation: Either continue paginating until page count is below limit (no upper cap) for the files endpoint, or surface a distinct error when the file list hits the cap so the operator can re-run with a higher pages value rather than seeing a confusing scope-mismatch error.
  • low — Silent skip of approval-binding check when git is unavailable
    • Evidence: control-plane/platformctl/apply.py:_validate_approval_binding gates the checkout-vs-approved-SHA check on current_sha is not None. _current_git_sha returns None on any subprocess failure or when not inside a git checkout. Result: in environments without git on PATH (or with a non-repo cwd), the bi
    • Recommendation: When current_sha is None, log a warning into the result (e.g. result["approval"]["checkout_check"] = "skipped: git unavailable") so operators can see when the binding was not enforced. Optionally fail-closed unless an explicit --no-checkout-binding flag is set.

Opportunities

  • Duplicate _current_git_sha implementations — Near-identical _current_git_sha helpers exist in both control-plane/platformctl/apply.py and control-plane/platformctl/plan.py. Consolidate into a small gitutil module so the hex-validation rules and timeout stay in one place and don't drift.
  • Status file write uses Path.with_suffix on a multi-dot suffix_write_status computes the tmp path via path.with_suffix(path.suffix + ".tmp") for <id>.status.json. This works on current CPython but relies on with_suffix accepting .json.tmp as a suffix replacement — semantics for multi-dot suffixes have shifted between Python versions. path.with_name(path.name + ".tmp") is unambiguous and portable.

Product hat: OK (confidence 0.70)

Risks

  • medium — Observed-state status file is gitignored and ephemeral in CI runners
    • Evidence: .gitignore now ignores .platform/state/; apply.py writes status to repo-root .platform/state/modules/<id>.status.json (apply.py:_status_path / _write_status). In Forgejo Actions this lives in the runner's checkout, which is destroyed when the job ends. PR description claims 'durable observed-state r
    • Recommendation: Pick a durability story before Phase 5 actually deploys: persist .platform/state/ via a Forgejo artifact / cache, push it back via PR (separate write-state job), or document that status.json is local-only and the canonical 'what SHA is applied to honcho-redis' answer lives in the audit log + workflow history. Operator on mobile needs ONE place to look — don't ship a state file that disappears on j

Opportunities

  • Surface last_apply_sha in the state read-out the operator already checks — Once status persistence is settled, fold last_apply_sha + last_apply_at + last_apply_pr into whatever index the operator scans on mobile (e.g. state/INDEX.json or mirror_mirror readout). One glance answers 'is rs2000 honcho-redis running PR #142 yet?' without grepping per-module files.
<!-- platform-review:claude:pdurlej/platform:PR-160:cf29e510 --> # 3+3 ensemble review by `claude` — tech + product hats > One of three independent AI reviewers (claude / codex / glm). See `state/reviews/PR-160/decision_packet.md` for consolidated risk across all 6 outputs. ## Tech hat: ✅ **OK** (confidence 0.72) ### Risks - **`medium`** — Broken regex in _safe_excerpt makes password redaction unreliable - Evidence: `control-plane/platformctl/apply.py: _TOKENISH_RE pattern uses `(password=)[^\\s]+` — the raw string `r"\\s"` produces regex `\\s` which in a character class means literal backslash and literal letter `s`, not whitespace. Intended pattern is `[^\s]+` (non-whitespace), written as `r"\S"` or `r"[^\s]+"` - Recommendation: Change to `r"(?i)(bearer\s+)[a-z0-9._=-]{12,}|(token=)[a-z0-9._=-]{12,}|(password=)\S+"` and add a unit test for `password=secret123` → `password=<redacted>`. - **`medium`** — Workflow sets FORGEJO_REPO but apply.py reads PLATFORMCTL_FORGEJO_REPO - Evidence: `control-plane/forgejo-actions/apply.yaml sets `FORGEJO_REPO: ${{ github.repository }}` on the Apply step. control-plane/platformctl/apply.py:verify_approved_sha reads `os.environ.get("PLATFORMCTL_FORGEJO_REPO")` only — `FORGEJO_REPO` is dead. Today this hides because `DEFAULT_FORGEJO_REPO = "pdurlej` - Recommendation: Either rename the workflow env to `PLATFORMCTL_FORGEJO_REPO` (matching the host knob convention), or also accept `FORGEJO_REPO` in the env-var fallback chain in apply.py. Same goes for documenting `PLATFORMCTL_FORGEJO_HOST` in the workflow if you ever move off the default host. - **`low`** — PR-files pagination cap of 500 can wrongly reject scope-binding for large PRs - Evidence: `control-plane/platformctl/apply.py:_forgejo_pr_files iterates `pages=10` × `limit=50` = 500 files maximum. _validate_approval_binding uses this list to enforce `modules/<id>/` prefix. If a legitimate merged PR touches 500+ files (rare but possible — schema/CI sweeps, mass renames), the module's mani` - Recommendation: Either continue paginating until page count is below `limit` (no upper cap) for the files endpoint, or surface a distinct error when the file list hits the cap so the operator can re-run with a higher pages value rather than seeing a confusing scope-mismatch error. - **`low`** — Silent skip of approval-binding check when git is unavailable - Evidence: `control-plane/platformctl/apply.py:_validate_approval_binding gates the checkout-vs-approved-SHA check on `current_sha is not None`. _current_git_sha returns None on any subprocess failure or when not inside a git checkout. Result: in environments without git on PATH (or with a non-repo cwd), the bi` - Recommendation: When `current_sha` is None, log a warning into the result (e.g. `result["approval"]["checkout_check"] = "skipped: git unavailable"`) so operators can see when the binding was not enforced. Optionally fail-closed unless an explicit `--no-checkout-binding` flag is set. ### Opportunities - **Duplicate _current_git_sha implementations** — Near-identical `_current_git_sha` helpers exist in both control-plane/platformctl/apply.py and control-plane/platformctl/plan.py. Consolidate into a small `gitutil` module so the hex-validation rules and timeout stay in one place and don't drift. - **Status file write uses Path.with_suffix on a multi-dot suffix** — `_write_status` computes the tmp path via `path.with_suffix(path.suffix + ".tmp")` for `<id>.status.json`. This works on current CPython but relies on `with_suffix` accepting `.json.tmp` as a suffix replacement — semantics for multi-dot suffixes have shifted between Python versions. `path.with_name(path.name + ".tmp")` is unambiguous and portable. ## Product hat: ✅ **OK** (confidence 0.70) ### Risks - **`medium`** — Observed-state status file is gitignored and ephemeral in CI runners - Evidence: `.gitignore now ignores .platform/state/; apply.py writes status to repo-root .platform/state/modules/<id>.status.json (apply.py:_status_path / _write_status). In Forgejo Actions this lives in the runner's checkout, which is destroyed when the job ends. PR description claims 'durable observed-state r` - Recommendation: Pick a durability story before Phase 5 actually deploys: persist .platform/state/ via a Forgejo artifact / cache, push it back via PR (separate write-state job), or document that status.json is local-only and the canonical 'what SHA is applied to honcho-redis' answer lives in the audit log + workflow history. Operator on mobile needs ONE place to look — don't ship a state file that disappears on j ### Opportunities - **Surface last_apply_sha in the state read-out the operator already checks** — Once status persistence is settled, fold last_apply_sha + last_apply_at + last_apply_pr into whatever index the operator scans on mobile (e.g. state/INDEX.json or mirror_mirror readout). One glance answers 'is rs2000 honcho-redis running PR #142 yet?' without grepping per-module files.
Author
Collaborator

3+3 ensemble review by codex — tech + product hats

One of three independent AI reviewers (claude / codex / glm). See state/reviews/PR-160/decision_packet.md for consolidated risk across all 6 outputs.

Tech hat: NOT_OK (confidence 0.90)

Risks

  • blocker — Apply workflow can fail before reaching platformctl apply
    • Evidence: control-plane/forgejo-actions/apply.yaml:107-111 appends to .audit/AUDIT_LOG.jsonl, but the repo tracks state/AUDIT_LOG.jsonl and no .audit directory; shell redirection to a missing directory fails the step before the Apply step runs.
    • Recommendation: Create the audit directory before redirection or align the workflow to the intended state/AUDIT_LOG.jsonl path, then add a workflow contract test that the audit target is creatable in a clean checkout.
  • blocker — Remote stderr is redacted in one field but leaked raw in error
    • Evidence: control-plane/platformctl/apply.py:636-643 stores _safe_excerpt(stderr) under remote.stderr, then assigns result["error"] = remote.get("stderr") without redaction; cli.py:40-45 prints the full result JSON.
    • Recommendation: Use the same redacted excerpt for result["error"] on remote failure and add a regression test with token/password-shaped stderr.
  • high — Observed status is not durable in the Actions apply path
    • Evidence: control-plane/platformctl/apply.py:92-97 writes .platform/state/modules/<id>.status.json only in the local checkout; .gitignore ignores .platform/state/, and control-plane/forgejo-actions/apply.yaml:139-169 does not upload, commit, or otherwise persist that file after the job.
    • Recommendation: Define and implement the durable sink for apply status in the workflow, such as a controlled persistent state location or explicit artifact/comment/state publication, and test that a clean subsequent run can observe the previous last_apply_sha.

Product hat: NOT_OK (confidence 0.82)

Risks

  • high — Observed apply state is not actually durable
    • Evidence: .gitignore:2 ignores .platform/state/ while control-plane/platformctl/apply.py writes last_apply_sha only to .platform/state/modules/<id>.status.json; apply.yaml does not persist or upload that state.
    • Recommendation: Before merge, define and implement where this state survives the apply run: commit it intentionally, upload it as an artifact, write it to the runtime host, or document a persistent runner-local state contract and test that path.
  • medium — PR is too large for a runtime-mutation safety change
    • Evidence: Diff stats: +1187/-118 across 7 files, touching approval verification, CLI shape, plan source SHA, workflow secrets, sacred-path enforcement, SSH execution, and state recording in one PR.
    • Recommendation: Split into smaller reviewable PRs unless canary explicitly accepts this as one security-sensitive packet: approval/SHA gate, workflow token wiring, compose execution, and observed-state persistence are separate owner-risk surfaces.

Opportunities

  • Make the owner-visible audit surface explicit — The product promise is safer apply, but the current surface mostly returns JSON and writes hidden local state. A single documented place where Piotr can check 'what was last applied, by which PR, and when' would reduce ADHD memory load and prevent false confidence.
<!-- platform-review:codex:pdurlej/platform:PR-160:cf29e510 --> # 3+3 ensemble review by `codex` — tech + product hats > One of three independent AI reviewers (claude / codex / glm). See `state/reviews/PR-160/decision_packet.md` for consolidated risk across all 6 outputs. ## Tech hat: ❌ **NOT_OK** (confidence 0.90) ### Risks - **`blocker`** — Apply workflow can fail before reaching platformctl apply - Evidence: `control-plane/forgejo-actions/apply.yaml:107-111 appends to .audit/AUDIT_LOG.jsonl, but the repo tracks state/AUDIT_LOG.jsonl and no .audit directory; shell redirection to a missing directory fails the step before the Apply step runs.` - Recommendation: Create the audit directory before redirection or align the workflow to the intended state/AUDIT_LOG.jsonl path, then add a workflow contract test that the audit target is creatable in a clean checkout. - **`blocker`** — Remote stderr is redacted in one field but leaked raw in error - Evidence: `control-plane/platformctl/apply.py:636-643 stores _safe_excerpt(stderr) under remote.stderr, then assigns result["error"] = remote.get("stderr") without redaction; cli.py:40-45 prints the full result JSON.` - Recommendation: Use the same redacted excerpt for result["error"] on remote failure and add a regression test with token/password-shaped stderr. - **`high`** — Observed status is not durable in the Actions apply path - Evidence: `control-plane/platformctl/apply.py:92-97 writes .platform/state/modules/<id>.status.json only in the local checkout; .gitignore ignores .platform/state/, and control-plane/forgejo-actions/apply.yaml:139-169 does not upload, commit, or otherwise persist that file after the job.` - Recommendation: Define and implement the durable sink for apply status in the workflow, such as a controlled persistent state location or explicit artifact/comment/state publication, and test that a clean subsequent run can observe the previous last_apply_sha. ## Product hat: ❌ **NOT_OK** (confidence 0.82) ### Risks - **`high`** — Observed apply state is not actually durable - Evidence: `.gitignore:2 ignores .platform/state/ while control-plane/platformctl/apply.py writes last_apply_sha only to .platform/state/modules/<id>.status.json; apply.yaml does not persist or upload that state.` - Recommendation: Before merge, define and implement where this state survives the apply run: commit it intentionally, upload it as an artifact, write it to the runtime host, or document a persistent runner-local state contract and test that path. - **`medium`** — PR is too large for a runtime-mutation safety change - Evidence: `Diff stats: +1187/-118 across 7 files, touching approval verification, CLI shape, plan source SHA, workflow secrets, sacred-path enforcement, SSH execution, and state recording in one PR.` - Recommendation: Split into smaller reviewable PRs unless canary explicitly accepts this as one security-sensitive packet: approval/SHA gate, workflow token wiring, compose execution, and observed-state persistence are separate owner-risk surfaces. ### Opportunities - **Make the owner-visible audit surface explicit** — The product promise is safer apply, but the current surface mostly returns JSON and writes hidden local state. A single documented place where Piotr can check 'what was last applied, by which PR, and when' would reduce ADHD memory load and prevent false confidence.
First-time contributor

3+3 ensemble review by glm — tech + product hats

One of three independent AI reviewers (claude / codex / glm). See state/reviews/PR-160/decision_packet.md for consolidated risk across all 6 outputs.

Tech hat: OK (confidence 0.95)

Risks

  • low — Subprocess environment inherits secrets
    • Evidence: control-plane/platformctl/apply.py:180, control-plane/platformctl/plan.py:60
    • Recommendation: subprocess.run calls in _current_git_sha and potentially other places inherit the parent environment by default. While git is trusted, best practice for secret hygiene is to pass an explicit environment dict (e.g., env={}) to prevent accidental secret leakage into child processes.

Product hat: OK (confidence 0.95)

Risks

  • high — Missing canary execution record
    • Evidence: PR Description: 'Canary status: missing — fire canary 3+3 manually before merge'
    • Recommendation: Do not merge until the manual canary (3+3) has been executed and recorded as passed in the PR comments or CI logs.
  • medium — Secret token exposure risk in apply workflow
    • Evidence: control-plane/forgejo-actions/apply.yaml:116-117 (FORGEJO_TOKEN passed as env var)
    • Recommendation: Verify that the runner environment does not leak FORGEJO_TOKEN into logs. The persist-credentials: false on checkout is good, but ensure no set -x or debug logging in the apply script leaks the token.

Opportunities

  • Reduce cognitive load on apply failures — The error messages for EXIT_APPROVAL_MISMATCH are detailed (e.g., 'did not change modules/honcho-redis/'). This is excellent for the operator to diagnose PR scope issues quickly without digging into Forgejo UI manually.
  • Idempotency prevents unnecessary attention cost — The check for last_apply_sha == approved_sha returning noop (exit 0) means re-running a successful apply doesn't require anxiety or attention. This fits the 'robustness' goal for a single-operator platform.
<!-- platform-review:glm:pdurlej/platform:PR-160:cf29e510 --> # 3+3 ensemble review by `glm` — tech + product hats > One of three independent AI reviewers (claude / codex / glm). See `state/reviews/PR-160/decision_packet.md` for consolidated risk across all 6 outputs. ## Tech hat: ✅ **OK** (confidence 0.95) ### Risks - **`low`** — Subprocess environment inherits secrets - Evidence: `control-plane/platformctl/apply.py:180, control-plane/platformctl/plan.py:60` - Recommendation: `subprocess.run` calls in `_current_git_sha` and potentially other places inherit the parent environment by default. While `git` is trusted, best practice for secret hygiene is to pass an explicit environment dict (e.g., `env={}`) to prevent accidental secret leakage into child processes. ## Product hat: ✅ **OK** (confidence 0.95) ### Risks - **`high`** — Missing canary execution record - Evidence: `PR Description: 'Canary status: missing — fire canary 3+3 manually before merge'` - Recommendation: Do not merge until the manual canary (3+3) has been executed and recorded as passed in the PR comments or CI logs. - **`medium`** — Secret token exposure risk in apply workflow - Evidence: `control-plane/forgejo-actions/apply.yaml:116-117 (FORGEJO_TOKEN passed as env var)` - Recommendation: Verify that the runner environment does not leak `FORGEJO_TOKEN` into logs. The `persist-credentials: false` on checkout is good, but ensure no `set -x` or debug logging in the apply script leaks the token. ### Opportunities - **Reduce cognitive load on apply failures** — The error messages for `EXIT_APPROVAL_MISMATCH` are detailed (e.g., 'did not change modules/honcho-redis/'). This is excellent for the operator to diagnose PR scope issues quickly without digging into Forgejo UI manually. - **Idempotency prevents unnecessary attention cost** — The check for `last_apply_sha == approved_sha` returning `noop` (exit 0) means re-running a successful apply doesn't require anxiety or attention. This fits the 'robustness' goal for a single-operator platform.
Collaborator

Review decision

Status: BLOCKER — recommended action: defer

Single-reviewer blockers

  • Apply workflow can fail before reaching platformctl apply
    • Seen by: tech-gpt
    • Decision: Create the audit directory before redirection or align the workflow to the intended state/AUDIT_LOG.jsonl path, then add a workflow contract test that the audit target is creatable in a clean checkout.
  • Remote stderr is redacted in one field but leaked raw in error
    • Seen by: tech-gpt
    • Decision: Use the same redacted excerpt for result["error"] on remote failure and add a regression test with token/password-shaped stderr.

Single-reviewer high-risk findings

  • Observed apply state is not actually durable
    • Seen by: product-gpt
    • Decision: Before merge, define and implement where this state survives the apply run: commit it intentionally, upload it as an artifact, write it to the runtime host, or document a persistent runner-local state contract and test that path.
  • Missing canary execution record
    • Seen by: product-glm
    • Decision: Do not merge until the manual canary (3+3) has been executed and recorded as passed in the PR comments or CI logs.
  • Observed status is not durable in the Actions apply path
    • Seen by: tech-gpt
    • Decision: Define and implement the durable sink for apply status in the workflow, such as a controlled persistent state location or explicit artifact/comment/state publication, and test that a clean subsequent run can observe the previous last_apply_sha.

Reviewer dissents

  • product-gpt voted NOT_OK (confidence 0.82)
  • tech-gpt voted NOT_OK (confidence 0.90)

Operator decisions (yes/no)

  1. Risk 'Apply workflow can fail before reaching platformctl apply' raised by 1/6: do you accept this risk, or should this PR be deferred until it's mitigated?
  2. Risk 'Remote stderr is redacted in one field but leaked raw in error' raised by 1/6: do you accept this risk, or should this PR be deferred until it's mitigated?
  3. Risk 'Observed apply state is not actually durable' raised by 1/6: do you accept this risk, or should this PR be deferred until it's mitigated?

Per-actor evidence: see comments by claude, codex, glm above. Tech: 2/3 OK · Product: 2/3 OK.

<!-- platform-review-decision:pdurlej/platform:PR-160:cf29e510 --> # Review decision **Status:** BLOCKER — recommended action: `defer` ### Single-reviewer blockers - **Apply workflow can fail before reaching platformctl apply** - Seen by: tech-gpt - Decision: Create the audit directory before redirection or align the workflow to the intended state/AUDIT_LOG.jsonl path, then add a workflow contract test that the audit target is creatable in a clean checkout. - **Remote stderr is redacted in one field but leaked raw in error** - Seen by: tech-gpt - Decision: Use the same redacted excerpt for result["error"] on remote failure and add a regression test with token/password-shaped stderr. ### Single-reviewer high-risk findings - **Observed apply state is not actually durable** - Seen by: product-gpt - Decision: Before merge, define and implement where this state survives the apply run: commit it intentionally, upload it as an artifact, write it to the runtime host, or document a persistent runner-local state contract and test that path. - **Missing canary execution record** - Seen by: product-glm - Decision: Do not merge until the manual canary (3+3) has been executed and recorded as passed in the PR comments or CI logs. - **Observed status is not durable in the Actions apply path** - Seen by: tech-gpt - Decision: Define and implement the durable sink for apply status in the workflow, such as a controlled persistent state location or explicit artifact/comment/state publication, and test that a clean subsequent run can observe the previous last_apply_sha. ### Reviewer dissents - `product-gpt` voted **NOT_OK** (confidence 0.82) - `tech-gpt` voted **NOT_OK** (confidence 0.90) ### Operator decisions (yes/no) 1. Risk 'Apply workflow can fail before reaching platformctl apply' raised by 1/6: do you accept this risk, or should this PR be deferred until it's mitigated? 2. Risk 'Remote stderr is redacted in one field but leaked raw in error' raised by 1/6: do you accept this risk, or should this PR be deferred until it's mitigated? 3. Risk 'Observed apply state is not actually durable' raised by 1/6: do you accept this risk, or should this PR be deferred until it's mitigated? --- _Per-actor evidence: see comments by `claude`, `codex`, `glm` above. Tech: 2/3 OK · Product: 2/3 OK._
fix(platformctl): harden apply workflow evidence
All checks were successful
canary-required / collect-diff (pull_request) Successful in 4s
pyfallow / Pyfallow gate (control-plane) (pull_request) Successful in 15s
python-ci / Python 3.11 (pull_request) Successful in 43s
python-ci / Python 3.12 (pull_request) Successful in 43s
python-ci / Python 3.13 (pull_request) Successful in 43s
canary-required / canary (pull_request) Successful in 11s
8eba363ca9
Collaborator

3+3 ensemble review by claude — tech + product hats

One of three independent AI reviewers (claude / codex / glm). See state/reviews/PR-160/decision_packet.md for consolidated risk across all 6 outputs.

Tech hat: OK (confidence 0.70)

Risks

  • medium — PR file enumeration capped at 500 entries can falsely reject valid applies
    • Evidence: control-plane/platformctl/apply.py _forgejo_pr_files (default pages=10, limit=50). _module_changed_by_approved_pr scans only approval.changed_files; a large PR (>500 changed files) whose modules//entries fall past page 10 will return False and trip thedid not change modules// rejecti
    • Recommendation: Either iterate until the API returns <50 (already coded but only up to pages cap), raise the cap to a generous number, or only fetch the file list when the changed-files check is needed and short-circuit positive on first hit. Test with a synthetic >500-file PR fixture.
  • medium — _safe_excerpt redaction misses common secret formats in remote stdout/stderr
    • Evidence: control-plane/platformctl/apply.py:_TOKENISH_RE only catches bearer , token=, password=. Output like "api_key":"...", AWS-style AKIA..., raw 40-char hex creds, Authorization: Basic ..., or environment dumps MY_SECRET=... pass through unredacted into result.remote.stdout/stderr
    • Recommendation: Either tighten the regex (cover (api[_-]?key|secret|credential)\s*[:=]\s*\S+, base64-blob length thresholds, common cloud key prefixes) or — safer — drop remote stdout/stderr from artifacts entirely and only persist returncode + a structured result. compose-up shouldn't need stdout in the persisted status.
  • low — Idempotency noop check is TOCTOU against concurrent applies
    • Evidence: control-plane/platformctl/apply.py: _load_statusthen later_write_statuswith no advisory lock. Two concurrentplatformctl apply --approved runs (e.g. CI re-trigger or workflow retry) can both pass thelast_apply_sha == approved_shanoop check, both invoketransport.run, bo
    • Recommendation: Take an exclusive flock on <repo>/.platform/state/modules/<id>.lock around the read→ssh→write block, or treat the workflow concurrency: group as the source of truth and document that local apply is single-runner. Either way, cite the strategy in apply.py so reviewers know which side enforces.

Opportunities

  • Status file records last_apply_sha on compose up -d success without verifying container health — control-plane/platformctl/apply.py writes last_apply_sha and last_apply_at whenever transport.run returncode==0. docker compose up -d <service> returns 0 as soon as container creation succeeds; a service that immediately crash-loops still leaves the status file claiming the SHA was applied. Acknowledged Phase 4 scope (smoke_test), but a 1-line `docker compose ps --status running --format j
  • Plan source_sha does not check working tree cleanliness — control-plane/platformctl/plan.py:_current_git_sha returns HEAD regardless of git status --porcelain. A plan generated from a dirty checkout records source_sha=HEAD but reflects uncommitted content; on apply the plan_source_sha→approved_sha equality check would still pass even though the plan artifact does not match what was reviewed in the PR. Low-impact in CI (clean checkout) but worth either

Product hat: OK (confidence 0.75)

Risks

  • medium — Merging flips apply from skeleton to live; operator should explicitly acknowledge first real-mutation path
    • Evidence: control-plane/platformctl/apply.py: removes Phase-05 gate, replaces 'not-implemented-yet' return with TailscaleTransport.run() that executes docker compose up -d on rs2000. control-plane/forgejo-actions/apply.yaml lines 109-138 now wire the token + PR number through to a real SSH-executing apply ste
    • Recommendation: Before merging, post a one-line comment confirming this is the intended go-live moment for the control-plane apply path. After merge, the next merged module-PR is no longer a no-op — pick that first PR deliberately (e.g., honcho-redis with no spec change) so the first live apply is observable, not incidental.
  • low — Token resolution leaks an agent identity (FORGEJO_TOKEN_CODEX) into the apply tool
    • Evidence: control-plane/platformctl/apply.py _forgejo_token() iterates ('PLATFORMCTL_FORGEJO_TOKEN', 'FORGEJO_TOKEN_CODEX', 'FORGEJO_TOKEN'). The middle name encodes 'this token belongs to the codex agent' inside platformctl, which should be agent-agnostic.
    • Recommendation: Drop FORGEJO_TOKEN_CODEX from the precedence list before the next agent (claude/glm/etc.) needs the same favor; let agents export PLATFORMCTL_FORGEJO_TOKEN in their wrapper instead. Not a merge blocker — fold into the next apply.py touch.

Opportunities

  • status.json is the right substrate for a future platformctl status <module> view — The new .platform/state/modules/.status.json carries last_apply_sha, last_apply_pr, last_apply_at, last_apply_host, last_apply_returncode. That is exactly what an operator-visible 'when was this module last touched and by which PR' lookup would need. Worth noting for the Phase 4 observability packet so the file format is treated as a published surface, not an internal artifact, before consumer
<!-- platform-review:claude:pdurlej/platform:PR-160:8eba363c --> # 3+3 ensemble review by `claude` — tech + product hats > One of three independent AI reviewers (claude / codex / glm). See `state/reviews/PR-160/decision_packet.md` for consolidated risk across all 6 outputs. ## Tech hat: ✅ **OK** (confidence 0.70) ### Risks - **`medium`** — PR file enumeration capped at 500 entries can falsely reject valid applies - Evidence: `control-plane/platformctl/apply.py _forgejo_pr_files (default pages=10, limit=50). _module_changed_by_approved_pr scans only `approval.changed_files`; a large PR (>500 changed files) whose `modules/<id>/` entries fall past page 10 will return False and trip the `did not change modules/<id>/` rejecti` - Recommendation: Either iterate until the API returns <50 (already coded but only up to `pages` cap), raise the cap to a generous number, or only fetch the file list when the changed-files check is needed and short-circuit positive on first hit. Test with a synthetic >500-file PR fixture. - **`medium`** — _safe_excerpt redaction misses common secret formats in remote stdout/stderr - Evidence: `control-plane/platformctl/apply.py:_TOKENISH_RE only catches `bearer <X>`, `token=<X>`, `password=<X>`. Output like `"api_key":"..."`, AWS-style `AKIA...`, raw 40-char hex creds, `Authorization: Basic ...`, or environment dumps `MY_SECRET=...` pass through unredacted into result.remote.stdout/stderr` - Recommendation: Either tighten the regex (cover `(api[_-]?key|secret|credential)\s*[:=]\s*\S+`, base64-blob length thresholds, common cloud key prefixes) or — safer — drop remote stdout/stderr from artifacts entirely and only persist returncode + a structured result. compose-up shouldn't need stdout in the persisted status. - **`low`** — Idempotency noop check is TOCTOU against concurrent applies - Evidence: `control-plane/platformctl/apply.py: `_load_status` then later `_write_status` with no advisory lock. Two concurrent `platformctl apply --approved <sha> <module>` runs (e.g. CI re-trigger or workflow retry) can both pass the `last_apply_sha == approved_sha` noop check, both invoke `transport.run`, bo` - Recommendation: Take an exclusive flock on `<repo>/.platform/state/modules/<id>.lock` around the read→ssh→write block, or treat the workflow `concurrency:` group as the source of truth and document that local apply is single-runner. Either way, cite the strategy in apply.py so reviewers know which side enforces. ### Opportunities - **Status file records last_apply_sha on `compose up -d` success without verifying container health** — control-plane/platformctl/apply.py writes `last_apply_sha` and `last_apply_at` whenever `transport.run` returncode==0. `docker compose up -d <service>` returns 0 as soon as container creation succeeds; a service that immediately crash-loops still leaves the status file claiming the SHA was applied. Acknowledged Phase 4 scope (smoke_test), but a 1-line `docker compose ps --status running --format j - **Plan source_sha does not check working tree cleanliness** — control-plane/platformctl/plan.py:_current_git_sha returns HEAD regardless of `git status --porcelain`. A plan generated from a dirty checkout records source_sha=HEAD but reflects uncommitted content; on apply the plan_source_sha→approved_sha equality check would still pass even though the plan artifact does not match what was reviewed in the PR. Low-impact in CI (clean checkout) but worth either ## Product hat: ✅ **OK** (confidence 0.75) ### Risks - **`medium`** — Merging flips apply from skeleton to live; operator should explicitly acknowledge first real-mutation path - Evidence: `control-plane/platformctl/apply.py: removes Phase-05 gate, replaces 'not-implemented-yet' return with TailscaleTransport.run() that executes docker compose up -d on rs2000. control-plane/forgejo-actions/apply.yaml lines 109-138 now wire the token + PR number through to a real SSH-executing apply ste` - Recommendation: Before merging, post a one-line comment confirming this is the intended go-live moment for the control-plane apply path. After merge, the next merged module-PR is no longer a no-op — pick that first PR deliberately (e.g., honcho-redis with no spec change) so the first live apply is observable, not incidental. - **`low`** — Token resolution leaks an agent identity (FORGEJO_TOKEN_CODEX) into the apply tool - Evidence: `control-plane/platformctl/apply.py _forgejo_token() iterates ('PLATFORMCTL_FORGEJO_TOKEN', 'FORGEJO_TOKEN_CODEX', 'FORGEJO_TOKEN'). The middle name encodes 'this token belongs to the codex agent' inside platformctl, which should be agent-agnostic.` - Recommendation: Drop FORGEJO_TOKEN_CODEX from the precedence list before the next agent (claude/glm/etc.) needs the same favor; let agents export PLATFORMCTL_FORGEJO_TOKEN in their wrapper instead. Not a merge blocker — fold into the next apply.py touch. ### Opportunities - **status.json is the right substrate for a future `platformctl status <module>` view** — The new .platform/state/modules/<id>.status.json carries last_apply_sha, last_apply_pr, last_apply_at, last_apply_host, last_apply_returncode. That is exactly what an operator-visible 'when was this module last touched and by which PR' lookup would need. Worth noting for the Phase 4 observability packet so the file format is treated as a published surface, not an internal artifact, before consumer
Author
Collaborator

3+3 ensemble review by codex — tech + product hats

One of three independent AI reviewers (claude / codex / glm). See state/reviews/PR-160/decision_packet.md for consolidated risk across all 6 outputs.

Tech hat: NOT_OK (confidence 0.86)

Risks

  • blocker — Apply rejects approved runtime-only PRs unless module.yaml changed
    • Evidence: control-plane/platformctl/apply.py:_validate_approval_binding rejects any approved PR whose file list does not include modules/<module_id>/
    • Recommendation: Bind apply to the actual plan/module scope instead of requiring the approving PR to edit the module directory; at minimum allow plan artifacts whose source_sha matches the approved PR and whose plan module matches the requested module.
  • high — Plan-file apply can execute a compose up even when the plan has zero changes
    • Evidence: control-plane/platformctl/apply.py: apply_target sets actions_planned from plan changes but always calls compose_up_command(manifest) after _validate_plan_binding accepts exitCode 0 or 1
    • Recommendation: For plan-file targets, return no-op when exitCode is 0 or changes is empty; only run compose for an applyable non-empty plan.
  • medium — Idempotency check uses approved_sha only, not the normalized applied PR reference
    • Evidence: control-plane/platformctl/apply.py: status['last_apply_sha'] is compared only to approved_sha, while approval accepts both PR head SHA and merge_commit_sha
    • Recommendation: Store and compare a canonical approval identity such as PR number plus both head_sha and merge_sha, or treat either approved ref from the same PR as already applied.

Product hat: NOT_OK (confidence 0.86)

Risks

  • high — Observed apply state is not durable
    • Evidence: .gitignore adds .platform/state/; apply.pywriteslast_apply_shaonly to local.platform/state/modules/.status.json; apply.yaml uploads it as an artifact but never restores or commits it
    • Recommendation: Block merge until the PR defines the durable source for observed apply state and makes platformctl apply read it on later runs, or explicitly narrows Packet 3.3 to non-durable runner-local state.
  • medium — PR exceeds safe review surface for runtime mutation
    • Evidence: Diff stats are +1217/-120 across 7 files for the first real runtime apply path
    • Recommendation: Split into smaller PRs: approval/SHA binding, compose command/sacred-path enforcement, workflow wiring, and observed-state persistence. This is too much trust-boundary change for one mobile operator approval.

Opportunities

  • Make the operator-facing apply record findable — If the final design keeps apply state as a Forgejo artifact, name the artifact and PR comment/output so Piotr can find the last applied module/SHA without inspecting runner internals.
<!-- platform-review:codex:pdurlej/platform:PR-160:8eba363c --> # 3+3 ensemble review by `codex` — tech + product hats > One of three independent AI reviewers (claude / codex / glm). See `state/reviews/PR-160/decision_packet.md` for consolidated risk across all 6 outputs. ## Tech hat: ❌ **NOT_OK** (confidence 0.86) ### Risks - **`blocker`** — Apply rejects approved runtime-only PRs unless module.yaml changed - Evidence: `control-plane/platformctl/apply.py:_validate_approval_binding rejects any approved PR whose file list does not include modules/<module_id>/` - Recommendation: Bind apply to the actual plan/module scope instead of requiring the approving PR to edit the module directory; at minimum allow plan artifacts whose source_sha matches the approved PR and whose plan module matches the requested module. - **`high`** — Plan-file apply can execute a compose up even when the plan has zero changes - Evidence: `control-plane/platformctl/apply.py: apply_target sets actions_planned from plan changes but always calls compose_up_command(manifest) after _validate_plan_binding accepts exitCode 0 or 1` - Recommendation: For plan-file targets, return no-op when exitCode is 0 or changes is empty; only run compose for an applyable non-empty plan. - **`medium`** — Idempotency check uses approved_sha only, not the normalized applied PR reference - Evidence: `control-plane/platformctl/apply.py: status['last_apply_sha'] is compared only to approved_sha, while approval accepts both PR head SHA and merge_commit_sha` - Recommendation: Store and compare a canonical approval identity such as PR number plus both head_sha and merge_sha, or treat either approved ref from the same PR as already applied. ## Product hat: ❌ **NOT_OK** (confidence 0.86) ### Risks - **`high`** — Observed apply state is not durable - Evidence: `.gitignore adds `.platform/state/`; `apply.py` writes `last_apply_sha` only to local `.platform/state/modules/<id>.status.json`; `apply.yaml` uploads it as an artifact but never restores or commits it` - Recommendation: Block merge until the PR defines the durable source for observed apply state and makes `platformctl apply` read it on later runs, or explicitly narrows Packet 3.3 to non-durable runner-local state. - **`medium`** — PR exceeds safe review surface for runtime mutation - Evidence: `Diff stats are +1217/-120 across 7 files for the first real runtime apply path` - Recommendation: Split into smaller PRs: approval/SHA binding, compose command/sacred-path enforcement, workflow wiring, and observed-state persistence. This is too much trust-boundary change for one mobile operator approval. ### Opportunities - **Make the operator-facing apply record findable** — If the final design keeps apply state as a Forgejo artifact, name the artifact and PR comment/output so Piotr can find the last applied module/SHA without inspecting runner internals.
First-time contributor

3+3 ensemble review by glm — tech + product hats

One of three independent AI reviewers (claude / codex / glm). See state/reviews/PR-160/decision_packet.md for consolidated risk across all 6 outputs.

Tech hat: OK (confidence 0.95)

Risks

  • low — Minor exit code inconsistency in error message
    • Evidence: control-plane/platformctl/apply.py:619
    • Recommendation: The error message says 'exitCode': 6 but actual returned exit_code is EXIT_APPROVAL_MISMATCH (constant value 5). The code correctly returns EXIT_APPROVAL_MISMATCH, only the JSON dict has a mismatched error message value. Not a blocker as the actual exit code is correct.
  • low — Forgejo PR files pagination limit (500 files)
    • Evidence: control-plane/platformctl/apply.py:168 - default pages=10 with limit=50
    • Recommendation: Current defaults allow 500 PR files. If ever needed, increase pages parameter or implement auto-detection of last page. Not a blocker for current scale.

Opportunities

  • Atomic status file writes using rename pattern — control-plane/platformctl/apply.py:87 already implements the write-to-tmp-then-rename pattern for status files. This is good practice preventing partial writes on crash/tail.
  • Secure token handling in Forgejo workflow — control-plane/forgejo-actions/apply.yaml:114-116 correctly passes FORGEJO_TOKEN only via env to platformctl, persist-credentials: false is set on checkout. No secret exposure in logs.
  • Approval gate blocks before any transport — control-plane/platformctl/apply.py:382-390 - approval check happens early, returning EXIT_APPROVAL_MISMATCH before any TailscaleTransport.run(). This matches the 'runtime mutation without merged PR SHA verification' blocker requirement.

Product hat: ABSTAIN (confidence 0.00)

Risks

  • medium — reviewer output unparseable
    • Evidence: no valid JSON object found in response
    • Recommendation: investigate provider response; possibly increase max_tokens or retry
<!-- platform-review:glm:pdurlej/platform:PR-160:8eba363c --> # 3+3 ensemble review by `glm` — tech + product hats > One of three independent AI reviewers (claude / codex / glm). See `state/reviews/PR-160/decision_packet.md` for consolidated risk across all 6 outputs. ## Tech hat: ✅ **OK** (confidence 0.95) ### Risks - **`low`** — Minor exit code inconsistency in error message - Evidence: `control-plane/platformctl/apply.py:619` - Recommendation: The error message says 'exitCode': 6 but actual returned exit_code is EXIT_APPROVAL_MISMATCH (constant value 5). The code correctly returns EXIT_APPROVAL_MISMATCH, only the JSON dict has a mismatched error message value. Not a blocker as the actual exit code is correct. - **`low`** — Forgejo PR files pagination limit (500 files) - Evidence: `control-plane/platformctl/apply.py:168 - default pages=10 with limit=50` - Recommendation: Current defaults allow 500 PR files. If ever needed, increase pages parameter or implement auto-detection of last page. Not a blocker for current scale. ### Opportunities - **Atomic status file writes using rename pattern** — control-plane/platformctl/apply.py:87 already implements the write-to-tmp-then-rename pattern for status files. This is good practice preventing partial writes on crash/tail. - **Secure token handling in Forgejo workflow** — control-plane/forgejo-actions/apply.yaml:114-116 correctly passes FORGEJO_TOKEN only via env to platformctl, persist-credentials: false is set on checkout. No secret exposure in logs. - **Approval gate blocks before any transport** — control-plane/platformctl/apply.py:382-390 - approval check happens early, returning EXIT_APPROVAL_MISMATCH before any TailscaleTransport.run(). This matches the 'runtime mutation without merged PR SHA verification' blocker requirement. ## Product hat: ⚪ **ABSTAIN** (confidence 0.00) ### Risks - **`medium`** — reviewer output unparseable - Evidence: `no valid JSON object found in response` - Recommendation: investigate provider response; possibly increase max_tokens or retry
Collaborator

Review decision

Status: BLOCKER — recommended action: defer

Single-reviewer blockers

  • Apply rejects approved runtime-only PRs unless module.yaml changed
    • Seen by: tech-gpt
    • Decision: Bind apply to the actual plan/module scope instead of requiring the approving PR to edit the module directory; at minimum allow plan artifacts whose source_sha matches the approved PR and whose plan module matches the requested module.

Single-reviewer high-risk findings

  • Observed apply state is not durable
    • Seen by: product-gpt
    • Decision: Block merge until the PR defines the durable source for observed apply state and makes platformctl apply read it on later runs, or explicitly narrows Packet 3.3 to non-durable runner-local state.
  • Plan-file apply can execute a compose up even when the plan has zero changes
    • Seen by: tech-gpt
    • Decision: For plan-file targets, return no-op when exitCode is 0 or changes is empty; only run compose for an applyable non-empty plan.

Reviewer dissents

  • product-gpt voted NOT_OK (confidence 0.86)
  • tech-gpt voted NOT_OK (confidence 0.86)
  • product-glm voted ABSTAIN (confidence 0.00)

Operator decisions (yes/no)

  1. Risk 'Apply rejects approved runtime-only PRs unless module.yaml changed' raised by 1/6: do you accept this risk, or should this PR be deferred until it's mitigated?
  2. Risk 'Observed apply state is not durable' raised by 1/6: do you accept this risk, or should this PR be deferred until it's mitigated?
  3. Risk 'Plan-file apply can execute a compose up even when the plan has zero changes' raised by 1/6: do you accept this risk, or should this PR be deferred until it's mitigated?

Per-actor evidence: see comments by claude, codex, glm above. Tech: 2/3 OK · Product: 1/3 OK.

<!-- platform-review-decision:pdurlej/platform:PR-160:8eba363c --> # Review decision **Status:** BLOCKER — recommended action: `defer` ### Single-reviewer blockers - **Apply rejects approved runtime-only PRs unless module.yaml changed** - Seen by: tech-gpt - Decision: Bind apply to the actual plan/module scope instead of requiring the approving PR to edit the module directory; at minimum allow plan artifacts whose source_sha matches the approved PR and whose plan module matches the requested module. ### Single-reviewer high-risk findings - **Observed apply state is not durable** - Seen by: product-gpt - Decision: Block merge until the PR defines the durable source for observed apply state and makes `platformctl apply` read it on later runs, or explicitly narrows Packet 3.3 to non-durable runner-local state. - **Plan-file apply can execute a compose up even when the plan has zero changes** - Seen by: tech-gpt - Decision: For plan-file targets, return no-op when exitCode is 0 or changes is empty; only run compose for an applyable non-empty plan. ### Reviewer dissents - `product-gpt` voted **NOT_OK** (confidence 0.86) - `tech-gpt` voted **NOT_OK** (confidence 0.86) - `product-glm` voted **ABSTAIN** (confidence 0.00) ### Operator decisions (yes/no) 1. Risk 'Apply rejects approved runtime-only PRs unless module.yaml changed' raised by 1/6: do you accept this risk, or should this PR be deferred until it's mitigated? 2. Risk 'Observed apply state is not durable' raised by 1/6: do you accept this risk, or should this PR be deferred until it's mitigated? 3. Risk 'Plan-file apply can execute a compose up even when the plan has zero changes' raised by 1/6: do you accept this risk, or should this PR be deferred until it's mitigated? --- _Per-actor evidence: see comments by `claude`, `codex`, `glm` above. Tech: 2/3 OK · Product: 1/3 OK._
fix(platformctl): make plan apply no-op safe
All checks were successful
canary-required / collect-diff (pull_request) Successful in 3s
pyfallow / Pyfallow gate (control-plane) (pull_request) Successful in 14s
python-ci / Python 3.11 (pull_request) Successful in 43s
python-ci / Python 3.12 (pull_request) Successful in 44s
python-ci / Python 3.13 (pull_request) Successful in 43s
canary-required / canary (pull_request) Successful in 13s
6d1f570c3c
Collaborator

3+3 ensemble review by claude — tech + product hats

One of three independent AI reviewers (claude / codex / glm). See state/reviews/PR-160/decision_packet.md for consolidated risk across all 6 outputs.

Tech hat: OK (confidence 0.70)

Risks

  • high — Checkout-vs-approved-SHA binding has zero test coverage
    • Evidence: control-plane/platformctl/apply.py:591-592 — enforce_checkout_bindingdefaults to True only whenapproval_checker is None. Every test in test_apply_phase3.py injects approval_checker=approve, so _current_git_sha(repo_root) is never compared against approved-SHA refs in tests. test_apply_succ
    • Recommendation: Add a test that initializes a real git repo in tmp_path (subprocess git init, write file, commit, capture HEAD), then calls apply_target(..., approval_checker=None, ...) (or explicit enforce_checkout_binding=True) with (a) approved_sha == HEAD → success, (b) approved_sha != HEAD → EXIT_APPROVAL_MISMATCH with approved SHA does not match current checkout. Without this, the gate is implementa
  • medium — Module-only apply requires module.yaml change, but compose-file-only PRs deploy outside that path
    • Evidence: control-plane/platformctl/apply.py:425-431 (_module_changed_by_approved_prcheckspath.startswith(f"modules/{module_id}/")). Honcho-redis manifest declares compose_file: compose/apps/compose.yaml(outsidemodules/honcho-redis/). A PR that only edits compose/apps/compose.yaml (e.g., image b
    • Recommendation: Either (a) extend the scope check to also accept the manifest's declared compose_file path, or (b) document explicitly that compose-only changes require platformctl plan ... -o plan.json && platformctl apply plan.json rather than module-id apply. The current behaviour will surprise operators on the first real RS2000 deploy.
  • medium — Audit log path silently moved from .audit/ to state/
    • Evidence: control-plane/forgejo-actions/apply.yaml:108-112 and 144-148 — >> .audit/AUDIT_LOG.jsonlchanged to>> state/AUDIT_LOG.jsonlin both audit_start and audit_finish steps; the closing PR comment was already updated to referencestate/AUDIT_LOG.jsonl. The PR description and packet contract describ
    • Recommendation: Either revert (out of scope for Packet 3.3) or call out the move explicitly in the description and in the rejection-as-learning-log. Worth a .gitignore entry for state/AUDIT_LOG.jsonl so the runner's local file doesn't surface in working-tree diffs.
  • medium — PR exceeds 300-LOC charter norm by ~4×
    • Evidence: Diff stats +1294/-120; apply.py alone goes from 137 to 715 lines. Per platform charter, larger PRs should be flagged for split rather than merged with concerns.
    • Recommendation: Split candidates: (1) Forgejo merged-PR verification + ApprovalCheck + plan.source_sha capture; (2) module-target resolution + compose_up_command + sacred path layering; (3) status persistence + workflow wiring (token env, persist-credentials, artifact upload). If the operator decides one bundle is preferable for canary cohesion, document the exception in the decision comment so the norm doesn't s

Opportunities

  • Status writer is not concurrency-safe on tmp suffix — control-plane/platformctl/apply.py:_write_status uses a fixed <id>.status.json.tmp filename. Two concurrent applies for the same module (or even an interrupted-then-resumed apply) would race the .replace(). Not a current scenario (Forgejo workflow serializes), but a one-line fix (tmp = path.with_name(f"{path.name}.{os.getpid()}.tmp")) makes it safe for any future parallel runner.
  • Duplicated _current_git_sha implementation in plan.py and apply.py — Identical (modulo the post-strip lowercase) helpers at plan.py:53 and apply.py:368. A shared helper in a small git.py module would prevent drift — e.g., the apply.py copy lowercases the SHA, the plan.py copy preserves case before validating, which is a subtle inconsistency that could matter if Forgejo ever returns mixed-case SHAs in API responses (current Gitea/Forgejo always lowercases, so harm

Product hat: OK (confidence 0.72)

Risks

  • medium — PR size (~1.3k LOC) exceeds the ≤300 LOC norm
    • Evidence: Diff stats +1294/-120; apply.py alone grows by ~595 LOC plus 528 LOC of new tests. Charter calls for splitting at this size.
    • Recommendation: Merge this one given the apply path is genuinely tightly coupled (approval → binding → compose → status writer all need each other to be safe), but flag in the merge note that future packets in this area must be smaller. If anything legitimately ships separately, the audit-log path move and .gitignore/workflow-token wiring could have been a precursor PR.
  • low — Audit log path silently moved from .audit/ to state/AUDIT_LOG.jsonl
    • Evidence: control-plane/forgejo-actions/apply.yaml lines 109, 145 — >> .audit/AUDIT_LOG.jsonlbecomes>> state/AUDIT_LOG.jsonl. The change isn't called out in the PR description except indirectly in the PR comment template, and there's no migration note for any prior .audit/ content.
    • Recommendation: Confirm nothing else (dashboards, ntfy templates, future audit collectors) reads .audit/AUDIT_LOG.jsonl before merge. If .audit/ was never written to in practice this is a no-op — but worth a one-line confirmation in the merge comment so future-you doesn't go hunting for missing audit history.

Opportunities

  • Surface the apply status file path in the ntfy/PR-comment template — The status artifact path .platform/state/modules/<id>.status.json is now the durable record of what was applied, but the apply workflow's PR comment only mentions state/AUDIT_LOG.jsonl and the artifact name. For a tired operator approving on mobile, including last_apply_sha/last_apply_at directly in the ntfy or PR comment (read out of the status file) would remove the click-through into th
<!-- platform-review:claude:pdurlej/platform:PR-160:6d1f570c --> # 3+3 ensemble review by `claude` — tech + product hats > One of three independent AI reviewers (claude / codex / glm). See `state/reviews/PR-160/decision_packet.md` for consolidated risk across all 6 outputs. ## Tech hat: ✅ **OK** (confidence 0.70) ### Risks - **`high`** — Checkout-vs-approved-SHA binding has zero test coverage - Evidence: `control-plane/platformctl/apply.py:591-592 — `enforce_checkout_binding` defaults to True only when `approval_checker is None`. Every test in test_apply_phase3.py injects `approval_checker=approve`, so `_current_git_sha(repo_root)` is never compared against approved-SHA refs in tests. test_apply_succ` - Recommendation: Add a test that initializes a real git repo in tmp_path (subprocess `git init`, write file, commit, capture HEAD), then calls `apply_target(..., approval_checker=None, ...)` (or explicit `enforce_checkout_binding=True`) with (a) approved_sha == HEAD → success, (b) approved_sha != HEAD → EXIT_APPROVAL_MISMATCH with `approved SHA does not match current checkout`. Without this, the gate is implementa - **`medium`** — Module-only apply requires module.yaml change, but compose-file-only PRs deploy outside that path - Evidence: `control-plane/platformctl/apply.py:425-431 (`_module_changed_by_approved_pr` checks `path.startswith(f"modules/{module_id}/")`). Honcho-redis manifest declares `compose_file: compose/apps/compose.yaml` (outside `modules/honcho-redis/`). A PR that only edits `compose/apps/compose.yaml` (e.g., image b` - Recommendation: Either (a) extend the scope check to also accept the manifest's declared `compose_file` path, or (b) document explicitly that compose-only changes require `platformctl plan ... -o plan.json && platformctl apply plan.json` rather than module-id apply. The current behaviour will surprise operators on the first real RS2000 deploy. - **`medium`** — Audit log path silently moved from .audit/ to state/ - Evidence: `control-plane/forgejo-actions/apply.yaml:108-112 and 144-148 — `>> .audit/AUDIT_LOG.jsonl` changed to `>> state/AUDIT_LOG.jsonl` in both audit_start and audit_finish steps; the closing PR comment was already updated to reference `state/AUDIT_LOG.jsonl`. The PR description and packet contract describ` - Recommendation: Either revert (out of scope for Packet 3.3) or call out the move explicitly in the description and in the rejection-as-learning-log. Worth a `.gitignore` entry for `state/AUDIT_LOG.jsonl` so the runner's local file doesn't surface in working-tree diffs. - **`medium`** — PR exceeds 300-LOC charter norm by ~4× - Evidence: `Diff stats +1294/-120; apply.py alone goes from 137 to 715 lines. Per platform charter, larger PRs should be flagged for split rather than merged with concerns.` - Recommendation: Split candidates: (1) Forgejo merged-PR verification + ApprovalCheck + plan.source_sha capture; (2) module-target resolution + compose_up_command + sacred path layering; (3) status persistence + workflow wiring (token env, persist-credentials, artifact upload). If the operator decides one bundle is preferable for canary cohesion, document the exception in the decision comment so the norm doesn't s ### Opportunities - **Status writer is not concurrency-safe on tmp suffix** — control-plane/platformctl/apply.py:_write_status uses a fixed `<id>.status.json.tmp` filename. Two concurrent applies for the same module (or even an interrupted-then-resumed apply) would race the .replace(). Not a current scenario (Forgejo workflow serializes), but a one-line fix (`tmp = path.with_name(f"{path.name}.{os.getpid()}.tmp")`) makes it safe for any future parallel runner. - **Duplicated _current_git_sha implementation in plan.py and apply.py** — Identical (modulo the post-strip lowercase) helpers at plan.py:53 and apply.py:368. A shared helper in a small `git.py` module would prevent drift — e.g., the apply.py copy lowercases the SHA, the plan.py copy preserves case before validating, which is a subtle inconsistency that could matter if Forgejo ever returns mixed-case SHAs in API responses (current Gitea/Forgejo always lowercases, so harm ## Product hat: ✅ **OK** (confidence 0.72) ### Risks - **`medium`** — PR size (~1.3k LOC) exceeds the ≤300 LOC norm - Evidence: `Diff stats +1294/-120; apply.py alone grows by ~595 LOC plus 528 LOC of new tests. Charter calls for splitting at this size.` - Recommendation: Merge this one given the apply path is genuinely tightly coupled (approval → binding → compose → status writer all need each other to be safe), but flag in the merge note that future packets in this area must be smaller. If anything legitimately ships separately, the audit-log path move and `.gitignore`/workflow-token wiring could have been a precursor PR. - **`low`** — Audit log path silently moved from `.audit/` to `state/AUDIT_LOG.jsonl` - Evidence: `control-plane/forgejo-actions/apply.yaml lines 109, 145 — `>> .audit/AUDIT_LOG.jsonl` becomes `>> state/AUDIT_LOG.jsonl`. The change isn't called out in the PR description except indirectly in the PR comment template, and there's no migration note for any prior `.audit/` content.` - Recommendation: Confirm nothing else (dashboards, ntfy templates, future audit collectors) reads `.audit/AUDIT_LOG.jsonl` before merge. If `.audit/` was never written to in practice this is a no-op — but worth a one-line confirmation in the merge comment so future-you doesn't go hunting for missing audit history. ### Opportunities - **Surface the apply status file path in the ntfy/PR-comment template** — The status artifact path `.platform/state/modules/<id>.status.json` is now the durable record of what was applied, but the apply workflow's PR comment only mentions `state/AUDIT_LOG.jsonl` and the artifact name. For a tired operator approving on mobile, including `last_apply_sha`/`last_apply_at` directly in the ntfy or PR comment (read out of the status file) would remove the click-through into th
Author
Collaborator

3+3 ensemble review by codex — tech + product hats

One of three independent AI reviewers (claude / codex / glm). See state/reviews/PR-160/decision_packet.md for consolidated risk across all 6 outputs.

Tech hat: NOT_OK (confidence 0.90)

Risks

  • blocker — Plan-file apply is not bound to the approved PR’s changed files
    • Evidence: control-plane/platformctl/apply.py:_validate_approval_binding skips module changed-file enforcement when plan_bound=True, and _validate_plan_binding trusts user-controlled plan.source_sha
    • Recommendation: For plan-file applies, require the approved PR file list to contain the module path, the plan artifact path, or another explicit trusted artifact binding. Do not let an arbitrary local JSON plan bypass module/PR scope just by setting source_sha to a merged SHA.
  • medium — Approved SHA search can miss valid older merged PRs and block apply
    • Evidence: control-plane/platformctl/apply.py:verify_approved_sha scans only the first 10 pages of closed PRs unless --approved-pr is supplied
    • Recommendation: Require --approved-pr for real apply, or fail with a clear error when it is absent instead of relying on a bounded recent-PR scan. Keep the scan only as a compatibility/helper path if needed.

Product hat: NOT_OK (confidence 0.90)

Risks

  • blocker — Split oversized runtime-mutation PR before approval
    • Evidence: Diff stats: +1294/-120 across 7 files; PR implements approval verification, plan binding, SSH apply, status persistence, CLI changes, workflow credentials, and tests in one change.
    • Recommendation: Do not merge as-is. Split into smaller PRs under the platform norm: approval/PR binding, compose apply execution, observed-state persistence/workflow artifact handling, and tests/contracts.
  • high — Observed state may be hidden in expiring artifacts
    • Evidence: .gitignore adds .platform/state/; control-plane/forgejo-actions/apply.yamluploads.platform/state/modules/*.status.jsononly asplatform-apply-state-${{ github.event.inputs.pr_number }} artifact.
    • Recommendation: Clarify and implement the durable owner-visible source of observed apply state before merge, or explicitly downgrade the claim from durable state to transient CI artifact.
<!-- platform-review:codex:pdurlej/platform:PR-160:6d1f570c --> # 3+3 ensemble review by `codex` — tech + product hats > One of three independent AI reviewers (claude / codex / glm). See `state/reviews/PR-160/decision_packet.md` for consolidated risk across all 6 outputs. ## Tech hat: ❌ **NOT_OK** (confidence 0.90) ### Risks - **`blocker`** — Plan-file apply is not bound to the approved PR’s changed files - Evidence: `control-plane/platformctl/apply.py:_validate_approval_binding skips module changed-file enforcement when plan_bound=True, and _validate_plan_binding trusts user-controlled plan.source_sha` - Recommendation: For plan-file applies, require the approved PR file list to contain the module path, the plan artifact path, or another explicit trusted artifact binding. Do not let an arbitrary local JSON plan bypass module/PR scope just by setting source_sha to a merged SHA. - **`medium`** — Approved SHA search can miss valid older merged PRs and block apply - Evidence: `control-plane/platformctl/apply.py:verify_approved_sha scans only the first 10 pages of closed PRs unless --approved-pr is supplied` - Recommendation: Require --approved-pr for real apply, or fail with a clear error when it is absent instead of relying on a bounded recent-PR scan. Keep the scan only as a compatibility/helper path if needed. ## Product hat: ❌ **NOT_OK** (confidence 0.90) ### Risks - **`blocker`** — Split oversized runtime-mutation PR before approval - Evidence: `Diff stats: +1294/-120 across 7 files; PR implements approval verification, plan binding, SSH apply, status persistence, CLI changes, workflow credentials, and tests in one change.` - Recommendation: Do not merge as-is. Split into smaller PRs under the platform norm: approval/PR binding, compose apply execution, observed-state persistence/workflow artifact handling, and tests/contracts. - **`high`** — Observed state may be hidden in expiring artifacts - Evidence: `.gitignore adds `.platform/state/`; `control-plane/forgejo-actions/apply.yaml` uploads `.platform/state/modules/*.status.json` only as `platform-apply-state-${{ github.event.inputs.pr_number }}` artifact.` - Recommendation: Clarify and implement the durable owner-visible source of observed apply state before merge, or explicitly downgrade the claim from durable state to transient CI artifact.
First-time contributor

3+3 ensemble review by glm — tech + product hats

One of three independent AI reviewers (claude / codex / glm). See state/reviews/PR-160/decision_packet.md for consolidated risk across all 6 outputs.

Tech hat: OK (confidence 0.95)

Risks

  • medium — apply.py exceeds 300 LOC norm
    • Evidence: control-plane/platformctl/apply.py: +663/-43 (706 lines total)
    • Recommendation: Flag for potential Phase 4 split: separate Forgejo client, transport layer, and core apply logic into focused modules. Not a blocker for this PR as the implementation is self-contained and necessary for RS2000 cutover.
  • low — PR file list pagination may miss module changes in very large PRs
    • Evidence: control-plane/platformctl/apply.py:192 (_forgejo_pr_files pages default=10, limit=50 → 500 files max)
    • Recommendation: Monitor for false rejections on PRs with >500 changed files. If encountered, increase pages parameter or fetch until empty page. Edge case unlikely for homelab scale.

Opportunities

  • Deduplicate _current_git_sha between apply.py and plan.py — Both files implement identical subprocess-based git SHA extraction. Consider extracting to platformctl/gitutils.py to avoid drift.

Product hat: OK (confidence 0.90)

Risks

  • low — Apply state location not yet ignored
    • Evidence: .gitignore shows .platform/state/ added, but operator needs to know this dir is for runtime state only, not source manifests.
    • Recommendation: Add a README.md in .platform/state/ root explaining 'DO NOT EDIT - runtime observed state only'.
  • low — Error exit codes mismatch for 'noop' vs 'applied'
    • Evidence: apply.py returns EXIT_NO_CHANGES (0) for both noop plan (in-sync) and successful apply.
    • Recommendation: Consider a distinct exit code for 'applied' (success with mutations) vs 'noop' (no mutations) to help automation detect actual change, though current behavior is not harmful.

Opportunities

  • Status file location is clear — Correctly writes to .platform/state/modules/<id>.status.json, keeping observed state separate from source module.yaml as per charter.
  • Forgejo SHA verification is robust — Accepts both head SHA and merge_commit_sha, matching current Actions behavior and avoiding a merge blocker.
<!-- platform-review:glm:pdurlej/platform:PR-160:6d1f570c --> # 3+3 ensemble review by `glm` — tech + product hats > One of three independent AI reviewers (claude / codex / glm). See `state/reviews/PR-160/decision_packet.md` for consolidated risk across all 6 outputs. ## Tech hat: ✅ **OK** (confidence 0.95) ### Risks - **`medium`** — apply.py exceeds 300 LOC norm - Evidence: `control-plane/platformctl/apply.py: +663/-43 (706 lines total)` - Recommendation: Flag for potential Phase 4 split: separate Forgejo client, transport layer, and core apply logic into focused modules. Not a blocker for this PR as the implementation is self-contained and necessary for RS2000 cutover. - **`low`** — PR file list pagination may miss module changes in very large PRs - Evidence: `control-plane/platformctl/apply.py:192 (_forgejo_pr_files pages default=10, limit=50 → 500 files max)` - Recommendation: Monitor for false rejections on PRs with >500 changed files. If encountered, increase pages parameter or fetch until empty page. Edge case unlikely for homelab scale. ### Opportunities - **Deduplicate _current_git_sha between apply.py and plan.py** — Both files implement identical subprocess-based git SHA extraction. Consider extracting to platformctl/gitutils.py to avoid drift. ## Product hat: ✅ **OK** (confidence 0.90) ### Risks - **`low`** — Apply state location not yet ignored - Evidence: `.gitignore shows `.platform/state/` added, but operator needs to know this dir is for runtime state only, not source manifests.` - Recommendation: Add a README.md in .platform/state/ root explaining 'DO NOT EDIT - runtime observed state only'. - **`low`** — Error exit codes mismatch for 'noop' vs 'applied' - Evidence: `apply.py returns EXIT_NO_CHANGES (0) for both noop plan (in-sync) and successful apply.` - Recommendation: Consider a distinct exit code for 'applied' (success with mutations) vs 'noop' (no mutations) to help automation detect actual change, though current behavior is not harmful. ### Opportunities - **Status file location is clear** — Correctly writes to `.platform/state/modules/<id>.status.json`, keeping observed state separate from source `module.yaml` as per charter. - **Forgejo SHA verification is robust** — Accepts both head SHA and merge_commit_sha, matching current Actions behavior and avoiding a merge blocker.
Collaborator

Review decision

Status: BLOCKER — recommended action: defer

Single-reviewer blockers

  • Plan-file apply is not bound to the approved PR’s changed files
    • Seen by: tech-gpt
    • Decision: For plan-file applies, require the approved PR file list to contain the module path, the plan artifact path, or another explicit trusted artifact binding. Do not let an arbitrary local JSON plan bypass module/PR scope just by setting source_sha to a merged SHA.
  • Split oversized runtime-mutation PR before approval
    • Seen by: product-gpt
    • Decision: Do not merge as-is. Split into smaller PRs under the platform norm: approval/PR binding, compose apply execution, observed-state persistence/workflow artifact handling, and tests/contracts.

Single-reviewer high-risk findings

  • Observed state may be hidden in expiring artifacts
    • Seen by: product-gpt
    • Decision: Clarify and implement the durable owner-visible source of observed apply state before merge, or explicitly downgrade the claim from durable state to transient CI artifact.
  • Checkout-vs-approved-SHA binding has zero test coverage
    • Seen by: tech-claude
    • Decision: Add a test that initializes a real git repo in tmp_path (subprocess git init, write file, commit, capture HEAD), then calls apply_target(..., approval_checker=None, ...) (or explicit enforce_checkout_binding=True) with (a) approved_sha == HEAD → success, (b) approved_sha != HEAD → EXIT_APPROVA

Reviewer dissents

  • tech-gpt voted NOT_OK (confidence 0.90)
  • product-gpt voted NOT_OK (confidence 0.90)

Operator decisions (yes/no)

  1. Risk 'Plan-file apply is not bound to the approved PR’s changed files' raised by 1/6: do you accept this risk, or should this PR be deferred until it's mitigated?
  2. Risk 'Split oversized runtime-mutation PR before approval' raised by 1/6: do you accept this risk, or should this PR be deferred until it's mitigated?
  3. Risk 'Observed state may be hidden in expiring artifacts' raised by 1/6: do you accept this risk, or should this PR be deferred until it's mitigated?

Per-actor evidence: see comments by claude, codex, glm above. Tech: 2/3 OK · Product: 2/3 OK.

<!-- platform-review-decision:pdurlej/platform:PR-160:6d1f570c --> # Review decision **Status:** BLOCKER — recommended action: `defer` ### Single-reviewer blockers - **Plan-file apply is not bound to the approved PR’s changed files** - Seen by: tech-gpt - Decision: For plan-file applies, require the approved PR file list to contain the module path, the plan artifact path, or another explicit trusted artifact binding. Do not let an arbitrary local JSON plan bypass module/PR scope just by setting source_sha to a merged SHA. - **Split oversized runtime-mutation PR before approval** - Seen by: product-gpt - Decision: Do not merge as-is. Split into smaller PRs under the platform norm: approval/PR binding, compose apply execution, observed-state persistence/workflow artifact handling, and tests/contracts. ### Single-reviewer high-risk findings - **Observed state may be hidden in expiring artifacts** - Seen by: product-gpt - Decision: Clarify and implement the durable owner-visible source of observed apply state before merge, or explicitly downgrade the claim from durable state to transient CI artifact. - **Checkout-vs-approved-SHA binding has zero test coverage** - Seen by: tech-claude - Decision: Add a test that initializes a real git repo in tmp_path (subprocess `git init`, write file, commit, capture HEAD), then calls `apply_target(..., approval_checker=None, ...)` (or explicit `enforce_checkout_binding=True`) with (a) approved_sha == HEAD → success, (b) approved_sha != HEAD → EXIT_APPROVA ### Reviewer dissents - `tech-gpt` voted **NOT_OK** (confidence 0.90) - `product-gpt` voted **NOT_OK** (confidence 0.90) ### Operator decisions (yes/no) 1. Risk 'Plan-file apply is not bound to the approved PR’s changed files' raised by 1/6: do you accept this risk, or should this PR be deferred until it's mitigated? 2. Risk 'Split oversized runtime-mutation PR before approval' raised by 1/6: do you accept this risk, or should this PR be deferred until it's mitigated? 3. Risk 'Observed state may be hidden in expiring artifacts' raised by 1/6: do you accept this risk, or should this PR be deferred until it's mitigated? --- _Per-actor evidence: see comments by `claude`, `codex`, `glm` above. Tech: 2/3 OK · Product: 2/3 OK._
Author
Collaborator

Codex status after canary rerun on commit 6d1f570: terminal recommendation is split_pr, not another in-place iteration.

Why:

  • Official canary remains BLOCKER/defer.
  • Remaining blocker class shifted from fixable single bugs to PR shape/scope: Split oversized runtime-mutation PR before approval.
  • This is security-sensitive runtime mutation work; continuing to add code to the same PR would violate the narrow-PR rule and hard review cap.

Current useful fixes already pushed on this branch:

  • approval SHA tied to explicit PR number / checkout / plan source_sha
  • plan no-change now returns no-op before remote compose
  • remote stderr error path redacted
  • audit path fixed to state/AUDIT_LOG.jsonl
  • apply state uploaded as platform-apply-state-<pr> artifact
  • tests passing locally: control-plane/transport 345 passed, root tests 316 passed / 15 skipped

Recommended next action: split #160 into smaller PRs: approval binding, plan artifact metadata, compose apply execution, workflow evidence/artifacts. Do not merge #160 as-is.

Codex status after canary rerun on commit `6d1f570`: terminal recommendation is `split_pr`, not another in-place iteration. Why: - Official canary remains `BLOCKER/defer`. - Remaining blocker class shifted from fixable single bugs to PR shape/scope: `Split oversized runtime-mutation PR before approval`. - This is security-sensitive runtime mutation work; continuing to add code to the same PR would violate the narrow-PR rule and hard review cap. Current useful fixes already pushed on this branch: - approval SHA tied to explicit PR number / checkout / plan `source_sha` - plan no-change now returns no-op before remote compose - remote stderr error path redacted - audit path fixed to `state/AUDIT_LOG.jsonl` - apply state uploaded as `platform-apply-state-<pr>` artifact - tests passing locally: control-plane/transport `345 passed`, root tests `316 passed / 15 skipped` Recommended next action: split #160 into smaller PRs: approval binding, plan artifact metadata, compose apply execution, workflow evidence/artifacts. Do not merge #160 as-is.
Author
Collaborator

Superseded by logical split PRs:

  • #162 approval binding only
  • #163 plan source provenance
  • #164 approved no-op semantics
  • #165 compose execution primitive
  • #166 apply status artifacts
  • #167 Forgejo Actions wiring

Reason: canary on this oversized PR correctly identified that approval binding, plan provenance, runtime mutation, evidence durability, and workflow token wiring should be reviewed as separate security-sensitive invariants. Closing this PR; do not merge #160.

Superseded by logical split PRs: - #162 approval binding only - #163 plan source provenance - #164 approved no-op semantics - #165 compose execution primitive - #166 apply status artifacts - #167 Forgejo Actions wiring Reason: canary on this oversized PR correctly identified that approval binding, plan provenance, runtime mutation, evidence durability, and workflow token wiring should be reviewed as separate security-sensitive invariants. Closing this PR; do not merge #160.
codex closed this pull request 2026-05-10 23:29:31 +02:00
All checks were successful
canary-required / collect-diff (pull_request) Successful in 3s
pyfallow / Pyfallow gate (control-plane) (pull_request) Successful in 14s
python-ci / Python 3.11 (pull_request) Successful in 43s
python-ci / Python 3.12 (pull_request) Successful in 44s
python-ci / Python 3.13 (pull_request) Successful in 43s
canary-required / canary (pull_request) Successful in 13s

Pull request closed

Sign in to join this conversation.
No reviewers
No labels
W6d-automerge-calibration
agent/claude-code
agent/codex
agent/hermes
agent/iskra
agent/ollama
agent/patchwarden
automerge-candidate
class/security-sensitive
cutover-gate
dependency/blocked
dependency/blocks-others
dependency/cross-repo
dependency/needs-confirmation
domain:agents
domain:ci
domain:docs
domain:forgejo
domain:infra
domain:memory
domain:runtime
domain:signal
domain:ux
flow/architecture
flow/blocked
flow/deployed
flow/done
flow/implementation
flow/intake
flow/maintained
flow/observed
flow/ready
flow/refining
flow/retired
flow/review
iterating
judge/codex-candidate
judge/hermes-candidate
judge/low-confidence
judge/needs-refinement
judge/operator-needed
judge/p0
judge/p1
judge/p2
judge/p3
judge/park
judge/patchwarden-candidate
judge/stale-priority
kind/adr
kind/bug
kind/chore
kind/feature
kind/infra
kind/ops
kind/refactor
kind/research
large-impact
merge/auto
merge/manual
merge/manual-dependency-conflict
merge/manual-failing-tests
merge/manual-merge-conflict
merge/manual-missing-review
merge/manual-operator-preference
merge/manual-red-zone
merge/manual-security-sensitive
merge/manual-unclear-scope
merge/manual-unknown
meta
mode:operator-only
mode:patchwarden-iskra-approved
mode:safe-auto
needs-operator-decision
needs-triage
not-ready
observed/erroring
observed/needs-followup
observed/pending
observed/retire-candidate
observed/unused
observed/used
operator-emotional
owner-attention
phase/02
phase/03
priority:p0
priority:p1
priority:p2
priority:p3
proposed
ready-for-agent
ready-for-operator
recovery
review:claude-reviewed
review:codex-reviewed
review:dziadek-reviewed
review:needs-human
risk/exposure
risk/process
risk/product
risk/runtime
safety:external-write
safety:no-prod-mutation
safety:prod-impact
safety:secret-touch
size/large
size/medium
size/small
size/tiny
size/unknown
source/adr
source/agent-generated
source/manual
source/operator-chat
source/voice-note
status:blocked
status:codex-ready
status:merged:pending-evidence
status:needs-evidence
status:operator-needed
status:parked
tier/full
tier/lite
tier/stacked
tier:0-platform-substrate
tier:1-iskra-value-layer
tier:2-tools-products-modules
type:bug
type:chore
type:docs
type:feat
type:policy
type:research
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
pdurlej/platform!160
No description provided.