Repository navigation
Conversation
75bb3ca to
f11c790
Compare
f11c790 to
5baaf50
Compare
5baaf50 to
2ec7390
Compare
2ec7390 to
1533ad9
Compare
1533ad9 to
d5e9e7e
Compare
d5e9e7e to
88dd99d
Compare
88dd99d to
c93686f
Compare
c93686f to
6d26afe
Compare
6d26afe to
fdd0a2e
Compare
UX Review (Opus 5.5, fork) — ⏭️ skippedRevision |
Design Review (Opus 5.5, fork) — ✅ PASSDesign-level review of Design-Verdict: PASS Nothing blocks: the change only turns refusals into allows, inside a closed Notes
[DESIGN-REVIEWED] b9399ca Description read[DESCRIPTION-READ] 5bd2c795e085d0bbaaaee32a17e3ff181500af712bac51d1f1d1873ee297757c That sha256 names everything this verdict read: the PR description, plus the 1 evidence file(s) the workflow collected from it and handed this review. It covers that evidence because the media strip replaces every attachment URL with the same placeholder, so an edit swapping one attachment for another would leave the description's captured text identical while what this review judged changed. Recompute it with |
First Principles Review (Opus 5.5, fork) — ✅ PASSPremise-level review of First-Principles-Verdict: PASS Nothing blocks. The only change this PR claims is a narrow What this change shipsInventory (9 items) — 5 justifiedIntent: let an agent's
[FIRST-PRINCIPLES-REVIEWED] 99508c3 Description read[DESCRIPTION-READ] 8804d16db64bce69d4d2350a32bfca7b048ba1456747ff8c5e96508b09dfc37c That sha256 names the PR description this verdict read. Recompute it with |
GPT 6.1 Review (fork) — ✅ no blocking findingsReviewed Review detailsNo findings. |
Opus 5.5 Review (fork) — ✅ no blocking findingsReviewed |
Security Scope Review (fork) — ✅ PASSWhich legitimate operations does adjudicated, zero confirmed regressions Scope-Verdict: PASS Reason: Mostly this change loosens the push gate: a newline inside quotes now stays part of its word. The one stricter part is a new per-line backstop in TIGHTENING: Push-floor per-line reading in the sensitive-bash tier ( ADJUDICATED: 16 candidates, 0 confirmed newly-refused PLATFORM COVERAGE: Most rows are platform-neutral ( UNADJUDICATED: The refusal text chosen by [SCOPE-REVIEWED] 99508c3Security scope review -- adjudicated candidates
No candidate flipped from allowed to refused. |
ae859b2 to
2ae1d03
Compare
2ae1d03 to
a2c8491
Compare
a2c8491 to
dfd95d2
Compare
dfd95d2 to
f465a22
Compare
|
Thanks for the review. The Opus 5.5 finding on Finding: Fix: the hint check now runs the rewritten command through Tests ( Sweep vs Gates: 1,852 passed (the 2 failures are a local Could a maintainer please approve the workflows on |
|
Status on Could a maintainer please re-run those two lanes? This PR still merges after #15438. |
|
Opus 5.5 finding on 8849ce5: confirmed. A quoted Cause. The rewrite in Fix. The fix belongs to the hint itself, so it went into #15438's commit (5986a0e), together with the nested-payload reading this PR used to carry. The rewrite now moves a Verdicts did not change. Against 8849ce5:
The test pass set is identical: 1,868 passed, plus the 2 known @iamwhatever @bolichen97 @cruisercohen could one of you approve the workflow runs on b9399ca? This PR is stacked on #15438, so it can land right after it. |
|
HOLD (policy: sensitive change) at b9399ca. This PR widens the git-publish deny floor: a The branch also contains all of #15438 (commit 5986a0e), which is still open. Merging this PR first would land #15438's security changes without their own review; the Design and First Principles lanes say the same. I found no concrete defect in the top commit: only Clears when: #15438 merges, this PR is rebased onto — crew-pr-reviewer (r124) |
|
@iamwhatever thanks for the careful read, and for confirming the top commit admits only Confirming the plan that clears the HOLD:
No change needed on the top commit meanwhile. I'll rebase both if |
_split_push_command_segments ended a segment at every newline, including one inside quotes. The next segment's quote walk then restarted unquoted, so the closing quote of a multi-line commit message opened a new quote that swallowed the following && and turned a literal feature-branch push into one unparseable word, refused as git-publish-target-unverifiable. A literal newline inside quotes is now word data, like the other quoted separators, but only while the quote walk can be trusted across lines. The walk does not model heredocs, command/parameter/process substitution, backticks or comments, where a phantom quote could hide the next line, so a command holding any of them keeps the split at every newline. A merged segment that does not parse as a push (quoted text that merely mentions one, like printed instructions or a PR comment body) is read line by line, exactly as before, so no quoted text is newly refused. Unquoted and escaped newlines still split, so backslash-newline splices keep failing closed, and a protected push after a multi-line message is now reported as push-protected-branch-name. kiro-cli titles a shell call `Running: <command>` and the deny tiers judge that title too, so the inert-word proof reads through ONE exact leading `running: ` (the prefix constant denied_rules already uses for its exceptions, now defined beside the walk) and the live call gets the verdict of the command it displays. The refusal's "put the push on its own line" hint moves a push only at an unquoted separator, and judges the rewritten command over every nested shell payload, as the floor does. A separator inside the quoted message, or a bash -c push to $b, earned the hint, and the command with the push on its own line came back with the same refusal and the same hint. Verdicts do not change. The security spec paragraph and the package line budget are updated to match. Refs kirodotdev#12631
After a multi-line commit message, a feature-branch push behind a PATH prefix (env PATH=/x:$PATH or a bare PATH=/x) was refused as git-publish-target-unverifiable, while the same push on its own line was allowed. The inert-word allowlist that decides when a quoted newline is harmless message text admitted no prefix and no expansion, so the sentinel stayed. A git push segment in that allowlist may now set PATH first, bare or after a plain env with no option, to plain text or $PATH. PATH can change which git runs but not the argv the per-line reading judges, and that reading still checks the push after the message in full. No other variable is admitted: a GIT_* value can remap the refspec, and BASH_ENV or ENV can make a shell that git starts source the message file. An env option, a lone assignment, a prefix on any other command, and $PATH anywhere outside the prefix keep the sentinel. Each form has a real-bash oracle row that pushes to a protected branch and stays denied; the oracle stubs env as a shell function so it reaches the stub git. The coverage test now requires a bare and an env row for every prefix variable. The security spec and the package line budget are updated to match. Closes kirodotdev#20847
Problem / Motivation
Goal: a feature-branch push behind a
PATHprefix after a multi-line commit message is allowed, the same as that push on its own line.With #15438 applied,
git commit -m 'a\n\nb' && env PATH=/x:$PATH git push origin featand... && PATH=/x git push origin featare still refused withrule=git-publish-target-unverifiable(with the "put the push on its own line" hint), while the same push on its own line is allowed onmain.Why it matters
Agents often prefix a push with
PATH=...(for example a toolchain or a credential helper on PATH) in the same chained command that commits with a multi-line message. The refusal forces a retry with the push on its own line, and the reason it gives does not explain why the chained form is different.Not a goal
PATH, anyenvoption, or a prefix on a command other thangit push.What changed (motivation → approach → change)
cd <dir>,git add,git commit -m/-q,git push -u/-q), checked by_quoted_word_is_inert/_inert_segmentinsrc/kiro_crew/security/shell_normalizer_walk.py(wheremain's split of the shell reader moved the push segment splitter and fix(security): keep a quoted newline inside its word on the push path #15438's helpers with it). Anenv/assignment prefix, and the$in$PATH, were outside that list, so the sentinel stayed.git pushsegment may now start withPATH=<value>assignments, bare or after a plainenvwith no option (_inert_segmentchecks the name isPATH; no other variable is admitted). The value may hold plain characters and$PATH;$PATHis now the one expansion the plain check admits, and_inert_segmentrefuses any$outside that prefix.PATH: it can change whichgitruns, but not the argv the per-line reading judges, and that reading still checks the push after the message in full (the protected-branch, bare,$b, brace and$'…'checks are unchanged). A literalPATH=/xcan already point at anygit, and the standalone form is allowed onmain. Other variables were rejected deliberately:GIT_CONFIG_*/git -c remote.origin.push=feat:maincan remapgit push origin featontomain, andBASH_ENV/ENVcan make a shell that git starts source.git/COMMIT_EDITMSG, which would execute the message._INERT_ORACLE_ROWS(env PATH=/x:$PATH git push origin main,PATH=/x git push origin mainafter a multi-line message) run in real bash and stay denied. The oracle stubsenvas a shell function that exports its leading assignments and runs the rest, so it reaches the stubgit(its PATH has nogit). Only the fixed rows bypass the corpus'senv/PATHfilter.docs/system-specs/modules/security.mdand_PACKAGE_LINE_BUDGET(32,731 → 32,749) updated.Backwards compatibility
Compatible: this only allows commands that were refused; every command allowed before is still allowed, and nothing is newly refused. The hint fix changes only the wording of some refusals (the substitution reason in place of a hint that did not apply).
Removes nothing: the change widens one allowlist and takes no path, row, key or function away.
Tests
In
test/test_push_branch_gate_parser.py:test_quoted_text_that_quotes_a_push_line_is_judged_line_by_line: both issue repros (plusPATH=/x git push -u fork fix/x) are allowed. 17 adversarial shapes stay refused throughis_denied:env GIT_DIR=…,GIT_CONFIG_*refspec remap,BASH_ENV=.git/COMMIT_EDITMSG,PATH=/x GIT_SSH_COMMAND=sh,env -i, bareenv,PATH=/x env git push, a lonePATH=/x;,PATH=$b,PATH=$PATH'x',PATH=/x*,PATH=/x git -c remote.origin.push=f:main push,PATH=/x git add, a prefixed commit,$PATHas a commit argument orcdtarget,$PATHin the push args. The out-of-allowlist hint example is nowenv GIT_DIR=/x. The no-hint rows addbash -c '<push> origin $b'andeval '<push> origin {a,main}'in front of a multi-line message plus a feature push: both keep the substitution reason.test_no_command_real_bash_runs_as_a_protected_push_is_allowed: runs the two new rows in real bash, and each one logs a protected push thatis_deniedrefuses (with the construct list on and off).test_every_inert_allowlist_entry_has_a_real_bash_oracle_row: also requires a barePATH=row and anenv PATH=row.test_the_own_line_hint_is_left_out_when_the_rewrite_cannot_be_read: a payload walk that fails on the rewritten command leaves the hint out.Hint mutation check: with the old
_own_line_fix_applies, both hint tests fail.Mutation check: with the source reverted, 2 tests fail. Each widening mutant (any variable name; a prefix on any command;
$PATHin any word) fails 1 test.Run (
pytest -n 4):test_push_branch_gate.py,test_push_branch_gate_parser.py,test_security_gate_liveness.py,test_argv_floor_inline_and_brace_scope.py,test_denied_commands_security.py,test_denied_commands_authority.py,test_security_split_patch_reach.py,test_denied_commands_security_*.py,test_denied_rules_facade.py,test_argv_floor_facade.py: 1852 passed, 1 skipped, 2 failed (ModuleNotFoundError: markdown_it, a local venv gap unrelated to this change). black, isort, flake8, mypy,check_comment_history.pyanddocs-lint.shpass.Manual verification
39-command probe through
is_denied, run onorigin/main, on #15438 and on this branch. Only the 4 intended env/PATH feature pushes change, from DENY to ALLOW. Every protected, bare,$b, brace,$'main',HEAD:main,--force mainandfeat:mainpush behind the prefix stays DENY, as do the non-PATH variables andenvoptions above. One case is allowed on both #15438 and this branch: a message whose body is a literal single-quoted$1 push origin mainline, followed by a feature push. #15438 already allows that message with an unprefixed push, because the body is literal data.Hint sweep: 11,024 commands (the parser module's multi-line push corpus, plus multi-line messages behind
bash -c/eval/sh -cpayloads that push to a feature branch,$b,{a,main}ormain), run on the previous headdfd95d228and on this one. 0 verdict changes, and 0 changes in the refusal reason apart from the hint sentence. The hint appeared on 2,766 refusals before and appears on 1,036 now. For each of those commands the probe resubmits the own-line rewrite. All 1,036 remaining hints are true (the rewrite is allowed). The 1,730 dropped hints were all false (the rewrite is still refused). No true hint was lost.Related Issues
Closes #20847
Follow-up of #15438. Refs #12631.
Pattern harvest
Not generalizable: the inert-word allowlist is deliberately closed and grows one reported shape at a time, each with its own real-bash oracle row; the coverage test already enforces that pattern.
Checklist
feat|fix|docs|style|refactor|perf|test|chore|ci|build|revert: ...)Contribution License Agreement
Contributed under the project's Apache-2.0 license.