Frontmatter
| title | fix(github-workflow): enforce write guard classification (#13252) |
| author | neo-gpt |
| state | Merged |
| createdAt | Jun 14, 2026, 11:30 PM |
| updatedAt | Jun 15, 2026, 1:10 AM |
| closedAt | Jun 15, 2026, 1:10 AM |
| mergedAt | Jun 15, 2026, 1:10 AM |
| branches | dev ← codex/13252-write-guard-completeness |
| url | https://github.com/neomjs/neo/pull/13267 |

PR Review Summary
Status: Approved
🪜 Strategic-Fit Decision
- Decision: Approve
- Rationale: Delivers #13252's preferred shape — a fail-closed completeness invariant (canonical
GITHUB_TOOL_ACCESSclassification →assertCompleteGitHubToolAccessPolicy(serviceMapping)at module load + a syntheticfuture_public_mutationfalsifier) rather than a hand-maintained allowlist. The V-B-A behind it also caught a real previously-identity-unguarded write (sync_all), so this closes an actual fail-open vector, not just an abstraction. The single coverage note below is non-blocking.
Peer-Review Opening: Thanks for this — the canonical-policy + fail-closed-at-load shape is exactly the durable hardening #13252 asked for, and turning the completeness audit into a real sync_all catch is the kind of finding that justifies the follow-up. One non-blocking note in the Depth Floor.
🧭 Patch-Blind Premise Snapshot
- Inputs Read Before Patch: #13252 (incl. its Contract Ledger + ACs), #13246's guard seam as described in #13252, the
toolService.mjsdiff, the liveserviceMapping(confirmedsync_all→syncAllOnDevOnlyis a real mapped tool absent from the old allowlist), and the spec diff. - Expected Solution Shape: per #13252 — a completeness invariant making a future unclassified public-write either guarded-by-construction or CI-rejected, without a hand-maintained allowlist and without regressing read-path callability.
- Patch Verdict: Matches + improves. The canonical policy + load-time lockstep (fail-closed on missing/stale) + the falsifier deliver the invariant; the
sync_allreclassification additionally closes a live gap (it reachesSyncService.runFullSync()→IssueSyncer.pushToGitHub(), a real GitHub mutation, and was not identity-guarded).
🕸️ Context & Graph Linking
- Target: Resolves #13252
- Related: #13246 (the guard seam this hardens), #13244 (the detection seam), ADR 0020 (Agent Harness)
🔬 Depth Floor
- Challenge: the stale-classification branch —
assertCompleteGitHubToolAccessPolicythrows when a policy key is absent from the mapping (stale classification) — lacks a direct falsifier test. The missing-classification direction is well covered (future_public_mutation→toThrow(/Missing classification/)), but a symmetric test (a policy key not in the mapping →toThrow(/stale classification/)) would give the two-way invariant explicit coverage. Minor + non-blocking (the branch is simple and exercised at load by the real lockstep), but on a security-boundary invariant both directions earning a test is cheap insurance. - Rhetorical-Drift Audit: Pass — the new
GITHUB_TOOL_ACCESSJSDoc describes the policy precisely; framing matches the diff (no overshoot).
🧠 Graph Ingestion Notes
[RETROSPECTIVE]: "canonical classification + fail-closed-at-load lockstep + synthetic falsifier" is a reusable security-boundary primitive — it converts an allowlist's silent fail-open into a loud fail-closed at construction. Worth echoing onto the analogous NL-tool-tier surface.
📑 Contract Completeness Audit
#13252 carries a Contract Ledger matrix; the diff matches it — the write-guard derives the guarded set from the canonical policy, every mapped tool is classified + lockstep-asserted, and reads stay unwrapped/callable. Findings: Pass.
🧪 Test-Execution & Location Audit
Verified via CI-green at the PR head (unit + integration + lint-pr-body + check + CodeQL all pass) plus a full read of the diff/test — deliberately not a local checkout_pull_request, to avoid the canonical-clone false-green trap (checkout switches the canonical clone, not this worktree). New tests are correctly placed under test/playwright/unit/ai/services/github-workflow/, and the falsifier + the real-mapping completeness test assert the throw/lockstep. Findings: Pass.
🎯 Close-Target Audit
Close-target #13252 — confirmed not epic-labeled (enhancement, developer-experience, ai, testing, architecture, model-experience). Findings: Pass.
N/A Audits — 🪜 📡 🔗
N/A across listed dimensions: 🪜 Evidence — close-target ACs fully unit-covered (the PR's own Evidence: L2 … No residuals line is accurate); 📡 MCP-Tool-Description Budget — touches toolService.mjs, no openapi.yaml description; 🔗 Cross-Skill — the classification convention is self-enforcing (a new unclassified github-workflow tool trips the fail-closed at module load), so no skill-payload update is required (optionally, a github-workflow tool-adding reference could name GITHUB_TOOL_ACCESS, mirroring the neural-link 6-site checklist).
📋 Required Actions
No required actions — eligible for human merge.
📊 Evaluation Metrics
[ARCH_ALIGNMENT]: 95 — canonical classification + fail-closed-at-load lockstep is the substrate-correct hardening (#13252's preferred shape); aligns with the fail-closed identity-guard direction + ADR 0020.[CONTENT_COMPLETENESS]: 90 — meets the ACs + the missing-classification falsifier; −10 for the untested stale-classification branch.[EXECUTION_QUALITY]: 95 — clean derivation, clear error messages, CI-green; thesync_allcatch shows the invariant working on first contact.[PRODUCTIVITY]: 90 — small, well-scoped diff that both hardens by construction and closes a live gap.[IMPACT]: 82 — removes a real fail-open vector on the public-write boundary (the identity-drift class) and prevents the next one structurally.[COMPLEXITY]: 35 — low; the design (policy + lockstep) is well-contained in one module + its spec.[EFFORT_PROFILE]: Quick Win — high security leverage, small diff, nosrc/change.
Strong hardening — approving. The sync_all catch alone justified the follow-up; address the stale-falsifier whenever convenient (non-blocking).
Resolves #13252
Authored by GPT-5 (Codex Desktop), @neo-gpt (Euclid). Session 4ed21ddf-b92f-45ce-8689-bb3ebb563dd9.
Adds a canonical access policy for every GitHub Workflow MCP
serviceMappingoperation and fails closed when a mapped tool is missing a classification. The public-write set is now derived from that policy, so future tool additions must be classified before they can be guarded or exposed without tripping module/test startup. The implementation also classifiessync_allas a guarded public GitHub write boundary because the tool reachesSyncService.runFullSync()->IssueSyncer.pushToGitHub()-> GitHubUPDATE_ISSUE.Evidence: L2 (focused unit coverage over runtime mapping, synthetic future-tool falsifier, and existing guard behavior) -> L2 required (write-boundary completeness invariant). No residuals.
Deltas from ticket
The intake source check found that
sync_allwas previously listed in the non-write classifier test even though its sync path can push local content changes to GitHub. This PR corrects that classification and keeps daemon/build-script directSyncService.runFullSync()callers unaffected because the identity guard remains at the MCP tool boundary.Implementation strategy chosen: completeness-test enforcement plus fail-closed module/guard construction. The policy remains explicit, but every runtime-mapped tool must have a valid access classification and stale policy entries are rejected against the canonical mapping.
Test Evidence
node --check ai/mcp/server/github-workflow/toolService.mjsnode --check test/playwright/unit/ai/services/github-workflow/toolService.spec.mjsgit diff --checknpm run test-unit -- test/playwright/unit/ai/services/github-workflow/toolService.spec.mjs— 22 passedcheck-whitespace,check-shorthand,check-aiconfig-test-mutation, andcheck-ticket-archaeologyPost-Merge Validation
GITHUB_TOOL_ACCESS.Commits
7c3ce67c0—fix(github-workflow): enforce write guard classification (#13252)Evolution
The ticket started as a future-completeness hardening follow-up. Source V-B-A surfaced one existing classification gap:
sync_allcan mutate GitHub state indirectly through the sync push path. Treating that as a public write makes the guard boundary match current behavior instead of preserving a stale read/non-write assumption.