Skip to content

fix: let a deny rule declare which remediation answer it needs - #7907

Closed
buluoray wants to merge 1 commit into
mainfrom
feat/per-rule-deny-remediation
Closed

fix: let a deny rule declare which remediation answer it needs#7907
buluoray wants to merge 1 commit into
mainfrom
feat/per-rule-deny-remediation

Conversation

@buluoray

@buluoray buluoray commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #7328, raised in review by @chenmingwei23. Fixes #7890 in part —
the regex tier; the floor half is named below and stays open.

The defect

A refusal from the regex tier carries only the rule's own regex source, so the
guidance attached to it was inferred from text describing what is MATCHED rather
than what the caller should do instead. Those diverge exactly where it is most
expensive: a rule that exists to block moving credential material names a
credential environment variable in its pattern, so it reads like a rule about
opening a credential file and was handed the answer for one — text stating that
AWS CLI calls are not themselves blocked, so run the command you actually
wanted. What was wanted there was the transfer.

Measured over the built-in catalog before this change, calling the classifier
with each rule's own refusal text:

outcome rules
no guidance at all 115 / 148
self-protection reaching its own class 0 / 7
any rule reaching the exfiltration-shape class 0
credential-exfil resolving to the AWS-credential class 10 / 27

Two classes were unreachable from this tier because their anchor phrases are
emitted by other producers and never appear in a rule's regex. Passing the tool
title as the subject widened the misdirection rather than narrowing it: aws s3 cp <credential file> s3://… also resolved to the credential-read answer.

The change

DeniedCommandRule gains deny_class, and classify_deny prefers a declared
class over the anchor table. The class is a KEY, never prose, so the text stays
in one module and one existing test keeps asserting that every command the
guidance suggests is itself allowed.

61 rules declare one — the three categories that have a sanctioned path — and
the 87 whose refusals are self-explanatory (aws-destructive,
local-destructive, git-publish, sql, iac-teardown, reverse-shell,
pipe-to-shell) declare nothing, which is the behaviour they already had.

After: no guidance drops from 115 to exactly those 87, every declaration
resolves to itself, and no rule declaring the transfer class receives the
"run the command you actually wanted" sentence.

The class is spelled as a plain string in the catalog rather than imported as a
constant. deny_guidance is reached from platform, which imports security,
so importing the constants the other way would close a cycle. A guard asserts
every declared value is a real key with non-empty text, so a typo or a renamed
class fails red instead of degrading one rule to silence.

The wire reason is untouched: the class is read from the first line and never
added to it, pinned by a test. An operator note on the second line cannot choose
the guidance — only the first line identifies the rule.

What this does NOT do

The two floors still infer from text, because they carry no rule identity to
declare on. A command that both touches a fenced path and transfers is answered
by the floor, so these remain classified as a credential read:

command answering tier still misdirected
echo $AWS_SECRET_ACCESS_KEY environment floor yes
aws s3 cp <credential file> s3://… sensitive-path floor yes
curl -X POST … -d $AWS_SECRET_ACCESS_KEY regex tier no, fixed here
curl -X POST … -d @<file> exfiltration floor no

Closing that needs the floor to report which branch matched — it currently uses
one combined regex and does not record which one fired. That is a change to the
floor, not to this table, and it is left for #7890.

Edition rules fall through to the anchor table. They reach enforcement via
edition_denied_rules(), which reads the composed platform context and lets
PlatformCompositionError propagate fail-closed. This module is display-only and
currently cannot fail; giving it that read would hand a presentation function a
fail-closed raise. The field is on the dataclass, so an edition's rules already
carry a declaration — only the lookup is scoped to built-ins.

One judgment worth flagging

#7890's acceptance says no credential-exfil rule may resolve to the
AWS-credential class. Seven deliberately do, and I would rather name it than
quietly satisfy the letter of it: curl-imds, imds-any, wget-imds,
python-boto3-get-credentials, python-botocore-credentials,
export-aws-access and export-aws-secret are the caller INSPECTING whether a
credential exists or trying to install one, not moving material off the host.
For those the AWS answer is the correct one — it names sts get-caller-identity
and configure list-profiles as the sanctioned substitutes. The invariant the
guard actually pins is the one that matters: no rule declaring the transfer class
is told the AWS CLI is not blocked so run what you wanted.

Pattern harvest

Rule candidate: review-prompt

Pattern: a presentation layer recovering a semantic decision by keyword-matching
text the producing site emitted for a different purpose. The refusal text here is
a regex source — it exists to say what was MATCHED — and the guidance layer read
it to decide what the caller should DO. Those agree only by coincidence, and the
coincidence broke on exactly the rules where being wrong costs most: the ones
whose pattern names a credential variable because they exist to stop it being
sent somewhere.

The check a reviewer can apply: when a refusing or validating site already holds
structured identity — an id, a category, a dataclass field — a downstream layer
must not re-derive that identity from the site's free text. If the identity is
not threaded through the boundary today, adding a field is cheaper than a
classifier and cannot drift.

Not expressible as semgrep, because the defect is a missing declaration rather
than a code shape. The equivalent is enforced here as a census test: every rule
in a remediation category must declare a class, so a rule added without one fails
red instead of shipping with no guidance and nothing to notice. That covers
authors who never read this rule, which a lint pattern over the diff would not.

Verification

  • 9 new guards, all asserting on OUTPUT; one drives is_denied end to end so it
    cannot go inert if the command stops being refused.
  • 7 mutations, each breaking exactly what one guard claims to protect, all
    caught: a declaration removed, a declaration added to a destructive rule, a
    misspelled class, the preference reverted, a transfer rule declared a read, the
    lookup reading the whole reason instead of the first line, and the wire reason
    growing a field.
  • 934 tests green across the nine deny-related suites; mypy --platform linux
    clean over 1263 files; black, isort, flake8, subprocess-encoding, docs-lint,
    harness-parity and brand gates clean.
  • docs/system-specs/modules/security.md updated in the same commit, both places
    that enumerate the model's fields.

@buluoray
buluoray requested a review from a team as a code owner September 2, 2026 16:04
@buluoray
buluoray requested a review from iamwhatever September 2, 2026 16:04
@github-actions github-actions Bot added the readiness: checking Automated validation is still running label Sep 2, 2026
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

GPT 5.6 Review — ✅ no blocking findings

GPT 5.6 completed its review of 581c7fde91a30e121c37207f61d8b1fa8df87dcd and found no blocking issues.

This comment is updated in place on each push.

Review details

No findings.
[GPT-REVIEWED] 581c7fd

False positive or not applicable? A repository writer can comment:
/ai-review override gpt 581c7fde91a30e121c37207f61d8b1fa8df87dcd: <one-sentence reason>

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Design Review (Fable 5) — ✅ PASS

Design-level review of 581c7fde91a30e121c37207f61d8b1fa8df87dcd — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

Design-Verdict: PASS

Declaring the remediation class on the rule fixes the real misclassification at its root, without touching the frozen refusal wire format.

Watch

  • The pattern string is the rule's identity in _DECLARED_CLASS_BY_PATTERN, so two rules sharing one regex with different classes would silently collapse to the last; the per-rule round-trip test (test_a_declared_rule_resolves_to_exactly_what_it_declared) is the only guard — keep it if that test file is ever reorganized.

[DESIGN-REVIEWED] 581c7fd

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

First Principles Review (Fable 5) — ✅ PASS

Premise-level review of 581c7fde91a30e121c37207f61d8b1fa8df87dcd — why this exists and whether the shipped surface is the smallest honest version. Updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

All verification is done. The description's counts check out against the diff (exactly 61 deny_class= additions: 16 exfil_shape, 16 aws_credential, 20 secret_file, 7 self_protection, 2 trust_root; the 7 flagged credential-exfilaws_credential rules match). The frozen wire format is a real external constraint (RecoveryCard.tsx parses the first line, documented at security.py:12240), the edition scoping has a derived fail-closed reason, and the floors are counted and deferred to a named issue. The one thing the repo turned up that the description doesn't weigh: security.py already owns two pattern-keyed reverse maps over the same catalog (_RULE_ID_BY_PATTERN:1686, _BASH_EXFIL_RULE_BY_PATTERN:12866), and this change builds a third in deny_guidance.py.

First-Principles-Verdict: PASS

A measured defect (115/148 refusals with no or wrong guidance), fixed at the cause the frozen wire format permits, with every scope cut declared and counted.

What this change ships

Intent: stop deny refusals handing the agent remediation for the wrong action — a FIX.

  1. 61 built-in refusals now carry guidance matching the rule's purpose — justified, measured (115→87 no-guidance)
  2. Transfer-blocking rules never told "run the command you actually wanted" — justified, the reported defect
  3. DeniedCommandRule gains optional deny_class — justified; 1 consumer (deny_guidance.py), plain field, not generalized
  4. A declared class beats anchor table and subject in classify_deny — justified; anchors retained for floors/editions, which still need them
  5. Edition-declared deny_class is silently inert (lookup reads built-ins only) — declared; derived from the display-only/fail-closed constraint
  6. Floor-tier refusals (echo $AWS_SECRET…, aws s3 cp <credfile>) still misdirected — declared, deferred to Per-rule deny remediation: ten exfiltration refusals get read guidance #7890 with the blocking mechanism named
  7. Census test makes an undeclared future rule in the three remediation categories fail red — justified
  8. security.md field enumeration updated — mandated same-commit by AGENTS.md

Root-cause placement is honest: the wire reason is pinned by an external parser, so declaring the class on the rule and exact-matching the pattern is the deepest fix reachable without a wire change, and the remaining symptom sites are counted, not hidden.

Subtractions

  • Replace _DECLARED_CLASS_BY_PATTERN + _declared_class's map lookup with the existing pattern→rule resolution: security._rule_id_for_pattern (security.py:1710) into _RULES_BY_ID (security.py:1683) already answers pattern→rule, and this change adds the catalog's third pattern-keyed reverse map (grepped BY_PATTERN: security.py:1686, security.py:12866, plus this one) — three maps rebuilt from one catalog is where a future keying change diverges silently.

[FIRST-PRINCIPLES-REVIEWED] 581c7fd

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Opus 4.8 Review — ✅ no blocking findings

Reviewed 581c7fde91a30e121c37207f61d8b1fa8df87dcd — this comment is updated in place on each push.

Review details

The change is mechanical and well-verified: an optional deny_class field added to DeniedCommandRule, populated on the remediation-relevant rules, and consulted in classify_deny before the anchor-text fallback.

Falsification results:

  • No import cycle: security.py references deny_guidance only in a docstring (line 77), never imports it, so _DECLARED_CLASS_BY_PATTERN builds from a fully-initialized BUILTIN_DENIED_RULES at import time.
  • First-line contract holds: _deny_reason always emits DENY_REASON_PREFIX + pattern on line 1 with any note on line 2; _declared_class reads only the first line and strips exactly the prefix, so the dict lookup key matches rule.pattern. The argv-structural floor reports the rule's own pattern on line 1 too.
  • All declared class strings are valid keys: exfil_shape, trust_root, self_protection, aws_credential, secret_file are all DENY_CLASS_* constants present in REMEDIATION.
  • No security regression: classify_deny/remediation_for are display-only and cannot unblock a call; the short-circuit only changes which advisory prose is shown, and the s3-cp-of-credentials case now correctly resolves to exfil_shape instead of the old "run the command you actually wanted" AWS answer — an improvement, not a regression.

No candidate survives (there were none) and no new groundable defect at 80+ emerged in Step 2.

No findings.

[OPUS-REVIEWED] 581c7fd

Verdict parsed from the review's SHA-scoped output markers for commit 581c7fde91a30e121c37207f61d8b1fa8df87dcd.

False positive or not applicable? A repository writer can comment:
/ai-review override fable 581c7fde91a30e121c37207f61d8b1fa8df87dcd: <one-sentence reason>

A refusal from the regex tier carries only the rule's own regex source, so
the guidance attached to it was inferred from text that says what is matched
rather than what the caller should do instead. The two diverge exactly where
it costs most: an outbound-transfer rule names a credential environment
variable in its pattern, reads like a rule about opening a credential file,
and was handed the answer for one — text stating that AWS CLI calls are not
blocked so the caller should run the command it actually wanted, when what it
wanted was the transfer. That is worse than silence, which this module's own
docstring names as the failure to avoid.

Measured over the built-in catalog before this change: 115 of 148 rules
resolved to no guidance at all, no `self-protection` rule reached its own
class, no rule reached the exfiltration-shape class, and 10 of 27
`credential-exfil` rules resolved to the AWS-credential class. Passing the
tool title as the subject widened that rather than narrowing it.

`DeniedCommandRule` now carries `deny_class`, and `classify_deny` prefers a
declared class over the anchor table. The class is a key, not prose, so all
text stays in one module and one test keeps asserting that every command the
guidance suggests is itself allowed. 61 rules declare one — the categories
with a sanctioned path — and the 87 whose refusals are self-explanatory
declare nothing, which is the behaviour they already had.

The wire reason is untouched: the class is read from the first line, never
added to it. An operator note on the second line cannot choose the guidance.

Two tiers still infer from text and are unchanged, because they carry no rule
identity to declare on: the sensitive-path floor and the argv-structural
floor. A command that both touches a fenced path and transfers is answered by
the floor, so `echo $AWS_SECRET_ACCESS_KEY` and an upload of a credential
file still classify as a credential read. Closing that needs the floor to
report which branch matched.

Edition rules reach enforcement through `edition_denied_rules()`, which reads
the composed platform context and lets `PlatformCompositionError` propagate
fail-closed. This module is display-only and cannot fail, so the lookup is
built from the built-in catalog and an edition's rules fall through to the
anchor table.

Refs #7890
@buluoray
buluoray force-pushed the feat/per-rule-deny-remediation branch from 6bb3f3e to 581c7fd Compare September 2, 2026 16:23
@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Sep 2, 2026
@buluoray

buluoray commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Both advisory lanes PASS on 581c7fde91a30e121c37207f61d8b1fa8df87dcd. Neither
item is being taken into this PR, and both are measured rather than waved off.

Design — Watch: two rules sharing one regex would collapse to the last

Not currently reachable: measured over the catalog, zero patterns are shared
(compared on the loaded r.pattern values, not on source lines — a grep over
source text reports one duplicate, which is an artefact of an identical
continuation line rather than two rules keyed the same).

The Watch is right that the round-trip guard is what would catch it. Worth
stating the mechanism so it survives a future reorganisation of that file: if
rules A and B shared pattern P with different declared classes, the dict keeps
B's, so classify_deny(reason(P)) returns B's class while A declares its own —
test_a_declared_rule_resolves_to_exactly_what_it_declared iterates EVERY
declaring rule and compares against its own declaration, so A fails. The guard
does not depend on noticing the collision; it falls out of the round trip.

First Principles — Subtraction: reuse the existing pattern→rule resolution

Fair observation, and the count is right: security.py already carries
_RULE_ID_BY_PATTERN (1686) and _BASH_EXFIL_RULE_BY_PATTERN (12866), and this
adds a third map over the same catalog.

The specific substitution proposed does not hold, though. _rule_id_for_pattern
(1710) is a PIN resolver: it consults _LEGACY_RULE_ID_BY_PATTERN (1700), whose
own comment says it is "LOOKUP-ONLY: consulted by _rule_id_for_pattern (the pin
resolvers), never merged into _RULE_ID_BY_PATTERN — the legacy spellings must
not count as built-ins for _DenyMatcher's fast-path election or SEL
enrichment". Its four entries exist so a governance pin written against a
pre-widening catalog still resolves after an upgrade.

Measured: all four legacy spellings belong to rules that declare
self_protection, and none of the four is in the effective denied set, so no
refusal can ever cite one. Routing display classification through the pin
resolver would therefore import pin semantics — accepting patterns that are
deliberately excluded from the matcher — to answer a question the matcher owns.

The equivalent-membership version of the subtraction does work:
_RULE_ID_BY_PATTERN_RULES_BY_ID.deny_class reuses the existing
non-legacy map and returns exactly what the new map returns. It is two lookups
instead of one and changes no behaviour, which is why it is recorded in #7890
rather than pushed here: it is a refactor of code that is mutation-verified as it
stands, and every push re-runs the full matrix and re-triggers all five lanes.

Status

Two checks are red, both attributed to causes outside this diff:

@bolichen97

Copy link
Copy Markdown
Collaborator

Closing — the same misclassification fix as #7906, which covers the wider rule set

Verified relationship: functional overlap

Both PRs are single commits off the same merge base (3a8dd4d, the merged #7328), neither is stacked on the other, and nothing from either has landed (_RULE_CLASSES, _CATEGORY_CLASSES, _rule_class_index, _DECLARED_CLASS_BY_PATTERN, _declared_class, deny_class: str, and both test classes are all absent from origin/main; main's classify_deny is still anchor-only). They are two incompatible designs for one requirement at one call site: each inserts an identity short-circuit as the first statement of classify_deny in src/kiro_crew/deny_guidance.py, keyed on the same wire token (the first line after security.DENY_REASON_PREFIX, emitted by security._deny_reason), resolved against a {rule.pattern: class} map built over security.BUILTIN_DENIED_RULES — so only one can land, and the loser is a head-on conflict in that function, in the rewritten module docstring, and in test/test_deny_guidance.py. I parsed both catalogs: 55 of the 61 remediation-category pairings are identical in effect (9 *-aws reads to aws_credential, 18 other sensitive-file-read to secret_file, 7 self-protection to self_protection, the two legacy-*-secret to secret_file, the IMDS trio to aws_credential, 16 exfil rules to exfil_shape), and both add a near-isomorphic catalog census plus matching guards for the transfer-never-told-to-rerun, subject-cannot-widen, operator-note and wire-reason-unchanged cases. Neither delivers a capability the other structurally cannot: 7907's deny_class field is a latent seam, since its own lookup is built from BUILTIN_DENIED_RULES only, so an edition's declaration is as inert there as in 7906. The nomination of 7906 holds on measured coverage: because 7907 short-circuits only on a truthy declaration, its 87 undeclared rules keep main's anchor+subject scan, so aws ec2 terminate-instances --instance-ids i-… --profile sso still draws enterprise-SSO login prose from the sso anchor at deny_guidance.py:64-66 — the same "retry what you were refused" failure mode on a different rule set — while 7906 makes a named rule's "" final and pins it. 7906 is also the more apt on the two kirocrew token rules, and it needs no edit to the keystone security.py. 7907's advantages are disclosure (all lanes PASS vs an unaddressed design-lane CONCERNS on 7906 about the git-publish gap, which 7907 shares) and a defensible reading on 4 credential-exfil rules — neither outweighs a live misclassification.

This was adjudicated twice, independently; the second reviewer reached the same ruling (functional overlap). Their strongest corroborating fact:

  1. Same fix site, same key derivation: fix: key deny remediation by rule identity in the regex tier #7906 inserts rule_class = _rule_class(reason) / if rule_class is not None: return rule_class as the first statement of classify_deny (src/kiro_crew/deny_guidance.py); fix: let a deny rule declare which remediation answer it needs #7907 inserts declared = _declared_class(reason) / if declared: return declared at the identical spot. Both then fall through to main's unchanged text = f"{reason or ''} {subject or ''}".lower().strip(). Both maps are keyed by rule.pattern over security.BUILTIN_DENIED_RULESfix: key deny remediation by rule identity in the regex tier #7906 index.setdefault(rule.pattern, _RULE_CLASSES.get(rule.id) or _CATEGORY_CLASSES.get(rule.category, "")), fix: let a deny rule declare which remediation answer it needs #7907 _DECLARED_CLASS_BY_PATTERN = {rule.pattern: rule.deny_class for rule in security.BUILTIN_DENIED_RULES if rule.deny_class} — and both parse the same first line after security.DENY_REASON_PREFIX. 2. The coverage gap that decides the survivor: fix: let a deny rule declare which remediation answer it needs #7907's if declared: leaves the 87 undeclared rules on the anchor-scan path, and aws-destructive-ec2-terminate-instances (src/kiro_crew/security.py:522) plus the sso anchor in _CLASS_ANCHORS (src/kiro_crew/deny_guidance.py:64-66, precedence 2, ahead of aws_credential and secret_file) plus the subject being passed as title at remediation_for (src/kiro_crew/dashboard/state.py:2641 and :2737) means aws ec2 terminate-instances ... --profile sso still receives DENY_CLASS_SSO_CREDENTIAL's "Ask the user to run their host's SSO login command in their own terminal, then retry". fix: key deny remediation by rule identity in the regex tier #7906 returns the rule's "" as final and pins it with test_a_named_rule_with_no_guidance_answers_silence_not_an_anchor; its test_no_rule_takes_its_class_from_the_anchor_scan asserts over all 148 rules, an assertion fix: let a deny rule declare which remediation answer it needs #7907's 61-entry map structurally cannot make. 3. Mis-assignment in fix: let a deny rule declare which remediation answer it needs #7907: deny_class="trust_root" on credential-exfil-kirocrew-token and credential-exfil-kirocrew-token-argv, whose prose in REMEDIATION is "This path is the security ceiling you are governed BY ... let the user edit it themselves", against DENY_CLASS_SELF_PROTECTION's "reach the product's own credential mint, or stop the supervisor that is running you" — the class fix: key deny remediation by rule identity in the regex tier #7906 assigns and the one _SELF_PROTECTION_FLOOR_NOTES (src/kiro_crew/security.py:1791) already yields through the "matched structurally on the command's argv" anchor.

Evidence

Same function, same mechanism: src/kiro_crew/deny_guidance.py classify_deny (anchor-only at origin/main:296) gains rule_class = _rule_class(reason); if rule_class is not None: return rule_class in #7906 and declared = _declared_class(reason); if declared: return declared in #7907, both immediately before the pre-existing text = f"{reason or ''} {subject or ''}".lower().strip(); both parse the first line after security.DENY_REASON_PREFIX (security.py:2844, emitted by _deny_reason at security.py:12855) against a pattern-keyed map over security.BUILTIN_DENIED_RULES (#7906 _rule_class_index() with setdefault(rule.pattern, _RULE_CLASSES.get(rule.id) or _CATEGORY_CLASSES.get(rule.category, "")); #7907 _DECLARED_CLASS_BY_PATTERN = {rule.pattern: rule.deny_class ...}). Coverage asymmetry favouring #7906: #7907's guard is truthiness, so an aws-destructive refusal (e.g. rule aws-destructive-ec2-terminate-instances, security.py:522) falls through to the scan and --profile sso in the subject matches the sso anchor at origin/main deny_guidance.py:64-66, returning SSO login prose; #7906 returns the rule's "" as final (test_a_named_rule_with_no_guidance_answers_silence_not_an_anchor). Prose-fit asymmetry: #7907 declares credential-exfil-kirocrew-token and -argv as trust_root, whose REMEDIATION text is about an unreachable policy path ("let the user edit it themselves"), while the self_protection text verbatim says "reach the product's own credential mint" — what those two rules refuse; #7906's _RULE_CLASSES routes them to self_protection and pins agreement with the argv-structural floor route.

Why this one and not the other

Yes — #7906 is the right survivor. It fixes a strict superset of the reported defect class: the same ten AWS-named exfil rules, plus the subject-driven misclassification on the 87 rules that have no sanctioned path, which #7907 leaves live because it only short-circuits on a truthy declaration (verified reachable: the sso anchor at origin/main deny_guidance.py:64-66 matches --profile sso in the command title). It is also the more apt on the two credential-mint rules that #7907 routes to trust_root, it carries the larger census (8 catalog guards including test_no_rule_takes_its_class_from_the_anchor_scan and TestWireReasonFirstLineIsUnchanged over all 148 rules), and it keeps the wire reason byte-identical by construction with zero churn in the keystone security.py. #7907's counter-arguments are real but weaker: all advisory lanes PASS versus an unaddressed design-lane CONCERNS on #7906 — but that CONCERNS names the git-publish ID-led gap, which #7907 shares and does not disclose, so the delta is disclosure rather than defect count. One caveat against #7906 worth recording: its deferred-import rationale is partly overstated, since cli_doctor.py:102 already imports kiro_crew.security at module level.

Carry this over first

This closure is about redundancy, and these items are the exception: they are not on main and not in the surviving PR, so they need a home before the topic is finished. Please don't let them go with the branch.

Three items from #7907, none of them blocking for #7906. (1) The classification reading for four rules — credential-exfil-python-boto3-get-credentials, credential-exfil-python-botocore-credentials, credential-exfil-export-aws-access, credential-exfil-export-aws-secretaws_credential, where #7906 sends them to exfil_shape; exfil_shape's prose is "move a local file's contents off this host … name the file and the destination", which fits neither export AWS_SECRET… nor boto3.Session().get_credentials(), whereas the aws_credential text ("the SDK inside the aws process still reads it for you") is actionable for both. Both authors flag #7890's acceptance criterion 1 as needing exceptions (3 in #7906, 7 in #7907), so a maintainer should settle those four. (2) The deny_class: str | None = None field on the frozen DeniedCommandRule (src/kiro_crew/security.py:72-90) plus the matching docs/system-specs/modules/security.md field-list sentence — worth carrying only if the edition-remediation seam #7906 explicitly leaves open is wanted; it delivers nothing today in either PR. (3) Not from #7907 but must survive the close: both leave the git-publish gated floor's ID-led refusal (return _reason(tag, …), security.py:13126) on the anchor+subject scan, so git push origin fix-sso-login still draws SSO prose from its own branch name; #7906's new module docstring and its learn-cron-dashboard.md paragraph claim that floor is identity-routed, so either add setdefault(rule.id, …) to the index or correct the claim before merge (flagged by #7906's design lane and GPT lane; #7907 shares the gap silently).


From a repository-wide duplicate/overlap audit of every pull request open against main (2026-09-02, 330 PRs, one reviewer per PR). Each PR was read as its full merge-base diff plus its description and every comment and review, then compared against each candidate PR's own diff and against origin/main at 1a765b88ceb7. Findings that implied a closure were re-adjudicated independently, including an adversarial pass whose only job was to refute them; the reasoning above is what survived. If it is wrong, reopening costs nothing — please say so, and treat the reasoning rather than the outcome as the thing to correct.

@bolichen97 bolichen97 closed this Sep 2, 2026
@github-actions github-actions Bot removed the readiness: action required A blocking check or review needs attention label Sep 2, 2026
@bolichen97
bolichen97 deleted the feat/per-rule-deny-remediation branch September 6, 2026 03:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Per-rule deny remediation: ten exfiltration refusals get read guidance

2 participants