fix(security): reconcile the publish gate's two merged lineages - #8721
fix(security): reconcile the publish gate's two merged lineages#8721pepmach wants to merge 1 commit into
Conversation
First Principles Review (Fable 5) — 🟡 CONCERNSPremise-level review of First-Principles-Verdict: CONCERNS The fix earns its place — main's CI is red — but it leaves What this change shipsIntent: make the push-publish gate green again after two security lineages squash-merged into a semantically conflicting
Watch
Subtractions
[FIRST-PRINCIPLES-REVIEWED] 3044d66 |
GPT 5.6 Review — 🔴 changes requested (blocking)GPT 5.6 found at least one blocking issue that must be resolved before merging This comment is updated in place on each push. BLOCKING -- src/kiro_crew/security.py:6589 -- Redirect-position process substitution bypasses the publish gate Adjudication (Opus 4.8) — is blocking on each finding proportionate?I've traced the finding through the code. Let me verify my trace is complete by confirming the split behavior keeps the nested push in one segment and that My analysis confirms the behavior: For
The gate allows the command; HARM: UNBOUNDED — a redirect-position process-substitution payload carrying a protected-branch push escapes the publish gate entirely. False positive or not applicable? A repository writer can comment: |
Design Review (Fable 5) — 🟡 CONCERNSDesign-level review of Design-Verdict: CONCERNS Sound layer-ownership reconciliation, but it multiplies near-identical hand-rolled quote-state machines whose divergence is the exact defect class being fixed. Watch
Suggestions
[DESIGN-REVIEWED] 3044d66 |
Opus 4.8 Review — ✅ no blocking findingsReviewed Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
f096901 to
1d18634
Compare
1d18634 to
621099a
Compare
621099a to
b632c9f
Compare
b632c9f to
c9979b9
Compare
63ac46e to
3101c24
Compare
3101c24 to
6bf30b7
Compare
Nine push-gate tests fail on every platform's shard 3 since #7808 merged: its squash resolution fused that PR's raw-word token walk with the subshell-gluing lineage's operator-cut argument builder, and the two contracts cancel each other. The builder handed the walk pre-cut words, so extglob openers, glued redirections and escape state were gone before the checks that look for them ran: an extglob pattern arrived as the phantom refspec at-sign, and a branch literally named feature-pipe-x was denied. Give each layer one job, satisfying both suites: - The argument builder returns RAW argv words and consumes only what is shell-removed from argv: redirection operators, their one-word targets, and redirect-position process substitution through its matching close paren. Word-position process substitution passes through and lands on the ungated branch, joining command substitution, since bash substitutes a /dev/fd path no static reading can check. - The token walk keeps operator arity, extglob (kept whole by the quote/escape state machine), glued-word decomposition, and a live trailing escape, read off the raw spelling. - Anchoring skips a program token inside a still-open quote: that is a wrapper's quoted payload, judged by the nested-payload reading, not this line's program. - Publish segments split quote-aware, so a separator inside quotes is data and a branch whose name contains one stays pushable, while backslash-newline splicing keeps its ungated posture. - Paren-only glue is a subshell edge, not an untrusted split: the metacharacter inventory row moves to the precise protected-branch tag its sibling suite pins, instead of the unparseable fallback that also denied feature pushes inside subshells.
6bf30b7 to
3044d66
Compare
|
Closing as redundant: main healed independently via #8712 (raw words to the arity scan — the same core reconciliation), #8719 (fail-closed unreadable process-substitution bodies), #8672 and #8282. Verified on current main: the nine broken shard-3 tests pass (51/51 across both classes), and all five adversarial cases this branch additionally fixed (ANSI $$-parity anchor skip, quoted paren inside a substitution, ANSI phase drift across separators, case grammar inside a substitution, comment inside a substitution) are correctly denied or ungated by main's implementation. Nothing left to merge. |
What broke
Since #7808 merged, shard 3 of Backend Tests fails on every platform — Linux and Windows alike — on nine push-gate tests (
test_push_branch_gate.py::TestUnrecognisedOptionsReadProtectively,test_security.py::TestGitPublishSubshellGluing). This is blocking every open PR, and main's own CI run2d69e0836fails identically.The cause is a semantic merge conflict, not any single commit's logic: #7808 was written against a
security.pywhere_git_push_argswas a plain whitespacesplit(), and its token walk does all operator reasoning on raw argv words. Between #7808's CI run and its merge, the subshell-gluing lineage landed an operator-cut argument builder. The squash resolution kept both — so the builder handed the walk pre-cut words, and every reading the walk performs on operator characters went blind:@(main)arrived as the phantom refspec@, taggedambiguous-refinstead ofwildcard-refspecfeature|x(quoted) was cut at the pipe at the segment level and denied as cross-segment evasion<<-, live trailing escapes: all invisibleBoth lineages' tests never coexisted green on any tree; #7808's own head fails its own tests once rebased.
The reconciliation
One job per layer, so both suites' invariants hold simultaneously:
_git_push_argsreturns RAW argv words. It consumes only what the shell removes from argv: redirection operators, their one-word targets, and redirect-position process substitution (> >(cmd)) through its matching). Word-position<(cmd)passes through and lands on the ungated branch, joining$(— bash substitutes a/dev/fdpath no static reading can check (round 8's exact pin).gitinside a still-open quote: that is a wrapper's quoted payload (bash -c '(cd x && git push …)'), judged by the nested-payload reading, not this line's program.;,|,&&,||, newline — outside quotes only), so'feature|x'is one word and stays pushable, while backslash-newline splicing keeps its ungated posture via the live-escape rule.(cd /tmp; git push origin main)keeps the preciseprotected-branch-nametag and(git push origin fix/x)stays allowed. One metacharacter-inventory row is updated accordingly — its old expectation (unparseable → ungated) directly contradicted the sibling suite's requirement that feature pushes inside subshells stay allowed, and the precise tag is strictly stronger.Testing
test_push_branch_gate.py,test_security.py,test_denied_commands_security.py,test_deny_guidance.py,test_credential_prefilter.py,test_mcp_cron_security.py,test_hooks_coverage.py,test_denied_commands_authority.py— 3,436 passed.--splits 4 --group 3): 21,946 passed, zero push-gate failures; the five residual failures are pre-existing dev-box artifacts (/local/homeuid ownership intest_service.py, one admission-cost flake) that also fail on an unmodified checkout.mypy --platform linux(1,293 files), black gate (one graduated baseline entry pruned).Pattern harvest
Rule candidate: a CI check (or merge-queue requirement) that re-runs a PR's own changed test files against the MERGE RESULT whenever
src/kiro_crew/security.pyis modified on both sides of the merge — the defect class is squash-merge semantic conflicts between security PRs that raced each other's rewrites of the same functions: each PR green on its own base, red combined, and nothing runs the combination until after merge.