Skip to content

fix(security): classify redaction by what a value can do (awaiting design approval) - #22887

Open
bolichen97 wants to merge 1 commit into
mainfrom
feat/redaction-tiers-22552-combined
Open

bolichen97 wants to merge 1 commit into
mainfrom
feat/redaction-tiers-22552-combined

Conversation

@bolichen97

@bolichen97 bolichen97 commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Do not merge: waiting for design approval on #22552.

Problem / Motivation

Goal: Redact every secret and never an identifier, by deciding once, per rule, what a matched value can do.

Today each redaction rule decides sensitivity on its own. AWS access key IDs are removed from every log row and CloudTrail record, so an investigator loses the value they join on. A JSON field loses its label along with its value and stops parsing. A secret behind an upper-case or camel-case label, or in an escaped form, is missed. A guess by entropy for unlabelled values removes digests, request IDs and inline images, and needs a new exemption for every false positive.

Why it matters

People investigating incidents cannot read the logs and transcripts Kiro Crew writes, because identifiers are replaced. Secrets in common spellings still reach the screen, the log and chat channels. Every new false positive costs another one-off exemption, and those exemptions have been a source of leaks themselves.

Not a goal

  • Personal data (PII) redaction.
  • Rewriting rows already stored in transcripts; they stay as they are.
  • Validating keys against a vendor (no outbound calls).
  • Redacting the model's own turn (the backend hands raw tool output to the model).
  • A per-user "show identifiers" switch.

What changed (motivation → approach → change)

The design is in #22552 (tier model, section 3; detection, section 4); the tier decision is recorded in docs/decisions/2026-10-20-redaction-classifies-by-what-a-value-can-do.md.

  • Tiers. Every rule has a tier: Secret, Bearer link or Identifier. AWS key IDs are Identifier and stay visible everywhere; a signed link loses its signature and session token, not its key ID.
  • Labels. A labelled value keeps its label and loses only the value, so {"SecretAccessKey": "[REDACTED: credential]"} still parses. Labels match in any case and spelling, after =, :, =>, an XML tag or a sibling name/value pair; generic secret fields and Authorization Basic/Bearer are covered. The log filter now applies the same Secret tier.
  • Key pairs. A 40-character secret-shaped value next to an AWS key ID is a secret.
  • Normalisation. Rules also run over a percent-decoded, JSON-unescaped view with control and invisible characters removed; spans map back to the original text. Base64 decoding covers URL-safe and wrapped runs.
  • No entropy guess. The entropy pass is removed, with every exemption that existed only for it.
  • Streaming. The stream redactor holds a value while its label, its key ID or an escape sequence may still be arriving.
  • Vendor catalog. scripts/gen_secret_catalog.py generates src/kiro_crew/data/secret_catalog.generated.json from the Betterleaks rule set (MIT, licence kept in the file) at build time. Rules that need a filter program, a file path, or are not linear-time in Python's re are skipped; a finding whose value is an identifier shape is dropped. No new runtime dependency.
  • Publication gates. The deploy scan, the design preview's file check and get_credential_patterns() read the same records and labels; a deploy finding names its rule and length, never a character of the value.

Where. Shared spellings live in src/kiro_crew/credential_patterns.py and feed src/kiro_crew/log_redaction.py and src/kiro_crew/deploy/scan.py. The key-ID copies leave src/kiro_crew/sel.py, src/kiro_crew/metrics/schema.py, src/kiro_crew/decisions/gate.py, src/kiro_crew/security_posture.py, src/kiro_crew/platform/, the built-in apps under src/kiro_crew/apps/, src/kiro_crew/computer_use/, src/kiro_crew/testing/fake_computer_use.py, and the frontend website/src/utils/sanitize.ts, website/src/utils/errorReport.ts and website/src/apps/mochi/src/shared/utils.ts (signed links keyed on the signature in website/src/utils/credentialSignature.test.ts; fixtures in website/src/test/). setup.cfg packages the catalog; .github/ drops the removed module from a path list.

This carries the work of #22757, #22736 and #22737 in one commit.

Backwards compatibility

Breaking: AWS access key IDs are no longer redacted or blocked, an unlabelled value with no prefix, label or key ID beside it is no longer redacted, and the redacted text now keeps a value's label. Writer sweep on origin/main: every in-repo caller of the removed helpers is updated in this diff (listed below); stored transcripts are unchanged, and credential records naming the old rule id still resolve.

Reader: src/kiro_crew/security/exfil.py:_oauth_url_payload_diagnostic call site -- chat -- scans with _contains_fixed_credential only; test_security.py
Reader: src/kiro_crew/security/exfil_oauth.py:_safe_oauth_parameter_name -- chat -- same; test_security.py
Reader: src/kiro_crew/security/display_safety.py:sanitize -- chat -- same; test_display_redaction_source_guard.py
Reader: src/kiro_crew/dashboard/chat_utils_display.py:_display_scope_changes_battery -- crew page -- reads only the owner-view scope; test_display_time_redaction.py
Reader: src/kiro_crew/dashboard/handlers/member_dashboard.py:_template_text_safe -- crew page -- script and style text get the same pass; test_member_dashboard_routes.py
Reader: src/kiro_crew/dashboard/file_api/project_dirs.py:_redact_project_path -- crew page -- canonical redactor; test_project_git.py
Reader: src/kiro_crew/dashboard/display_verdicts.py:_rules_fingerprint -- crew page -- new rule files are in the digest, so stored verdicts are recomputed; test_display_time_redaction.py
Reader: src/kiro_crew/security/credential_sources.py:_AWS_TEMPLATE_KEYS -- chat -- keeps the old rule id for existing records; test_credential_sources.py
Reader: src/kiro_crew/security/stream_aws_anchor.py:names_a_key -- chat -- reads the shared label table; test_stream_redactor_aws_keyed.py

Tests

  • test/test_redaction_context_rules.py: everyday inputs (CloudTrail row, assume-role output, env dump, ~/.aws/credentials, escaped JSON, percent-encoded values, CSV pair, SigV4 header, presigned S3 and CloudFront links, base64 INI, Basic header) lose every secret and keep every identifier; label shapes beyond key=value; fields that name no secret stay; stream splits mid-label, mid-URL, after each separator, across invisible characters and beside a key ID.
  • test/test_secret_catalog.py: one test per ported vendor rule with a sample generated at test time from the rule itself, the Go-to-Python translation cases, the identifier veto, and a zero-noise check over the repository's own docs.
  • test/test_redaction_tiers.py, test/test_log_redaction.py, test/test_redaction_key_id_identifier.py: tiers for the new rules, the log Secret tier, and key IDs kept.
  • Tests that pinned the entropy pass or its exemptions are removed; tests that used a bare value as "some credential" now use a labelled or prefixed fake.

Manual verification

N/A — unit coverage sufficient: the change is text-in, text-out, and every surface is covered by the tests above. An independent read of the diff looked for secrets that survive; the shapes it found are fixed and pinned in test_redaction_context_rules.py.

Screenshots / video

Why no screenshot: the frontend change only swaps which strings the shared sanitizer replaces (key IDs stay, signing values go), so no panel, layout or component renders differently.

Related Issues

Closes #22552

Pattern harvest

Rule candidate: review-prompt
Pattern: a redaction rule that decides sensitivity from a value's shape instead of reading its tier from the shared table

Checklist

  • At most two commits (one is the norm), with a Conventional Commits title (feat|fix|docs|style|refactor|perf|test|chore|ci|build|revert: ...)
  • Existing tests pass and new tests added for new functionality
  • Self-review completed; code follows project style guidelines
  • Documentation updated (if applicable)
  • No secrets, credentials, or internal references in the diff

@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Sensitive Change Review

Sensitive files changed:

  • src/kiro_crew/log_redaction.py

  • src/kiro_crew/security/__init__.py

  • src/kiro_crew/security/_exports.py

  • src/kiro_crew/security/credential_sources.py

  • src/kiro_crew/security/denied_rules_catalog.py

  • src/kiro_crew/security/display_safety.py

  • src/kiro_crew/security/exfil.py

  • src/kiro_crew/security/exfil_oauth.py

  • src/kiro_crew/security/exfil_url_rules.py

  • src/kiro_crew/security/pass_compose.py

  • src/kiro_crew/security/redaction.py

  • src/kiro_crew/security/redaction_decode.py

  • src/kiro_crew/security/redaction_entropy.py

  • src/kiro_crew/security/redaction_labels.py

  • src/kiro_crew/security/redaction_normalise.py

  • src/kiro_crew/security/redaction_patterns.py

  • src/kiro_crew/security/redaction_switch.py

  • src/kiro_crew/security/secret_catalog.py

  • src/kiro_crew/security/stream_aws_anchor.py

  • src/kiro_crew/security/text_redaction.py

  • src/kiro_crew/security_posture_controls.py

  • src/kiro_crew/security_posture_sinks_delivery.py

  • No reasoning template for f4b2c33 yet.

How to clear this check:

  1. The PR author posts the template below as a PR comment, with
    every field filled (N/A needs a reason). Keep the
    Head commit line: it must name the current head SHA.
  2. The check turns green within a minute, with no push.
  3. A new push resets this check: post it again for the new head.

Only the author's comment counts this way. A reviewer can instead
paste the filled template into an "Approve" review on this head.
Merging still needs a reviewer's approval.

## Sensitive change review
- Head commit: f4b2c3357451574e98faaccf5d097697dd7331e5
- What rule changed:
- Before -> after (what was allowed/blocked, now):
- Worst case if wrong (breaks commands / leaks secret / locks user out):
- Evidence checked (which sandbox result proves it):
- No regression:
  - Tools still work (which tools/commands were run, result):
  - Backward compatible (old config / old data / old callers still work, how checked):
  - Existing tests (which suites passed on this head):
- How to undo:

@github-actions github-actions Bot added the readiness: checking Automated validation is still running label Oct 21, 2026
@bolichen97
bolichen97 enabled auto-merge (squash) October 21, 2026 20:05
@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

First Principles Review (Opus 5.5) — 🔴 BLOCK

Premise-level review of a5358d77fb88230010dd7e81ceca20cb45af0104 — 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 does not.

First-Principles-Verdict: BLOCK

Product-shape change without accepted RFC: AWS key IDs now stay visible in chat, channels, logs and links, and the entropy guess is removed. The only redaction RFC on base, a partial one, pins the opposite.

Notes

  • Item 6 — oversized: a generated Betterleaks catalog of vendor rules plus a generator script. The maintainer's decision record does not mention it, and the Goal is met by labels, prefixes and key-ID pairing. Subtraction: defer scripts/gen_secret_catalog.py, data/secret_catalog.generated.json and security/secret_catalog.py to their own PR.

What this change ships

Inventory (8 items) — 6 justified

Intent: let investigators read logs and transcripts while every real secret is still redacted. This is a FIX that also changes defaults.

  1. AWS access key IDs are no longer redacted or blocked anywhere — product shape, no RFC
  2. Unlabelled high-entropy values are no longer redacted — product shape, no RFC
  3. Redacted text keeps its label, so JSON still parses — justified
  4. Upper-case, camel-case, XML and name/value labels, and escaped secrets, are now caught — justified
  5. A 40-character value next to a key ID is redacted as its secret — justified
  6. A vendor-prefix catalog generated at build time from Betterleaks — oversized
  7. The stream redactor holds partial labels and escapes until they complete — justified
  8. Deploy scan findings name the rule and length, never any character of the value — justified

Blockers

product-shape change without accepted RFC — key IDs and entropy (items 1, 2)
Trigger: the diff says "AWS access key IDs are no longer redacted or blocked" and "The entropy pass is removed". The base RFC rfc-redaction-explain-and-reveal.md (status partial) says "Known credential formats such as AKIA … remain hidden everywhere … chat, every messaging channel, artifacts, logs, backups — including inside links." No RFC on the base list covers the tier model. The docs/decisions/ entry is neither of the two accepted forms, and the PR itself says "Do not merge: waiting for design approval on #22552."
Outcome: every operator's redaction default changes, and it contradicts the one RFC that governs redaction.
Clears when: an RFC covering the Secret/Bearer link/Identifier tiers (key IDs visible, entropy removed) is merged non-draft on the base, then a push or rebase; or a maintainer posts /ai-review override first-principles a5358d77fb88230010dd7e81ceca20cb45af0104 product-shape: <decision>.

[FIRST-PRINCIPLES-REVIEWED] a5358d7

Description read

[DESCRIPTION-READ] 8dc2246e52ce11038832e29a3e7ecbe36611298e46a4af3c3e0f7994885fbdd6

That sha256 names the PR description this verdict read. Recompute it with .github/scripts/pr-description-capture.sh; if it differs, the description changed after this verdict was formed and any finding drawn from the description is unproven.

@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 Oct 21, 2026
@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Opus 5.5 Review — 🔴 changes requested (blocking)

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

The streaming redactor this PR adds leaks labelled secrets that batch redact() catches, and it silently drops the rest of a reply after an unclosed PEM header or <password> tag. The label tier also hard-blocks deploys of ordinary minified login code.

BLOCKING — src/kiro_crew/security/text_redaction.py:941
for start, end in spans: / if start < hold_cut < end: / hold_cut = _token_start(self._buf, start)
Trigger: the dashboard, Slack or messaging StreamRedactor.feed receives 'x password: hunter2,secret: abc;db_pass: XYZ\n' in chunks of 1–4 characters. The single unsorted pass moves hold_cut back into the secret: span, and the cut lands inside the password: span, which the loop has already passed.
Outcome: the stream emits x password: hunter2,secret: [REDACTED: credential];…, so the password reaches the user surface in clear. Batch redact() gives password: [REDACTED: credential].
Anchor: backend-security-controls
Fix: iterate sorted(spans, reverse=True), or repeat until hold_cut stops moving, as _aws_label_cut / _label_safe_cut already do.

BLOCKING — src/kiro_crew/security/text_redaction.py:625
mid_line = credential_values.LINE_PREFIX.set(self._out_line)
Trigger: a streamed reply quotes JSON with escaped newlines, '{"output": "config:\\n auth_token=hunter2\\n host=x"}\n', at any chunk size from 1 to 8. _out_line only resets on a real "\n", so the window after the commit counts as mid-line, and the auth_token= label, which needs a line start, is never read.
Outcome: the stream emits auth_token=hunter2 raw, while batch redacts it to auth_token=[REDACTED: credential].
Anchor: backend-security-controls
Fix: treat a literal \n as a line break when updating _out_line, the same way the batch line-start reading does.

BLOCKING — src/kiro_crew/security/text_redaction.py:250
"pem": re.compile(r"(?:[^-]|-(?!----END [A-Z ]*PRIVATE KEY-----))*(?:-----END …)?") and "xml-value": re.compile(r"[^<]*")
Trigger: a reply that mentions -----BEGIN RSA PRIVATE KEY----- in prose with no END line, or says "put it inside <password>", and then keeps going past the hold reach. The fail-closed discard (_discard_kind = ("pem",) at :1111, ("xml-value",) at :1048) then consumes everything up to an END line or the next <.
Outcome: reproduced at every chunk size. The stream emits 30 of 9,252 characters (PEM case) or 53 of 5,656 (<password> case), and the rest of the reply is lost silently. Batch redacts only the header or the one word, and the base stream matched batch.
Anchor: residual/crash-data-loss-corruption
Fix: limit each discard to what the batch rule redacts. For PEM, stop at the first line that is not base64; for an XML value, stop at the batch xml_value_end (line end or value class), not at the next <.

BLOCKING — src/kiro_crew/credential_values.py:486
return _CODE_VALUE_RE.fullmatch(value) is not None or ( — only a one-character minified name is exempt. deploy.sh:151 then runs if any(is_credential_finding(f) for f in scan_content(data)):.
Trigger: a static-site bundle whose minified login code contains {username:u,password:Ze}. Two-letter names are what minifiers produce once the one-letter names run out. deploy.sh and _common.sh call scan_content, which now returns severity="credential" for it. The base returned no finding.
Outcome: the deploy aborts with "This finding cannot be overridden", so a site with a login form cannot be published. The same judgement rewrites password: pw inside member-dashboard <script> text now that redact_code_via_context is gone.
Anchor: defect
Fix: exempt short identifier-shaped values (a minified name of up to about 3 characters, or an identifier with no digits-and-mixed-case entropy) behind a label in code, or treat label-only deploy findings as overridable info.

[BLOCK-MERGE] a5358d7
[OPUS-REVIEWED] a5358d7

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

False positive or not applicable? A repository writer can comment:
/ai-review override fable a5358d77fb88230010dd7e81ceca20cb45af0104 false-positive: <reason, 20 to 500 characters>

@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

GPT 6.1 Review — 🔴 changes requested (blocking)

GPT 6.1 found at least one blocking issue that must be resolved before merging a5358d77fb88230010dd7e81ceca20cb45af0104. 2 of 2 blocking finding(s) are security-class and were withheld from adjudication, so the blocking verdict stands.

This comment is updated in place on each push.

BLOCKING -- src/kiro_crew/credential_values.py:740 -- Later quoted fields expose a folded YAML password

head = text[line_start:]
Trigger: Tool output contains a nested, two-space-indented password: with a four-space-indented continuation, followed by note: " text"; dashboard redaction → label_spans → config_value_end → folded_plain_end reaches this slice.
Outcome: key_indent reads the six spaces inside the later quoted field instead of the password key’s indentation. Only the first password line is redacted; its continuation survives in dashboard output and logs.
Anchor: residual/security
Fix: Calculate indentation from the prefix before the password key, excluding its value and subsequent lines.

BLOCKING -- src/kiro_crew/credential_patterns.py:245 -- Triple-quoted passwords starting with a newline bypass redaction

rf"""(?:{BLOCK_SCALAR_HEADER}|{LABEL_VALUE}|(?<=")(?=[^"\n])|(?<=')(?=[^'\n])"""
Trigger: Tool output contains a TOML password = """ whose content starts on the next line and has no recognizable vendor prefix; dashboard redaction → label_spans → _LABELLED_RE uses this fragment.
Outcome: Neither quoted branch accepts the newline, so no labelled match reaches value_extent, despite its triple-quote support. The complete password survives batch redaction, streaming redaction, and log filtering.
Anchor: residual/security
Fix: Permit a newline at the start of a quoted value and use value_extent to read its full extent.

[BLOCK-MERGE] a5358d7
[GPT-REVIEWED] a5358d7

Adjudication (Opus 5.5) — is blocking on each finding proportionate?

F1 — UNBOUNDED harm (the credential reaches the dashboard and logs, and nothing notices). Reproduced through security/text_redaction.redact: ' password: Xq7…\n Kp4Zw8Nc3Hy\nnote: " text"' leaves Kp4Zw8Nc3Hy unredacted. Changing the final line to note: plain redacts it. folded_plain_end at src/kiro_crew/credential_values.py:740 passes the rest of the text to key_indent at credential_values.py:235-238, which cuts the head after the last quote and returns 6. Every input condition is ordinary YAML that tools print, and there is no recovery path.

F2 — UNBOUNDED harm. Reproduced: 'password = """\nXq7Lm9Rt2VbKp4Zw8\n"""' is returned unchanged, while the same value on the opening line is redacted. Neither quoted branch at src/kiro_crew/credential_patterns.py:245 accepts a newline. Starting the value on the line after """ is common TOML. There is no recovery path.

[ADJUDICATION] a5358d77fb88230010dd7e81ceca20cb45af0104 total=0 uphold=0 downgrade=0
[GPT-ADJUDICATED] a5358d77fb88230010dd7e81ceca20cb45af0104

[ADJUDICATION-FENCED] a5358d77fb88230010dd7e81ceca20cb45af0104 fenced=2 flagged=0
UPHOLD-FENCED F1 src/kiro_crew/credential_values.py:740 -- Reproduced: a later quoted field with leading spaces makes key_indent return 6, so the folded password continuation stays unredacted; ordinary YAML triggers it and nothing recovers.
UPHOLD-FENCED F2 src/kiro_crew/credential_patterns.py:245 -- Reproduced: a TOML password whose triple-quoted value starts on the next line passes redaction unchanged; this layout is common and nothing recovers.
[GPT-ADJUDICATED-FENCED] a5358d77fb88230010dd7e81ceca20cb45af0104

False positive or not applicable? A repository writer can comment:
/ai-review override gpt a5358d77fb88230010dd7e81ceca20cb45af0104 false-positive: <reason, 20 to 500 characters>

@github-actions

Copy link
Copy Markdown
Contributor

Security Scope Review -- 🔴 could not complete (fails closed)

No scope verdict for aadfe699790147c27d10184cf5e28828b21b9864 (the candidate stage failed). This lane fails CLOSED: an unmeasured tightening must not read as "nothing newly refused". See the job logs, then re-run -- or record /ai-review override scope aadfe699790147c27d10184cf5e28828b21b9864: <reason> if a human has judged the scope by hand.

Comment thread src/kiro_crew/security/secret_catalog.py Fixed
@bolichen97
bolichen97 disabled auto-merge October 21, 2026 20:48
@bolichen97
bolichen97 force-pushed the feat/redaction-tiers-22552-combined branch from aadfe69 to ca60662 Compare October 21, 2026 21:25
@bolichen97

Copy link
Copy Markdown
Contributor Author
  • Streaming releases a secret before its following key ID arrives span=8c954628938f

fixed in ca60662: the stream holds a secret-shaped 40-character run, and any key ID, until the pairing reach after it has arrived (redaction_labels.stream_hold_start); pinned by test_a_label_or_key_id_shape_is_held_with_its_value_in_the_stream.

@bolichen97

Copy link
Copy Markdown
Contributor Author
  • Explicitly labelled passwords escape redaction when shaped like identifiers span=2b43374f05cb

fixed in ca60662: behind a secret label only a value that can only name something (key ID, account ID, ARN) is kept; a UUID or hex value is redacted. Pinned by test_a_hex_or_uuid_value_behind_a_secret_label_is_a_secret.

@bolichen97

Copy link
Copy Markdown
Contributor Author
  • Two-line base64 wrapping leaves recoverable secret bytes span=871d244cd480

fixed in ca60662: the wrapped-run pattern accepts one full line plus a last line, so a two-line run is claimed whole. Pinned by test_base64_wrapped_over_two_lines_is_redacted_whole.

@bolichen97

Copy link
Copy Markdown
Contributor Author
  • Publication warnings expose plaintext password characters span=52346a28f400

fixed in ca60662: a record-based deploy finding is described by rule and length only, no character of the value. Pinned by test_a_deploy_finding_carries_no_character_of_a_labelled_value.

@bolichen97

Copy link
Copy Markdown
Contributor Author
  • Per-match sorting stalls the gateway on credential-dense output span=50b2215ffa8d

fixed in ca60662: _claim no longer re-sorts per span; each span is checked against the already-sorted added and taken lists by bisect. 64,000 labelled rows redact in about 1.4 s, linear.

@bolichen97

bolichen97 commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor Author
  • design_tweak preview serves labelled AWS secrets and Bearer headers span=712f553b6342

fixed in ca60662: preview_files.contains_credential also refuses any AWS-label or Authorization span from label_spans; test_a_labelled_aws_secret_or_bearer_header_is_refused pins both shapes.

@bolichen97

bolichen97 commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor Author
  • Stream anchors miss the no-separator label form and a secret before its key ID span=8d4983ffcc76

fixed in ca60662: the stream holds from a label word, a no-separator AWS label, a name/value pair or a secret-shaped run until its reach arrives; pinned at chunk sizes 1-64 by test_a_label_or_key_id_shape_is_held_with_its_value_in_the_stream.

@bolichen97

Copy link
Copy Markdown
Contributor Author
  • product-shape change without accepted RFC span=bfc88d73796a

needs-a-decision: the tier model (key IDs visible, no entropy guess) is the design under review in #22552, and this PR is held for that design approval. Only a maintainer can record it, by an RFC on main or /ai-review override first-principles <head>; the agent does not post this override.
Note on item 9 (catalog duplicates hand-written prefixes): kept on purpose. The hand-written alternation is the pre-filtered fast path that the stream holdback, the log floor and get_credential_patterns() read; catalog rules are keyword- and entropy-gated, regenerated per upstream release and may be skipped, so they add coverage and do not replace that floor.

@bolichen97
bolichen97 force-pushed the feat/redaction-tiers-22552-combined branch from ca60662 to 6a1aab9 Compare October 21, 2026 22:39
@bolichen97

bolichen97 commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor Author
  • Records repeat a label the text now keeps span=be5fce1c4c0f

fixed in 6a1aab9: a record carries no label now that the label stays in the cleaned text, so a card or a copy shows the field name once (test_redaction_credential_records).

@bolichen97

bolichen97 commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor Author
  • Type annotations and attribute reads behind a label are redacted span=61e11dd40c95

fixed in 6a1aab9: an unquoted value that is a call, a subscript or type parameter, a union, a dotted, constant, variable or type name is code, not a value; the AWS label branch exempts calls, subscripts and dotted reads (test_source_code_that_names_or_reads_a_secret_is_kept).

@bolichen97

Copy link
Copy Markdown
Contributor Author

GPT 6.1 findings on 457b4ddc96: all five are fixed in 0a8f57815d8eacecf1098cc0083a0e0cf16c3a49. F1 and F3 follow the owner's fail-closed design decision; no residual and no override. New tests: 43 cases for F1/F3 and 9 for F2/F4/F5 are red on 457b4ddc96 and green on 0a8f57815d.

F1: plain words behind a strong label were exempt. In credential_values.is_exempt_value, a strong label exempts only a value that is visibly code (_CODE_VALUE_RE):

  • a call (load_key());
  • a dotted or indexed read (settings.key, os.environ['K']);
  • a snake_case or SNAKE_CASE identifier, or a camelCase identifier (each capital followed by lower case, so a random token's mixed case is not one);
  • $VAR or ${VAR}.

A plain word is redacted at any length or case (password: hunter, api_key=None). A weak label (pwd, a glued _token) keeps the old length and word tests. A value with no letter or digit (==) is an operator.

F3: config values stopped at , and ;. credential_values.config_value_end recognises a config line: optional indentation, export or a YAML - , then the key and = or : (dotenv, shell, YAML, ini, TOML). The value is a literal:

  • KEY=value (shell form) runs to the first blank or &;
  • key: value and key = value run to the line end or a blank-and-# comment;
  • both run through , and ;.

Behind a strong label only an exact $VAR/${VAR} is kept, plus a call, dotted or indexed read, since no config file writes one as a literal. A key ID, account ID or ARN stays an Identifier. Batch (label_spans), the log filter (_labelled_value_end, _keep_*) and the stream all use it. The stream holds an open config line to its end (open_config_value), bounded by QUOTED_VALUE_HOLD_REACH; past that it drops to the value's end. It judges a line's start on what it already emitted on that line (credential_values.LINE_PREFIX), so a window that starts mid-line reads the line as the whole text does.

  • F2: Authorization scheme padding. After Bearer, Basic or token, any run of horizontal blanks is accepted ([^\S\n]+), in batch, log and the stream trigger.
  • F4: a long sibling value expired its hold. open_name_value holds a name/value pair whose unquoted value is still open, as far as a quoted value; past that the value goes as one tag and the rest is dropped.
  • F5: a YAML block past its reach. The block's label is found in the whole held buffer. Past the reach, the header is committed, the body goes as one tag, and lines are dropped (block-N) until one returns to the label's indent. That indent is read with the already-committed part of the line, and indentation cut by a chunk is carried. A closing tag still arriving (</pass) is held too, so </password> never reads as a label.

Tests updated to the decided design:

  • password: required, api_key=None, password=/usr/local/bin and password = new_password as config lines are now redacted examples.
  • Non-config path and code examples are kept.
  • test_promotion_ignores_client_content_and_reads_the_source uses a dotted value.
  • The .env mutation fixture uses ${SECRET}, so only the name gate can block it.

Battery: 27,130 passed. The remaining failures were the line budget (updated to 34,318) and those two fixtures. Web: 853 passed.

@bolichen97

Copy link
Copy Markdown
Contributor Author

F1 and F3 on 457b4ddc96: fixed by a design change, made fail-closed by the conductor under the owner's standing authority. They are not residuals and have no override. Head: b1228c3ff91f340e5f8ad0b9e0ba1beccc86da64, which supersedes 0a8f57815d and narrows its config-line form to the decided one. F2, F4 and F5 stay fixed as in the previous disposition.

F1: a plain word behind a strong label. Behind a strong secret label (password, passwd, pwd, secret, token, api_key and the rest), a value is exempt only when it is visibly code:

  • a call name(...);
  • a dotted or indexed read (a.b, a[...]);
  • an identifier with _, or camelCase (new_password, getToken);
  • $VAR or ${VAR}.

A plain word (hunter2, letmein, a short lower-case word) is a literal and is redacted at any length and case. Weak labels keep their old tests only off config lines. A value with no letter or digit (==) is an operator.

F3: config-line values stopped at , and ;. The config-line form is NAME=value at the line start with nothing around the = (dotenv, ini, shell, with export), or YAML key: value (with - ). Behind a strong label there, the value is always Secret, except an exact $VAR or ${VAR}. No word or code exemption applies, and every secret label counts as strong (DB_PASS). The unquoted value runs to the line end or to a blank-and-# comment, through ,, ; and &. key = value with blanks around the = is code, so the F1 rule judges it. A key ID, account ID or ARN keeps its Identifier tier, and the shell's PWD / OLDPWD stay, in batch and log alike.

Batch, log and stream agree. All three use credential_values.config_value_end. The stream holds an open config line to its end (open_config_value), AWS labels included, bounded by the quoted-value reach; past it, it drops to the line end. It judges a line's start on what it already emitted on that line (LINE_PREFIX). A decoded view (password%3D...) is not a config file, so config form is off inside views, in the redactor and in the prefilter oracle.

Tests (red on the previous heads, green here):

  • test_visibly_code_behind_a_strong_label_is_kept: password = new_password and token = get_token() stay visible, in batch, log and stream.
  • test_a_config_line_value_is_a_literal_read_to_its_line_end: PASSWORD=hunter2, password=letmein, DB_PASS=a,b;c, DB_PASSWORD=new_password, secret: settings.db_secret, a # comment, and & in a value. These are redacted, and stream equals batch at chunk sizes 1, 7 and 64.
  • test_a_plain_word_behind_a_strong_label_is_redacted and test_a_config_line_keeps_only_an_exact_reference_or_an_identifier.
  • On 0a8f57815d, 7 of these cases fail.

Tests updated to the design:

  • Line-start AWS stream fixtures and percent-quoted fixtures now start mid-line (x ), so they keep testing the AWS and encoded-quote drops rather than a config line's literal.
  • access_token: Optional[str] at a line start is YAML form now, so its keep example moved into a signature.

Battery: 27,142 passed. Web: 853.

Comment thread src/kiro_crew/credential_values.py Fixed
Comment thread src/kiro_crew/credential_values.py Fixed
Comment thread src/kiro_crew/credential_values.py Fixed
@bolichen97

bolichen97 commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor Author

The GPT 6.1 finding on b1228c3ff9 and the two Opus 5.5 findings on 0a8f57815d are fixed in 4dfc45fb57d04f5e47a28c32c6bfef2067b541c7. The new tests are red on b1228c3ff9 (27 cases) and green here.

  1. GPT: PEM private-key bodies streamed past the floor. Only the header phrase was held, so once it was committed the body lines had no context left. StreamRedactor now tracks an open PEM block: a complete -----BEGIN ... PRIVATE KEY----- with no -----END line yet, or with its end line still at the buffer end. It holds the block from its header as far as an 8192-bit key runs (_PEM_HOLD_REACH). Past that, it fails closed: the text before is committed, the block goes as one tag, and the rest is dropped through the -----END line (discard kind pem, with the end line carried when a chunk cuts it). The stream equals the batch pass. Test: test_a_pem_private_key_never_streams_its_body covers a 2048-bit key, a larger one and one past the reach, with and without text after, at chunk sizes 1, 7, 64 and 500. No body line streams.
  2. Opus: a key with / inside a URL slipped the bundle gate. When no /-separated segment of a URL run is long enough to hold a 40-character key, _scan_bare_secret_runs now judges the run whole. A source permalink, whose 40-hex SHA is a segment of its own, is still split and passes. Test: test_the_bundle_gate_refuses_a_key_with_slashes_inside_a_url (?k=<key> and a path key). The permalink tests still pass.
  3. Opus: the decode pass held the event loop on large binary base64. _holds_secret now treats a decode with more than 5% non-text characters as binary (_is_binary), for example base64 logo.png. It reads that decode for the fixed patterns only, with no normalised views and no context rules over random bytes. On 1 MB of base64, redact_credentials takes 1.4 s, down from 9.9 s. Most of what remains is the catalog on the raw text and its one-time load. Test: test_a_binary_decode_is_read_for_fixed_patterns_only, which patches through the facade and fails if a view is built. A text decode is still read in full.

FP passed on b1228c3ff9, and Opus on it failed on infrastructure (no [OPUS-DISCOVERY] marker). Battery: 27,158 passed. The 10 other failures in that run were contention; they pass alone (298 passed), except the facade-manifest entries, which are now registered.

@bolichen97

bolichen97 commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor Author

GPT 6.1 findings on 6e2be1afa7: both are fixed in af65c03a3eac6589267fa8c04727ea9bf5ac7c74 at the root. The new tests are red on 6e2be1afa7 (6 cases) and green here.

  1. F1: non-ASCII decoded text counted as binary. _is_binary measured the share of all non-ASCII characters, so base64 JSON with a Cyrillic or CJK password skipped the context rules. It now counts only control characters, which are what random bytes decode to. Printable Unicode in any script is text. The large-blob guard still holds: 1 MB of base64 takes 1.46 s. Test: test_an_encoded_non_ascii_password_is_text_not_binary (Cyrillic, CJK and Spanish, batch and stream).
  2. F2: the caption exemption ran before the config-line literal rule. In _value_is_secret, _is_caption (<label>Password:</label>) now applies only off config lines, so password: password and password: Password are redacted in batch and stream, as the log filter already did. Tests: test_a_config_value_equal_to_its_label_is_redacted, and test_a_caption_in_markup_still_stays, which checks markup captions still stay.

Opus on 6e2be1afa7 failed on infrastructure (no [OPUS-DISCOVERY] marker). Its last review, on 0a8f57815d, is answered in the earlier disposition. Battery: 27,177 passed.

@bolichen97

Copy link
Copy Markdown
Contributor Author

The GPT 6.1 and Opus 5.5 findings on af65c03a3e are fixed in 14f0ff400efeaa3c175b531c65396d7d0afcfd2a at the root. The new tests are red on af65c03a3e (7 Python cases, 3 web cases) and green here.

  1. GPT F1: code-reference exemptions applied to literal data. XML element content and a sibling value field are now read as literals, like a config line's value. Only an exact $VAR/${VAR} is kept; no word or code exemption applies (label_spans and the log filter's _on_config_line). <password>correct_horse_battery</password> and - name: DB_PASSWORD / value: correct_horse_battery are redacted in batch and stream. Tests: test_xml_content_and_sibling_values_are_literals, test_a_reference_in_xml_content_is_still_kept.

  2. GPT F2: binary decodes skipped labelled text. _holds_secret on a binary decode now does two things:

    • It drops the NULs of UTF-16 text (PowerShell -EncodedCommand); if what is left is text, it reads that in full.
    • Otherwise it reads the fixed patterns, then every printable run of 16 or more characters (a PNG tEXt chunk, EXIF, XMP) through the full rules, joined. Random bytes seldom form such runs, so 1 MB of binary base64 still takes about 1.5 s.

    Tests: test_a_labelled_secret_in_a_binary_decodes_text_chunk_is_found (a PNG with aws_secret_access_key= in tEXt) and test_utf16_encoded_text_is_read_as_text. test_a_binary_decode_is_read_for_fixed_patterns_only now checks that no view spans the binary whole.

  3. GPT F3: HTML-escaped query separators. The log filter's _QUERY_START_RE and the TS scanner now count &amp;, &#38; and &#x26; as a query separator, so &amp;s%69g= is read as sig in a query. Tests: test_an_html_escaped_query_separator_still_reads_sig and the matching sanitizeCredentials suite.

  4. Opus: the encoded-bitmap scan blanked real images. _scanned_bitmap_bytes ran the whole redactor over the near-random base64 body, so vendor rules matched by chance. The encoded body is now searched only for an AWS key ID, the one structured marker that fits the base64 alphabet, at ~1.6e-7 per 20 KB. The decoded bytes are still redacted in full. Test: test_an_ordinary_bitmap_body_is_not_condemned_by_chance_vendor_shapes. The glued key-ID test still passes.

Battery: 27,187 passed. Web: 856 passed.

@bolichen97

bolichen97 commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor Author

GPT 6.1 findings on 14f0ff400e: all three are fixed in 89e5991dcb729e7079b369d4630615c35687a714 at the root. The new tests are red on 14f0ff400e (5 cases) and green here.

  1. F1: views and decodes lost the document's vendor context. _credential_redaction_plan now finds the input's catalog keywords once (catalog_keywords_in) and sets them as KEYWORD_CONTEXT around the whole plan (_planned_spans). So a normalised-view window or a base64 decode that no longer holds the # Airtable heading still selects the vendor's rules, as the plain text does. Test: test_a_vendor_keyword_far_before_an_encoded_token_still_selects_the_rule (8 KB of prose, then a percent-encoded or plain PAT).
  2. F2: null and boolean literals were redacted into invalid syntax. Off a config line, an exact null, NULL, None, nil, undefined, true, false, True or False behind a label is a language literal and stays (const password = null;, {"password": null}). A config line still reads its value as a literal first, so PASSWORD=false and password: null stay redacted, as does a word like nullish. Tests: test_a_null_or_boolean_literal_behind_a_label_is_kept, test_a_config_literal_or_a_word_like_null_is_still_redacted.
  3. F3: a URL run of only slashes crashed the bundle gate. max(..., default=0). Test: test_a_url_run_of_only_slashes_does_not_crash_the_bundle_gate.

The regex-linearity shape test now reads _planned_spans, where the passes live. Battery: 27,194 passed. The other two failures in that run were that shape test and the line budget, both updated. Opus on 14f0ff400e failed on infrastructure again.

Comment thread src/kiro_crew/credential_values.py Fixed
@bolichen97

Copy link
Copy Markdown
Contributor Author

GPT 6.1 findings on be09090e68: both are fixed in 67f70511c7730fa8786483d52a08a7f71a80cd5b at the root. The new tests are red on be09090e68 (12 cases) and green here.

  1. F1: a YAML doubled quote ended a password. In credential_values.value_extent, a doubled single quote inside a value ('it''s', as in YAML and SQL) is one quote, not the close. An empty '' still closes at once. The past-the-bound reader and the stream's quoted-sq drop read it the same way, and the stream carries a single quote at the buffer end until the next chunk shows whether it is doubled. Tests: test_a_yaml_doubled_quote_does_not_end_a_password (short, nested, and a 5 KB value past the reach, at chunk sizes 1, 7 and 64, batch, stream and log) and test_an_empty_single_quoted_value_still_closes_at_once.
  2. F2: URL query values were taken for code identifiers. A secret-labelled value right after ?, &, %3F, %26 or an HTML-escaped & is a URL query value, which is data. credential_values.in_url_query marks it, and batch (label_spans), the log filter (_on_config_line) and the stream read it as a literal, so no code-reference exemption applies. A stream window that starts mid-line reads the ? or & from what the line already emitted. Test: test_a_url_query_value_is_a_literal (?password=correct_horse_battery, &password=...&x=1 with the next parameter kept, and &amp;api_key=userApiKey).

Opus on be09090e68 failed on infrastructure (no [OPUS-DISCOVERY] marker). FP passed. Battery: 27,209 passed. The local backend lint gates all pass (black, line cap, subprocess encoding, sync IO, isort, flake8, mypy).

@bolichen97

Copy link
Copy Markdown
Contributor Author

[operator: bolichen97] Disposition for GPT 6.1 on 67f7051, fixed on 5a79d39.

F1 (credential_patterns.py:186), punctuation-prefixed config passwords: fixed.

  • Root cause: LABEL_VALUE cannot start with a delimiter (;, [, \, {, ...), so the labelled match failed before config_value_end could read the line.
  • Fix: LABEL_VALUE_OR_QUOTED gains DELIMITER_LED_VALUE, a zero-width match before such a delimiter. On a config line config_value_end extends it to the line end (or a # comment), and the config rule judges the whole value as a literal: Secret unless it is an exact $VAR/${VAR} or a redaction tag. Off a config line the empty value is no value, as before. Batch, log (_labelled_value_end) and stream (open_config_value) share the pattern.
  • The MCP launch display's own digest mask (KEY=<hidden sha256:xxxxxxxx>) is a fixed point on a config line, as a tag is (_DIGEST_MASK_RE, exact shape only).
  • Tests: test_a_config_value_led_by_a_delimiter_is_redacted (DB_PASSWORD=;..., password=[..., API_KEY=\..., password: {abc}..., a trailing # note kept, export API_TOKEN=&..., - secret: (..., <hidden Xq...>; batch, log, stream at 1/7/64) and test_a_tag_or_code_after_a_label_is_unchanged, red on 67f7, green here.

Verification: backend lint gates (black, isort, flake8, mypy, line cap, comment history, flake ratchet, semgrep) pass; full battery 27,237 passed. F1/F3 tests remain on the head: test_a_plain_word_behind_a_strong_label_is_redacted, test_visibly_code_behind_a_strong_label_is_kept, test_a_config_line_value_is_a_literal_read_to_its_line_end, test_a_config_line_keeps_only_an_exact_reference_or_an_identifier.

@bolichen97

Copy link
Copy Markdown
Contributor Author

[operator: bolichen97] Disposition for GPT 6.1 and Opus 5.5 on 5a79d39, fixed on 132a1f1.

GPT F1 (redaction_labels.py:942), block scalars in JSON-escaped strings: fixed. open_escaped_block_scalar holds a label whose |/> header is followed by an escaped break (\n, \r\n, doubled one level deeper) until a line returns to the label's indent or the string closes; past the hold the header is committed and the rest of the string is dropped (fail closed). The log floor reads the same body (escaped_block_scalar_end). Literal CRLF block headers were also unread in batch; BLOCK_SCALAR_HEADER and the stream patterns now take \r?\n. Tests: test_a_block_scalar_in_an_escaped_string_never_streams (0/1/2 JSON layers, LF and CRLF, sizes 1/7/64), test_a_long_escaped_block_scalar_drops_its_body_past_the_hold.

GPT F2 (credential_values.py:327), await: fixed. An unquoted await/new/yield/typeof/lambda followed by a blank is code. With no operand, or on a config line, it is still redacted. Tests: test_a_keyword_that_takes_an_operand_is_code and test_an_operand_keyword_with_no_operand_or_on_a_config_line_is_redacted.

GPT F3 (scan.py:340), inline images: fixed without dropping the canonical-mode bare-secret scan. Removing that scan is the guard removal Opus flagged earlier. Instead, a run is exempt only when it follows a data:image|audio|video|font/...;base64, header and decodes to that format's leading bytes (PNG, JPEG, GIF, RIFF, ICO, WOFF, Ogg, ID3, EBML, ftyp). Random bytes under the same header are still flagged. Test: test_an_inline_image_is_no_bare_secret_but_random_bytes_under_its_header_are.

Opus, credential_patterns.py:133:

  • Fixed: a bare > separates only when it is glued to the label (<password>), so if password > threshold is kept.
  • Fixed: a value is never carried across a line break onto a line that is itself key:/key= (VALUE_SPACE), so secret:\n secretName: my-tls is kept.
  • Tests: test_a_comparison_or_a_mapping_after_a_label_is_no_value, test_an_xml_element_or_a_next_line_value_is_still_redacted.
  • Not changed: plain words and type names behind a strong label (password: str, password:t), quoted literals, URL query values, and a config line read to its end (password=abc user=bob). These follow the fail-closed F1/F3 design decision recorded on this PR and are referred back to it.

Opus, stream_aws_anchor.py:47: fixed.

  • A generic label's strong-anchor cut is the word the label ends, not the whitespace-free token. Compact JSON ({"id":123,"name":"alice","secret":false},×120) now streams whole and equals batch.
  • A run of glued labels (password=×4000) drops to the first label within the floor of the end, so the buffer stays at or under the 4096 ceiling and the cost is linear: 2.6 s at ×4000 and 5.4 s at ×8000, down from 10 s and growing at ×4000.
  • Tests: test_compact_json_with_a_generic_label_streams_whole, test_a_run_of_glued_labels_keeps_the_stream_buffer_bounded.

Opus note, SECRET_ACCESS_KEY=\ continuation: fixed. A config value ending in an unescaped \ continues onto the next line, for up to 8 lines, in batch, log and stream. Tests: test_a_continued_config_value_is_read_onto_its_next_line, test_an_escaped_backslash_at_a_line_end_does_not_continue_it.

Opus, deploy/scan.py:186 (label-only records are hard and non-overridable): not changed in this push. It changes a deploy gate's severity and is referred to the design decision.

First Principles BLOCK (product-shape, no accepted RFC): the decision is recorded in #22577 and approved on #22552. The owner-authorized override will be posted on the final head.

Verification: backend lint gates (black, isort, flake8, mypy, line cap, comment history, flake ratchet, semgrep) pass. Full battery: 27,285 passed, plus one facade-export test, now fixed: the packaging suite passes 782. The F1/F3 tests remain on the head.

Comment thread src/kiro_crew/credential_values.py Fixed
Comment thread src/kiro_crew/credential_values.py Fixed
Comment thread src/kiro_crew/credential_values.py Fixed
Comment thread src/kiro_crew/credential_values.py Fixed
Comment thread src/kiro_crew/credential_values.py Fixed
Comment thread src/kiro_crew/credential_values.py Fixed
@bolichen97

Copy link
Copy Markdown
Contributor Author

[operator: bolichen97] Disposition for GPT 6.1 and Opus 5.5 on 132a1f1, plus Opus's q1/q2 asks from 5a79d39. Fixed on 0bae467. (89e0dd1 was pushed with one facade-manifest entry missing; it is replaced by this head.)

GPT F1 (credential_values.py:399), padded INI keys: fixed. Inside an INI section, password = value is now a config literal read to the end of its line. An INI section is a [section] header followed only by keys, comments and blank lines. Batch reads the section from the text in one cached pass (_ini_section_lines). The stream carries the section state across commits (INI_SECTION, updated from emitted lines). password = new_password with no header above it is still code, as the F1/F3 decision requires. Tests: test_a_padded_key_in_an_ini_section_is_a_literal (a punctuated value, a header 400 keys back, sizes 1/7/64, and a hard deploy finding) and test_a_padded_assignment_outside_an_ini_section_is_code.

GPT F2 (scan.py:378), public digests: fixed without removing the canonical-mode bare-secret scan. That scan is a guard, as Opus showed earlier. A run that is exactly the shape of a base64 SHA-256 (43 characters + =) or SHA-512 (86 characters + ==) is exempt. A 40-character key never has that shape. Test: test_a_base64_sha_digest_is_no_bare_secret.

GPT F3 (credential_values.py:508), YAML null and boolean values: fixed. In the YAML form only (key: value), a core-schema null or boolean (~, null, true, false and their case forms) is a typed value, not a string. PASSWORD=false and PASSWORD=null in dotenv are still literals and are redacted. Test: test_a_yaml_null_or_boolean_is_no_secret, which also checks that the deploy scan has no hard finding.

Opus (redaction_decode.py:249), binary decodes in _encoded_blocks: fixed as suggested. _decoded_blocks reads a binary decode the way _holds_secret does: it strips UTF-16, applies the fixed patterns, and runs the full rules on its printable runs only, never on views over the raw bytes. _encoded_blocks on a 300 KB inline PNG now takes 0.13 s, and on 1.5 MB 0.67 s. Test: test_a_binary_decode_blocks_only_on_its_fixed_patterns_and_text_runs.

Opus q1, plain words behind a strong label: changed in part.

  • Fixed: a type name from a closed list is now kept: str, string, bytes, int, number, bool, boolean, any, unknown, object, None, null, undefined, Optional, SecretStr. Generic wrappers such as Optional[str] are read as subscripts. A minified one-character name ({password:t}) is kept too. Both are kept only when code punctuation (, ) = ; } ] > |), a blank or the line end follows them. Tests: test_a_type_name_or_a_minified_name_behind_a_label_is_code (batch, stream, log and deploy scan) and test_a_literal_or_a_near_miss_of_a_type_is_still_redacted.
  • Fixed, logfmt: on a config line, a blank followed by name= starts the next field. So password=abc user=bob hides only abc, while PASSWORD=a b c is hidden whole. Tests: test_a_logfmt_value_ends_at_the_next_field, test_a_single_assignment_still_reads_to_its_line_end.
  • Not changed: a quoted literal ({"password":"Password"}) and a URL query value (?token=abc123) stay redacted. Redaction: classify by what a value can do (Secret / Bearer link / Identifier) #22552 §3 fails closed on secrets. A quoted value behind a secret label is the literal the writer chose, and nothing in the text proves it is not the password, while a leak cannot be undone.

Opus q2 (deploy/scan.py:186), severity: severity is unchanged. The deploy scan already reads the shared predicate (redact_credentials_with_records). With the exemptions above, password:t, password: str and password: null now produce no finding at all. Everything that is still a finding stays a hard 409.

Verification: backend lint gates (black, isort, flake8, mypy, line cap, comment history, flake ratchet, semgrep) pass. Full battery: 27,319 passed. The F1/F3 tests remain on the head.

@bolichen97

Copy link
Copy Markdown
Contributor Author

[operator: bolichen97] Disposition for GPT 6.1 on 0bae467 and Opus 5.5 on 89e0dd1. Fixed on eb690bf.

GPT F1 (redaction_labels.py:180), Python string prefixes: fixed. STRING_PREFIX (b, r, u, f and two-letter forms, only right before a quote) is part of the separator, so the quoted value behind password = b"…" is read whole in batch, log and stream (the stream anchor takes it too). Test: test_a_python_prefixed_string_is_read_whole.

GPT F2 (redaction_decode.py:49), short URL-safe base64: fixed. _B64URL_SHORT_RE reads a whole 24-39 character URL-safe run that holds a - or _. Both short readers' boundaries now cover both alphabets. Test: test_a_short_url_safe_base64_password_is_redacted.

GPT F3 (text_redaction.py:939), continued config values past the hold: fixed. A config value past its hold reach drops as config-line, which runs through every line a \ at the line end continues it onto. A lone \ at the buffer end is kept in the drop until the next chunk arrives. An aws-keyed or other unquoted drop that stops at a continuation switches to config-line. Test: test_an_overflowing_config_value_drops_its_continued_lines (generic and AWS labels, chunk sizes 1 and 512).

GPT F4 (scan.py:324), bare-secret snippet: fixed. The finding shows only (N chars), no character of the secret. Test: test_a_bare_secret_finding_shows_no_character_of_it.

GPT F5 (text_redaction.py:1028), clamped block indent: fixed. A block-N drop is built for the label's own indent N (_DiscardRuns); nothing is clamped. Test: test_a_deeply_indented_block_scalar_keeps_its_sibling_past_the_hold.

Opus 1 (text_redaction.py:1175), a long base64 run's tail: fixed. The stream records how far into its last 4-character group a commit left a base64 run (_b64_phase). The held rest of the run is decoded from the run's own boundary, both in _encoded_secret_run and when a window that continues the run is committed. Test: test_a_long_base64_run_never_streams_its_encoded_token (several alignments, chunk sizes 1, 3 and 64).

Opus 2 (redaction_patterns.py:63), AKIA key IDs and bare 40-character secrets: by design, not changed. That is the tier model recorded in #22577 and approved on #22552. A key ID is an Identifier and stays visible. Every Secret-tier value is still redacted: one with a key ID beside it, a label or a prefix. The entropy guess for a value with none of these is removed. First Principles' product-shape item covers this, and the owner's override is posted on the final head.

Opus 3 (secret_catalog.py:152), cost on large output: fixed.

  • xml_value_end now does one search to the next <, so a run of labels is linear. "<password>"*40000 dropped from 28.7 s to 1.2 s.
  • catalog_keywords_in checks substrings per keyword above 4 KB. 1 MB of prose dropped from 2.1 s to 1.15 s.
  • The tool-result handler redacts outputs over 64 KB on a worker thread (asyncio.to_thread), so a large result no longer stalls the gateway loop.
  • Test: test_a_run_of_xml_labels_is_read_once_each.

Opus 4 (deploy/scan.py:219), severity: not changed. Downgrading a deploy gate is outside this PR. The deploy scan reads the shared predicate, so code shapes (password:t, password: str, YAML null) already give no finding. A client-public key tier is recorded as an owner decision item.

Opus note, MYSQL_PWD= in logs: fixed. The log filter's working-directory test reads the name glued in front of the label, so only PWD= and OLDPWD= stay visible. Test: test_a_prefixed_pwd_is_a_password_in_the_log_too.

Verification: the backend lint gates (black, isort, flake8, mypy, line cap, comment history, flake ratchet, semgrep) pass. The full battery passed 27,349, and its one failure was the package line budget, now raised to the measured 34,677 and passing. The F1/F3 tests remain on the head.

@bolichen97

Copy link
Copy Markdown
Contributor Author

[operator: bolichen97] Disposition for CI, CodeQL, GPT 6.1 and Opus 5.5 on eb690bf. Fixed on ec63898.

CI Backend Tests (3.12 shards 1, 3, 5, 8; Windows shards 1, 3, 5, 8): fixed. All six failing tests pass on fresh main and fail on eb690bf, so they are regressions from this branch.

  • test_acp_frame_record::…whatever_its_shape (18 cases, KeyError: 'v'): this was a real bug. The recorder probes a dict key under a credential key by joining them (apiSecret: v). The config-line rule now reads that join as a literal. _scrub_dict_key already rules on keys under a credential ancestor (a field-name shape is kept), so it now skips the joined probe for those ancestors. It still probes the others.
  • test_agent_capabilities_http (args14): 9DB_PASSWORD=ordinary and DB.PASSWORD=ordinary are now masked. Under the fail-closed label rule, a plain word behind a strong label is a literal. The row now expects [0, 1], and the copy-secret probe keys on p7! actually being in the args.
  • test_dashboard_chat_api_chat, test_members_dm_thread, eventlog::test_redact_projection_value_scrubs_short_url_secret_param: the value is redacted, but by the batch rule first, so the tag is [REDACTED: credential]. These tests pinned the log floor's [REDACTED]. Nothing leaks; the expected tag is updated.
  • test_dynamic_dashboard_cards: aws_secret_access_key: rotated is a YAML literal and is redacted. The parity case now uses a value the text scan keeps (${ROTATED_KEY}).

CodeQL: 7 new high alerts (py/polynomial-redos in credential_values.py), fixed. None of these regexes is tried at every position any more:

  • _CODE_VALUE_RE, _CONFIG_KEY_SEP_RE, _INI_KEY_SEP_RE, _INI_SECTION_RE, _INI_BODY_RE are anchored with \A…\Z. They match a bounded slice, the key or the line.
  • _BLOCK_HEADER_RE and _ESC_BLOCK_HEADER_RE are anchored and read on the header's own line (_anchored_end) or a 128-character slice.
  • _LOGFMT_NEXT_FIELD_RE starts at a name behind a fixed-width blank lookbehind, with a bounded name length.
  • _ESC_BREAK_RE and its siblings in redaction_labels take bounded backslash runs behind (?<!\\).
  • VALUE_SPACE's lookahead is bounded.

GPT F1 (redaction_normalise.py:73), %20 together with %22: fixed. A new chain keeps encoded blanks, quotes and backslashes together and decodes the label, the separator and &. So password%3Dab%20cd%22<rest> is read whole, and an encoded & after a value still ends it. Test: test_an_encoded_value_with_an_encoded_blank_and_quote_is_read_whole.

GPT F2 (text_redaction.py:116), long CSI sequences: fixed. _csi_start walks back over a CSI's parameter bytes at any length, bounded by the 4096 ceiling, to its ESC [. A long sequence inside a token never adds a commit boundary. Test: test_a_long_terminal_escape_inside_a_token_never_splits_it (20 and 200 parameters, sizes 1/7/64).

Opus (redaction_labels.py:91), --secret-key <key>: fixed. _AWS_SHAPED_RE also accepts a secret / secret key name (--secret-key, --secret_key, --secret) before a value of exactly the key's 40-character shape, with only blanks between. Batch and the log floor share it (shaped_secret_spans). Tests: test_a_secret_flag_before_a_key_of_its_shape_is_redacted and test_a_secret_flag_before_any_other_value_is_unchanged.

Verification: the backend lint gates (black, isort, flake8, mypy, line cap, comment history, flake ratchet, semgrep) pass. The local battery passed 27,365. The six CI-failing modules plus agent_capabilities passed 1,007. A local CodeQL run was not available; the next CodeQL run on this head confirms the alerts.

@bolichen97

Copy link
Copy Markdown
Contributor Author

[operator: bolichen97] #22887 was CONFLICTING on ec63898, so I rebased it onto fresh main as f4cb752. The content is the same as ec63898 (disposition 6389764613).

Two test files conflicted. main had folded tests into parametrized forms: test/test_chat_runner_coverage_output.py (the OAuth URL gate) and test/test_slack_sessions_view.py (credential redaction). I kept main's form and carried over this branch's change: the credential sample is glpat-… instead of AKIA…, because a key ID is Identifier tier and stays visible.

After the rebase, the conflicted files, the CI-failing modules, the line budget and the facade guards passed 896; the local battery passed 27,396; mypy, black and the line cap pass.

@bolichen97

Copy link
Copy Markdown
Contributor Author

[operator: bolichen97] Disposition for GPT 6.1 on f4cb752. Fixed on 11fbc2d.

F1 (credential_values.py:119), CDATA values: fixed.

  • Batch and log: an XML-labelled value that opens a <![CDATA[ section now runs through its ]]>. If the section is still open, it runs to the end of the text. A < inside the section no longer ends it.
  • Stream: open_xml_value holds from the element's tag while the section, or its opener cut by the chunk (<password><![C), is open, and until the closing tag arrives.
  • Deploy: scan_content reports a hard credential finding for it.
  • Test: test_a_cdata_value_behind_an_xml_label_is_read_whole (AWS and generic labels, an inner <, sizes 1/7/64, deploy finding).

F2 (credential_values.py:89), multi-line quoted YAML: fixed.

  • value_extent reads a quoted value across a line break when the next line is indented deeper than the value's first line. This is how YAML continues a quoted scalar.
  • A line not indented deeper still ends the value at the break.
  • A break at the buffer end keeps the value open, so the stream holds it until the next line's indent arrives.
  • Tests: test_a_quoted_yaml_value_over_indented_lines_is_read_whole (both quotes, sizes 1/7/64, sibling kept) and test_a_quoted_value_ends_at_a_line_not_indented_deeper.

Other lanes on f4cb752:

  • CodeQL passed with no new alerts, so the polynomial-ReDoS rewrites hold.
  • First Principles passed.
  • The Opus run was an infra failure twice ("Capture discovery candidates" step; review step skipped); it reruns on this head.

Verification: the backend lint gates pass. The local battery passed 27,409. The modules that failed in CI earlier passed 626.

@bolichen97

bolichen97 commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor Author

[operator: bolichen97] Disposition for GPT 6.1 on 11fbc2d. Fixed on 9adca1b.

F1 (credential_values.py:123), blank lines in quoted YAML: fixed. _continued_quote_line treats blank lines inside a quoted scalar as part of it: they fold to a line break. The next line that has text decides whether the value goes on. A buffer that ends inside blank lines keeps the value open, so the stream holds it. Test: test_a_blank_line_inside_a_quoted_yaml_value_does_not_end_it.

F2 (credential_values.py:160), XML values over lines: fixed.

  • xml_value_end reads an element's content to its closing </ across line breaks, and also when the content starts on the next line.
  • In the stream, open_xml_value and the xml-value drop span lines too.
  • A closing tag held by the stream (</password>) is held together with the element it closes, so the content is never committed apart from its tag.
  • Test: test_an_xml_value_over_lines_is_a_literal (batch, stream 1/7/64, log, deploy hard finding).

F3 (credential_patterns.py:236), triple-quoted literals: fixed. The separator reads """/''' as one opener (TRIPLE_OR_QUOTE). value_extent reads a triple-quoted value over lines to its closing triple quote, also from the stream trigger's single-quote view. Test: test_a_triple_quoted_password_is_read_to_its_triple_quote (single-line and multi-line, the line after kept).

F4 (credential_values.py:568), encoded config lines: fixed.

  • A normalised view is now read with its first line on the source line its window starts on (reading_view). A view line is therefore a config line exactly where the source line is one, or where a decoded %0A starts one.
  • Inside a view, a \ does not continue a line, since it may be a decoded escape, and an & ends the value, since it joins query parameters.
  • A label mid-line (?q=password%3Dnew_password) is still code.
  • The prefilter oracle in test_credential_prefilter.py now uses the same reader and joins touching spans, as the real view pass does.
  • Tests: test_an_encoded_config_line_is_still_a_config_literal and test_an_encoded_label_mid_line_is_still_code.

Other lanes on 11fbc2d:

  • CodeQL passed.
  • First Principles is the product-shape BLOCK that the owner's override covers on the final head.
  • Opus failed on infra on both attempts: "Discovery produced no [OPUS-DISCOVERY] marker". It reruns on this head.

Verification: the backend lint gates pass. The local battery passed 27,431, and the earlier CI-failing modules passed 626.

@bolichen97

Copy link
Copy Markdown
Contributor Author

[operator: bolichen97] Disposition for GPT 6.1 on 9adca1b. Fixed on 2550983.

F1 (credential_patterns.py:138), next-line values containing : or =: fixed.

  • VALUE_SPACE stops at a line only when that line is a real YAML mapping key (key: then a blank or the line end) or an unindented dotenv NAME= line.
  • A : or = inside a scalar (AbCd1234:efGh5678, AbCd=1234) is the value.
  • secret: then secretName: my-tls is still a mapping.
  • Test: test_a_next_line_value_with_a_colon_or_equals_is_still_the_value (batch, stream, log, deploy hard finding).

F2 (credential_values.py:657), folded plain YAML values: fixed.

  • On a YAML config line, folded_plain_end reads the value on through the lines YAML folds into it. Each such line is indented deeper than the key's line and is not a key, a list item or a comment. Blank lines between them fold too.
  • Dotenv lines do not fold.
  • The stream's open_config_value holds a YAML value from its key while only its continuation or blank lines follow.
  • Tests: test_a_folded_plain_yaml_value_is_read_through_its_lines (sizes 1/7/64, siblings kept) and test_a_list_item_or_a_dotenv_line_does_not_fold.

F3 (scan.py:297), standalone media bypass: fixed. In standalone mode, a base64 run that decodes to binary is no longer skipped. Its printable runs (a PNG tEXt chunk, EXIF, XMP) are read by the same _HARD_PATTERNS, so a labelled secret in an inline image's metadata is found. Test: test_a_secret_in_an_inline_image_text_chunk_is_found_without_the_redactor.

Other lanes on 9adca1b:

  • First Principles is the product-shape BLOCK that the owner override covers on the final head.
  • Opus failed again before any review: "Discovery produced no [OPUS-DISCOVERY] marker". This is its fifth infra failure in a row.

Verification: the backend lint gates pass. The local battery passed 27,442, the earlier CI-failing modules plus the prefilter oracle passed 1,035, and the line budget was re-measured at 34,771.

@bolichen97

Copy link
Copy Markdown
Contributor Author

[operator: bolichen97] Disposition for GPT 6.1 on 2550983. Fixed on a7238fa, which is also rebased onto fresh main: #22887 had gone CONFLICTING.

F1 (text_redaction.py:225), long values past the stream's hold: fixed. When a value runs past the hold, the stream drops it through the value's own close:

  • quoted-dq and quoted-sq go on across a line break when the next line is indented or blank, as a YAML quoted scalar folds.
  • A triple-quoted value drops as quoted-triple-dq / quoted-triple-sq through its closing triple quote, over lines.
  • A CDATA value drops as xml-cdata through its ]]>, past any < it holds. The drop is over once the ]]> is taken.
  • A line break, a quote or two, or a ] or two at the buffer end stays in the drop until the next chunk shows which it is.
  • Batch: value_extent past QUOTED_READ_MAX still reads an indented continuation line. The log filter uses the same reader.
  • Test: test_a_long_value_past_the_hold_drops_through_its_own_close (a 5,100-character value in YAML double and single quotes, a triple quote, and CDATA; chunk sizes 1, 64 and 1024; batch and log; the text after the value kept).

Rebase:

  • One conflict: main raised the security package line budget by 1, to 32,527, for member-records. Both notes are kept, and the budget is re-measured on the rebased tree at 34,803 (+2,276 from 32,527).
  • 2550983 was also pushed with the budget at a placeholder (33,000). This head corrects it.

Lanes on 2550983:

  • First Principles is the product-shape BLOCK that the owner override covers on the final head.
  • Opus failed again before reviewing (no [OPUS-DISCOVERY] marker, on both attempts).

Verification: the backend lint gates pass. The local battery passed 27,480. The earlier CI-failing modules, the prefilter oracle and the two files main touched passed 1,226.

@bolichen97

Copy link
Copy Markdown
Contributor Author

[operator: bolichen97] Disposition for GPT 6.1 on a7238fa. Fixed on 5d8d178.

F1 (credential_values.py:756), quoted config values behind weak labels: fixed.

  • A quoted value on a config line (DB_PASS="AbCdEfG", db_pass: 'AbCdEfG') is a literal, as its unquoted form is.
  • quoted_config_value uses the same line, key and separator test as config_value_end, now shared as _config_line_start.
  • Batch (label_spans) and the log floor (_on_config_line) both read it.
  • The weak-label length and word tests do not apply to such a value.
  • Off a config line, a quoted value keeps the weak-label tests: {"pwd": "deny"} and x db_pass="AbCdEfG".
  • Tests: test_a_quoted_config_value_behind_a_weak_label_is_a_literal (batch, stream, log, deploy hard finding) and test_a_quoted_word_off_a_config_line_keeps_the_weak_label_tests.
  • One existing case moved: in test_code_that_names_a_secret_is_left_alone, lane_pass: '\u7eff\u706f' alone on a line is now a config literal. It now sits in code, const lanes = {lane_pass: '…'}, where the weak-label test still keeps it.

Lanes on a7238fa:

  • First Principles is the product-shape BLOCK that the owner override covers on the final head.
  • Opus failed again before reviewing, with no [OPUS-DISCOVERY] marker.

Verification: the backend lint gates pass. The local battery passed 27,487; its one failure was the case moved above, which now passes. The earlier CI-failing modules and the prefilter oracle passed 1,035. The line budget was re-measured at 34,807.

@bolichen97

Copy link
Copy Markdown
Contributor Author

[operator: bolichen97] Disposition for GPT 6.1 on 5d8d178. Fixed on 903e7a2.

F1 (credential_values.py:786), short quoted passwords behind weak labels: fixed. A quoted value behind a weak label (db_pass, pwd) now keeps only the word test. The length test is gone. So {"db_pass": "Ab1$cD"} and {"pwd": "x9!Q"} are redacted in batch, stream and log, while {"pwd": "deny"}, {"pwd": "on"} and a word of a script with no letter case (\u7eff\u706f) are kept.

Tests: test_a_short_quoted_password_behind_a_weak_label_is_redacted and test_a_quoted_word_behind_a_weak_label_is_kept. In last round's test_a_quoted_word_off_a_config_line_keeps_the_weak_label_tests, the off-line case is now a word (x db_pass="deny"); a mixed-case non-word there is now a secret.

Lanes on 5d8d178:

  • First Principles: the product-shape BLOCK, which the owner override covers on the final head.
  • Opus: failed again before reviewing (no [OPUS-DISCOVERY] marker).

Verification: the backend lint gates pass. The local battery passed 27,495. The earlier CI-failing modules and the prefilter oracle passed 1,035.

@bolichen97

Copy link
Copy Markdown
Contributor Author

[operator: bolichen97] Disposition for GPT 6.1 on 903e7a2. Fixed on c51a175.

F1 (redaction_labels.py:1034, credential_values.py:704), long folded YAML values: fixed.

  • No line caps remain. folded_plain_end reads a plain YAML value through every line folded into it, up to its indentation boundary. The quoted-scalar reader past QUOTED_READ_MAX does the same.
  • open_config_value looks back over every continuation line in the held buffer for the value's key.
  • Past the hold, a YAML config value drops as block-N (N = its key's indent): the rest of its line, then every line indented deeper or blank, the same as a block scalar's body. It is never released line by line.
  • Test: test_a_long_folded_yaml_value_keeps_its_label_to_its_end (70 lines of about 100 characters each, chunk sizes 7, 64 and 1024, batch and log; the next key is kept).

F2 (credential_values.py:357), vendor Identifier tier behind a generic label: fixed.

  • names_vendor_identifier returns whether a value is, whole, the value of an Identifier-tier catalog rule whose keyword is in its context (twilio-api-key: SK<32 hex> beside twilio).
  • label_spans keeps such a value behind a generic label, so deploy gives no finding.
  • The same string behind password:, with no vendor keyword, is still a secret.
  • Tests: test_a_vendor_identifier_keeps_its_tier_behind_a_generic_label and test_the_same_string_behind_a_password_label_is_still_a_secret.

Lanes on 903e7a2:

  • First Principles is the product-shape BLOCK that the owner override covers on the final head.
  • Opus again failed before reviewing (no [OPUS-DISCOVERY] marker).

Verification: the backend lint gates pass. The local battery passed 27,501 and the earlier CI-failing modules passed 1,035. The line budget was re-measured at 34,847.

@bolichen97

Copy link
Copy Markdown
Contributor Author

[operator: bolichen97] Disposition for GPT 6.1 on c51a175. Fixed on dea0017.

F1 (credential_patterns.py:152), backtick-quoted values: fixed.

  • The separator takes a backtick as an opening quote (TRIPLE_OR_QUOTE), and the value grammar opens after it.
  • value_extent reads a template literal over lines to its unescaped closing backtick.
  • The batch reader, the log floor's quote test and the stream's label trigger all accept it.
  • Test: test_a_backtick_password_is_read_to_its_closing_backtick (single-line and multi-line with an escaped backtick, sizes 1 and 7, batch and log).

F2 (redaction_labels.py:121), name/value fields in any order: fixed.

  • _object_name_value_spans reads every quoted value field of the flat object that holds a secret-naming name field, in any field order and with any fields between them (CodeBuild's sorted name, type, value; or value before name). Each one gets the name's rule, so the AWS name keeps its blocking tier and deploy gives a hard finding.
  • The stream holds a value field while its object is still open and within reach, so a name that follows it is read with it.
  • A value beside an ordinary name (STAGE) is kept.
  • Tests: test_a_name_and_value_pair_in_any_order_is_one_secret (both orders, sorted JSON, sizes 1/7/64, deploy) and test_a_value_beside_an_ordinary_name_is_kept.

Lanes on c51a175:

  • First Principles is the product-shape BLOCK, which the owner override covers on the final head.
  • Opus again failed before reviewing, with no [OPUS-DISCOVERY] marker.

Verification: backend lint gates pass. Local battery 27,512 passed; earlier CI-failing modules 1,035 passed. Line budget re-measured at 34,897.

@bolichen97

Copy link
Copy Markdown
Contributor Author

[operator: bolichen97] Disposition for GPT 6.1 on dea0017. Fixed on 1274e13.

F1 (text_redaction.py:500), a value streamed before its secret name: fixed. The stream's name hold now starts at a value field earlier in the same object ({"value":"...", "name":"DB_PASSWORD"}). The whole pair reaches one window once the object closes, and the stream no longer cuts between them. Test: test_a_value_before_its_secret_name_never_streams (opaque values with blanks, punctuation and braces; chunk sizes 1, 3, 7 and 64).

F2 (redaction_labels.py:149), braces inside quoted values: fixed. _object_bounds and the stream's _same_object find an object's edges with strings skipped, so a { or } inside a quoted value is no boundary. Each object keeps its own name. Tests: the ab{cd}ef case above, and test_each_object_keeps_its_own_name.

F3 (redaction_labels.py:146), upper-case fields: fixed. The pre-check is now case-insensitive ("value" in text.lower()), the same as the matchers. Test: test_upper_case_sibling_fields_are_one_secret, which includes a deploy hard finding.

Lanes on dea0017: First Principles is the product-shape BLOCK that the owner override covers on the final head.

Verification: the backend lint gates pass. The local battery passed 27,534, and the earlier CI-failing modules passed 1,035. The line budget was re-measured at 34,965.

@bolichen97

Copy link
Copy Markdown
Contributor Author

[operator: bolichen97] Disposition for CI, CodeQL and GPT 6.1 on 1274e13. Fixed on 1e2b431.

CI Backend Tests (3.12, 3) and (Windows, 3): fixed. test/test_dashboard_elicitation.py::test_display_redaction_touches_labels_and_taints_values used AKIAIOSFODNN7EXAMPLE1234 as its "secret". A key ID alone is now Identifier tier and stays visible, so the test now uses a Secret-tier token (glpat-…). It passes on main only because main still redacts key IDs. It recurred because my local battery did not include that file. The battery now also runs every test module that mentions AKIA or a redactor entry point: 294 modules, 20,932 passed.

CodeQL (1 new high): fixed. test/test_markdown_link_parentheses.py:90 re.match(r"\[\^(?:\\.|[^\]])*\]", …) could backtrack exponentially on \\ runs, because [^\]] also matched the backslash that \\. consumes. The class now excludes the backslash: (?:\\.|[^\]\\])*. The line predates this PR, but CodeQL counts it as new because this PR edits the file (its samples moved from AKIA… to glpat-…).

GPT F1 (redaction_labels.py:622), sibling fields far apart in the stream: fixed.

  • open_object_field holds a secret-naming name field, or a quoted value field, of an object still open at the buffer end, as far as the batch rule's object reach. The hold is like a quoted value's.
  • object_pair_extents keeps a pair already whole in the buffer from being cut between its fields, by the natural cut or by the 512 floor.
  • Test: test_sibling_fields_far_apart_never_stream_apart (an 800-character field between them, both orders, chunk sizes 7, 64 and 1024).

GPT F2 (text_redaction.py:1230), long backtick values: fixed. A backtick value past the hold now drops as quoted-bt: over lines, through its unescaped closing backtick. Test: test_a_long_backtick_value_drops_through_its_closing_backtick.

Verification:

  • The backend lint gates pass.
  • The local battery passed 27,543. The 294 redaction-touching modules passed 20,932.
  • The line budget was re-measured at 35,021.

@bolichen97

bolichen97 commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor Author

[operator: bolichen97] Disposition for GPT 6.1 on 1e2b431. Fixed on a5358d7.

F1 (credential_values.py:235), YAML list-item values swallowing sibling fields: fixed.

  • key_indent gives the column a key sits at with sequence indicators counted. In - password: |, the key is at column 2, so its body is the lines indented deeper than 2, and username: alice ends it.
  • A key's own opening quote ( "password": |) counts as part of the key. Text before an earlier quote ({"c": "- password: |) is the string the YAML sits in, so it is not counted.
  • Every reader uses it: block scalars, escaped block scalars, folded plain values, quoted continuations, the stream's open_block_scalar and block_scalar_header, and the config drop past the hold.
  • The batch and stream readers also read the part of the line the stream already emitted (LINE_PREFIX), so a - committed earlier still counts.
  • Test: test_a_list_item_value_ends_at_its_sibling_field (block, folded, quoted, escaped and quoted-key forms; sizes 1/7/64; batch, stream and log; the sibling field is kept).

Verification:

  • The backend lint gates pass.
  • The battery now includes every module that mentions AKIA or a redactor. It passed 42,008.
  • The line budget was re-measured at 35,024.

Every redaction rule now carries a tier (Secret, Bearer link, Identifier),
decided once by what a value can do, not guessed from its shape.

- AWS access key IDs (AKIA/ASIA) are Identifier tier and stay visible on
  every surface; a signed link loses its signature and session token, not
  its key ID.
- Labelled values keep their label and lose only the value
  (`api_key = [REDACTED: credential]`), so structured output still parses.
  Labels match case-insensitively with the usual word separators, cover
  generic secret fields and Authorization Bearer/Basic, and are shared with
  the log filter, which now redacts the Secret tier too.
- A 40-character secret-shaped value next to an AWS key ID is the secret
  half of the pair; the key ID stays.
- Every rule also runs over a normalised view (percent-decoded,
  JSON-unescaped, control and invisible characters dropped), with spans
  mapped back to the original text. Base64 decoding covers URL-safe and
  wrapped runs.
- The entropy guess for unlabelled values is removed, with every exemption
  that existed only for it (document and diagram links, commit permalinks,
  macOS temp-dir ids, the code-text scope).
- The stream redactor holds a value with any secret label while it may
  still be arriving.
- A vendor token catalog is generated at build time from the Betterleaks
  rule set (MIT) by scripts/gen_secret_catalog.py into
  src/kiro_crew/data/secret_catalog.generated.json; a finding whose value
  is an identifier shape is dropped. No new runtime dependency.

Closes #22552
@bolichen97

Copy link
Copy Markdown
Contributor Author

[operator: bolichen97] Disposition for GPT on a5358d7, fixed on f4b2c33:

  • F1 (credential_values.py, folded plain YAML indent read from a later quoted field): fixed. folded_plain_end now takes the config key's own start and measures the indent from the line start to that key, so a later quoted field on the line cannot set it. Red/green: test_a_later_quoted_field_does_not_set_a_folded_value_indent.
  • F2 (credential_patterns.py, a triple-quoted value whose quote is followed by a newline was not read): fixed. LABEL_VALUE_OR_QUOTED accepts an empty match right after """ or ''' at end of line, so the value body on the next lines is consumed through its close. Red/green: test_a_triple_quoted_value_opening_on_the_next_line_is_read.

Local gate (black, isort, flake8, mypy, repo checks, comment-history, semgrep) and the 294-module redaction battery (42,017 passed) are green.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

blocked Waiting on another issue or external system readiness: action required A blocking check or review needs attention

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Redaction: classify by what a value can do (Secret / Bearer link / Identifier)

3 participants