From e976b65433e8e1bb9fef2187a665503a9468aa4c Mon Sep 17 00:00:00 2001 From: Tom Softreck Date: Mon, 19 Oct 2026 16:20:55 +0200 Subject: [PATCH] fix(queue): prevent redact_secrets from corrupting code excerpts and repair missing + on closing delimiters (ticket-467, Closes #728) --- project/TICKETS.md | 2 + project/ticket-467/README.md | 22 +++++++ project/ticket-467/intent.json | 79 +++++++++++++++++++++++++ src/koru/queue/diff_repair.py | 61 +++++++++++++++++-- src/koru/queue/patch_mode.py | 9 ++- src/koru/queue/transaction/preflight.py | 1 - tests/test_planfile_queue.py | 44 ++++++++++++++ 7 files changed, 211 insertions(+), 7 deletions(-) create mode 100644 project/ticket-467/README.md create mode 100644 project/ticket-467/intent.json diff --git a/project/TICKETS.md b/project/TICKETS.md index abc8051b..4aefbcfd 100644 --- a/project/TICKETS.md +++ b/project/TICKETS.md @@ -461,4 +461,6 @@ project analysis or human-owned ticket inputs. | **ticket-463** | [`README.md`](./ticket-463/README.md) | - | - | - | - | - | | **ticket-464** | [`README.md`](./ticket-464/README.md) | - | - | - | - | - | | **ticket-465** | [`README.md`](./ticket-465/README.md) | - | - | - | - | - | +| **ticket-466** | [`README.md`](./ticket-466/README.md) | - | - | - | - | - | +| **ticket-467** | [`README.md`](./ticket-467/README.md) | - | - | - | - | - | diff --git a/project/ticket-467/README.md b/project/ticket-467/README.md new file mode 100644 index 00000000..c00fe573 --- /dev/null +++ b/project/ticket-467/README.md @@ -0,0 +1,22 @@ +# Ticket 467: fix(queue): prevent redact_secrets from corrupting code excerpts and repair missing + on closing delimiters (#728) + +- **ID**: ticket-467 +- **Owner**: agent:antigravity +- **Status**: IN_PROGRESS +- **Workflow state**: EDIT +- **Created**: 2026-10-19 + +SESSION_EXECUTION_AUTHORIZATION: User authorized autonomous resilience and prevention repairs in `semcod/koru` (`#728`) observed while delegating tickets through `willman` / `willops`. + +## Goal and scope + +Prevent `redact_secrets` in `src/koru/queue/patch_mode.py` from corrupting code expressions (`===`, `new Set([`, `decryptJson(...)`, `this.key = encryptionKey`) when quoting files in `current_file_excerpt` and `build_verify_retry_prompt`, and recover unmarked closing delimiters (`}`, `};`, `)`, `);`, `]`, `];`) plus trailing-blank `pre_ctx` anchors in `src/koru/queue/diff_repair.py`. + +## Acceptance criteria + +- [x] AC-01: `PYTHONPATH=src pytest -q -o addopts='' tests/test_planfile_queue.py -k 'redact or context_excerpt' tests/test_queue_evidence.py -k 'secrets'`, `python3 -m ruff check src/koru/queue/patch_mode.py src/koru/queue/diff_repair.py tests/test_planfile_queue.py`, and `./project/governance-check.sh` pass. + +## Tracking boundary + +This directory contains the minimal reviewed intent. Optional participant prose +and raw command logs are not required delivery output. diff --git a/project/ticket-467/intent.json b/project/ticket-467/intent.json new file mode 100644 index 00000000..b7fba529 --- /dev/null +++ b/project/ticket-467/intent.json @@ -0,0 +1,79 @@ +{ + "schema": "new-project.intent/v3", + "ticket": "ticket-467", + "summary": "fix(queue): prevent redact_secrets from corrupting code excerpts and repair missing + on closing delimiters (#728)", + "workstream": "application", + "classification": { + "kind": "BUG", + "priority": "P1", + "origin": "health" + }, + "allowedPaths": [ + "project/ticket-467/**", + "TODO.md", + "project/TICKETS.md", + "src/koru/queue/diff_repair.py", + "src/koru/queue/patch_mode.py", + "src/koru/queue/transaction/preflight.py", + "tests/test_planfile_queue.py" + ], + "forbiddenPaths": [ + "project/ticket-*/user-*.md" + ], + "stacks": [], + "dependsOn": [], + "conflictsWith": [], + "integrationTicket": null, + "delivery": { + "acceptedBaseSha": "8b00aaa907526dad25f88295147cdb459338141b", + "targetBranch": "main", + "outcome": "Prevent redact_secrets in src/koru/queue/patch_mode.py from corrupting code expressions in current_file_excerpt and build_verify_retry_prompt, and recover unmarked closing delimiters and trailing-blank pre_ctx anchors in src/koru/queue/diff_repair.py (#728).", + "nonGoals": [ + "Do not weaken credential redaction for quoted or env-style secret assignments." + ], + "complexity": "S", + "estimatedMinutes": 20, + "budgets": { + "maxImplementationFiles": 5, + "maxAffectedComponents": 1, + "maxPublicInterfaceChanges": 0, + "maxRuntimeDependencies": 0 + }, + "architecture": { + "status": "accepted", + "decision": "Narrow _SECRET_PATTERNS in src/koru/queue/patch_mode.py to quoted and env-style secret assignments and add _try_promote_unmarked_closing_delimiter plus non-empty pre_ctx anchor resolution in src/koru/queue/diff_repair.py.", + "components": [ + { + "name": "queue-patch-repair", + "paths": [ + "src/koru/queue/patch_mode.py", + "src/koru/queue/diff_repair.py", + "src/koru/queue/transaction/preflight.py", + "tests/test_planfile_queue.py" + ] + } + ], + "responsibilityChanges": false, + "interfaceChanges": [], + "dataChanges": [], + "ui": { + "impact": "none", + "states": [], + "evidence": [] + }, + "rollback": "Revert commit for ticket-467." + }, + "runtimeDependencies": [], + "validation": [ + { + "criterion": "AC-01", + "commands": [ + "PYTHONPATH=src pytest -q -o addopts='' tests/test_planfile_queue.py -k 'redact or context_excerpt' tests/test_queue_evidence.py -k 'secrets'", + "python3 -m ruff check src/koru/queue/patch_mode.py src/koru/queue/diff_repair.py tests/test_planfile_queue.py", + "./project/governance-check.sh" + ], + "evidence": "project/ticket-467/README.md" + } + ] + } +} diff --git a/src/koru/queue/diff_repair.py b/src/koru/queue/diff_repair.py index 0498c926..e0ee859f 100644 --- a/src/koru/queue/diff_repair.py +++ b/src/koru/queue/diff_repair.py @@ -521,15 +521,22 @@ def _apply_edit_subblocks_fallback( else: ins_pos: int | None = None if pre_ctx: - anchor = pre_ctx[-1].strip() - if anchor: + non_empty = [ + (idx, line.strip()) + for idx, line in enumerate(pre_ctx) + if line.strip() + ] + if non_empty: + last_idx, anchor = non_empty[-1] + trailing_blanks = len(pre_ctx) - 1 - last_idx best_dist = len(working) + 1000 for cand, w_line in enumerate(working): if w_line.strip() == anchor: - dist = abs(cand + 1 - expected_start) + target_pos = min(len(working), cand + 1 + trailing_blanks) + dist = abs(target_pos - expected_start) if dist < best_dist: best_dist = dist - ins_pos = cand + 1 + ins_pos = target_pos if ins_pos is None and post_ctx: anchor = post_ctx[0].strip() if anchor: @@ -552,6 +559,45 @@ def _apply_edit_subblocks_fallback( return working, changed_any +_CLOSING_DELIMITER_STRIPS = frozenset({"}", "};", ")", ");", "]", "];"}) + + +def _try_promote_unmarked_closing_delimiter( + working: list[str], + new_chunk_template: list[tuple[str, str]], + expected_start: int, +) -> tuple[list[tuple[str, str]], list[str], int] | None: + """Recover when a model omits the leading '+' on a closing delimiter right after '+' lines.""" + candidates: list[list[tuple[str, str]]] = [] + for idx in range(1, len(new_chunk_template)): + kind, text = new_chunk_template[idx] + if kind == " " and text.strip() in _CLOSING_DELIMITER_STRIPS and new_chunk_template[idx - 1][0] == "+": + promoted = list(new_chunk_template) + promoted[idx] = ("+", text) + candidates.append(promoted) + if idx + 1 < len(new_chunk_template) and new_chunk_template[idx + 1] == (" ", ""): + promoted_with_blank = list(promoted) + promoted_with_blank[idx + 1] = ("+", "") + candidates.append(promoted_with_blank) + for cand_tpl in candidates: + cand_old = [t for k, t in cand_tpl if k in (" ", "-")] + m_len = len(cand_old) + if not m_len or m_len > len(working): + continue + cand_strip = [x.strip() for x in cand_old] + best_dist = len(working) + 1 + best_pos: int | None = None + for i in range(len(working) - m_len + 1): + if [w.strip() for w in working[i : i + m_len]] == cand_strip: + dist = abs(i - expected_start) + if dist < best_dist: + best_dist = dist + best_pos = i + if best_pos is not None: + return cand_tpl, cand_old, best_pos + return None + + def _apply_hunks_with_fuzzy_fallback( orig_lines: list[str], hunk_blocks: list[list[str]] ) -> tuple[list[str], bool] | None: @@ -623,6 +669,13 @@ def _apply_hunks_with_fuzzy_fallback( if dist < best_dist: best_dist = dist match_pos = i + if match_pos is None: + promoted_delim = _try_promote_unmarked_closing_delimiter( + working, new_chunk_template, expected_start + ) + if promoted_delim is not None: + new_chunk_template, old_chunk, match_pos = promoted_delim + n = len(old_chunk) if match_pos is not None and match_pos >= 0: matched_disk_lines = working[match_pos : match_pos + n] diff --git a/src/koru/queue/patch_mode.py b/src/koru/queue/patch_mode.py index 3c77a2e7..f97a7867 100644 --- a/src/koru/queue/patch_mode.py +++ b/src/koru/queue/patch_mode.py @@ -259,10 +259,15 @@ def wrap_reply_in_proposal_envelope( _SECRET_PATTERNS = ( - # Assignments: API_KEY=..., token: "...", password = '...' + # Quoted secret assignments: API_KEY="...", token: "...", password = '...' re.compile( r"(?i)\b([A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL)[A-Z0-9_]*)" - r"(\s*[:=]\s*)(\"[^\"]*\"|'[^']*'|\S+)", + r"(?:])(\s*[:=](?![=>:])\s*)(\"[^\"\n]{4,}\"|'[^'\n]{4,}')", + ), + # Unquoted env/CLI secret assignments: OPENAI_API_KEY=sk-live-... + re.compile( + r"(?i)\b([A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL)[A-Z0-9_]*)" + r"(?:])(=)(?![=>])([A-Za-z0-9_\-./+=]{6,})(?=$|\s|[,;\"'])", ), # Bearer / Authorization headers. re.compile(r"(?i)\b(authorization|bearer)(\s*:?\s+)(\S+)"), diff --git a/src/koru/queue/transaction/preflight.py b/src/koru/queue/transaction/preflight.py index 8295844f..42b371b1 100644 --- a/src/koru/queue/transaction/preflight.py +++ b/src/koru/queue/transaction/preflight.py @@ -39,7 +39,6 @@ from koru.queue.transaction.result import PatchPlan from koru.queue.types import CommandResult - #: A ticket opted into ``inputs.strict_files`` and the patch touches files it did not declare. PATCH_TARGETS_OUTSIDE_DECLARED_FILES = "patch_targets_outside_declared_files" diff --git a/tests/test_planfile_queue.py b/tests/test_planfile_queue.py index a2f2e096..3330ed7d 100644 --- a/tests/test_planfile_queue.py +++ b/tests/test_planfile_queue.py @@ -2859,6 +2859,50 @@ def test_retry_feedback_is_length_bounded(self) -> None: self.assertLess(len(cleaned), 2_200) self.assertIn("truncated", cleaned) + def test_redact_secrets_preserves_code_expressions_and_repairs_closing_brace(self) -> None: + from koru.queue.diff_repair import extract_unified_diff + from koru.queue.patch_mode import redact_secrets + + code_excerpt = ( + "const SECRET_FIELDS = new Set(['username', 'password']);\n" + "this.key = encryptionKey;\n" + "return {...entry, secrets: decryptJson(entry.envelope, this.key)};\n" + "if (nextKey === this.key) return {rotated: false};\n" + ) + self.assertEqual(redact_secrets(code_excerpt), code_excerpt) + + with tempfile.TemporaryDirectory() as tmp: + project = Path(tmp) + target = project / "src" / "vault.mjs" + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text( + " async metadata(id, requestedOrigin) {\n" + " return {...entry, fields: Object.keys(secrets).sort()};\n" + " }\n" + "\n" + " async rotateEncryptionKey(nextKey) {\n" + " if (nextKey === this.key) return {rotated: false};\n" + " }\n", + encoding="utf-8", + ) + raw_diff = ( + "diff --git a/src/vault.mjs b/src/vault.mjs\n" + "--- a/src/vault.mjs\n" + "+++ b/src/vault.mjs\n" + "@@ -1,6 +1,12 @@\n" + " return {...entry, fields: Object.keys(secrets).sort()};\n" + " }\n" + " \n" + "+ async verifyKeyReadiness() {\n" + "+ return {ok: true};\n" + " }\n" + " \n" + " async rotateEncryptionKey(nextKey) {\n" + ) + repaired = extract_unified_diff(raw_diff, project=project, known_files=("src/vault.mjs",)) + assert repaired is not None + self.assertIn("+ }\n+\n+ async verifyKeyReadiness() {\n+ return {ok: true};\n }\n", repaired) + def test_patch_creating_a_new_file_is_promoted(self) -> None: from koru.queue.patch_transaction import apply_proposed_patch