Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from pathlib import Path
from urllib.parse import urlparse

from kiro_crew.platform.context import redact_via_context
from kiro_crew.platform_compat import (
first_linked_ancestor,
is_link_or_junction,
Expand Down Expand Up @@ -491,7 +492,10 @@ def setup_safe_clone(url: str, scratch_root: Path, *, timeout_s: int = 300) -> t
if proc.returncode != 0:
tail = (proc.stderr or "").strip().splitlines()[-1:] or [""]
rmtree_force(dest)
return {}, f"git clone failed: {tail[0][:200]}"
# Redact BEFORE the bound (here and at every sibling site below): the slice
# can cut a credential in the echoed remote URL mid-match, leaving a fragment
# no downstream redaction pass recognises.
return {}, f"git clone failed: {redact_via_context(tail[0])[:200]}"
if not _repository_is_safe(dest):
rmtree_force(dest)
return {}, "cloned repository failed Git metadata safety verification"
Expand Down Expand Up @@ -552,7 +556,7 @@ def list_clone_branches(clone: Path, *, timeout_s: int = 30) -> tuple[list[str],
)
if proc.returncode != 0:
tail = (proc.stderr or "").strip().splitlines()[-1:] or [""]
return [], f"could not list branches: {tail[0][:160]}"
return [], f"could not list branches: {redact_via_context(tail[0])[:160]}"
names: list[str] = []
seen: set[str] = set()
for raw in (proc.stdout or "").splitlines():
Expand Down Expand Up @@ -845,7 +849,7 @@ def _run(*args: str, tmo: int = timeout_s) -> subprocess.CompletedProcess:
if co.returncode == 0:
return True, f"checked out {bare} @ origin/{bare}"
err = (co.stderr or "").strip().splitlines()[-1:] or [""]
return False, f"could not check out {bare}: {err[0][:160]}"
return False, f"could not check out {bare}: {redact_via_context(err[0])[:160]}"
# The fetch failed. That is the NORMAL case here, not an edge case: this clone's
# origin is neutralized to DISABLED_NO_PUSH (both urls — see `_disable_push`), so
# `git fetch origin <branch>` always exits 128. Measured against a local bare repo.
Expand Down Expand Up @@ -874,7 +878,7 @@ def _run(*args: str, tmo: int = timeout_s) -> subprocess.CompletedProcess:
if co.returncode == 0:
return True, f"checked out local {bare} (fetch failed — offline?)"
err = (fetched.stderr or "").strip().splitlines()[-1:] or [""]
return False, f"could not fetch {bare}: {err[0][:160]}"
return False, f"could not fetch {bare}: {redact_via_context(err[0])[:160]}"


def _ok(spec: CloneSpec, dest: Path, *, reused: bool) -> dict:
Expand Down
34 changes: 28 additions & 6 deletions src/kiro_crew/apps/builtins/auto_improvement/backend/commit.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import threading
from pathlib import Path

from kiro_crew.platform.context import redact_via_context
from kiro_crew.security import redact

from ..profiles.github_repo.pr_recipe import _prefer_authenticated_remote
Expand Down Expand Up @@ -134,7 +135,13 @@ def materialize_queued_diff(
if fetch.returncode != 0:
return {
"ok": False,
"error": f"could not fetch {branch}: {(fetch.stderr or '')[:160]}",
# Redact BEFORE the bound: git echoes the authenticated remote URL —
# userinfo and all — on an auth failure, and a slice can cut the
# credential mid-match into a fragment the downstream serving route's
# redaction pass no longer recognises.
"error": (
f"could not fetch {branch}: " f"{redact_via_context(fetch.stderr or '')[:160]}"
),
}
else:
# No configured url: the push cannot succeed either, so this degrades to
Expand All @@ -154,7 +161,10 @@ def materialize_queued_diff(
if checkout.returncode != 0:
return {
"ok": False,
"error": f"could not check out {branch}: {(checkout.stderr or '')[:160]}",
"error": (
f"could not check out {branch}: "
f"{redact_via_context(checkout.stderr or '')[:160]}"
),
}

apply_proc = subprocess.run(
Expand All @@ -173,7 +183,10 @@ def materialize_queued_diff(
_git(clone, "reset", "--hard", base_ref_local)
return {
"ok": False,
"error": f"the queued diff did not apply: {(apply_proc.stderr or '')[:160]}",
"error": (
f"the queued diff did not apply: "
f"{redact_via_context(apply_proc.stderr or '')[:160]}"
),
}
return {"ok": True, "base": base_ref_local}

Expand All @@ -197,7 +210,10 @@ def commit_staged_for_draft(*, clone: Path, body_path: Path, fp: str) -> dict[st
if commit.returncode != 0:
return {
"ok": False,
"error": f"could not commit the staged diff: {(commit.stderr or '')[:160]}",
"error": (
f"could not commit the staged diff: "
f"{redact_via_context(commit.stderr or '')[:160]}"
),
}
return {"ok": True, "sha": (_git(clone, "rev-parse", "HEAD").stdout or "").strip()}

Expand Down Expand Up @@ -266,7 +282,10 @@ def _commit_finding_locked(fp: str) -> dict[str, object]:
commit = _git(clone, "-c", "commit.gpgsign=false", "commit", "-m", message)
if commit.returncode != 0:
_git(clone, "reset", "--hard", base_ref_local)
return {"ok": False, "error": f"commit failed: {(commit.stderr or '')[:160]}"}
return {
"ok": False,
"error": f"commit failed: {redact_via_context(commit.stderr or '')[:160]}",
}
sha = (_git(clone, "rev-parse", "HEAD").stdout or "").strip()

# Scan the CONTENT before it leaves the host. `_commit_message` is already redacted;
Expand Down Expand Up @@ -327,7 +346,10 @@ def _commit_finding_locked(fp: str) -> dict[str, object]:
push = _git(clone, "push", url, f"HEAD:refs/heads/{branch}", timeout=_PUSH_TIMEOUT_S)
if push.returncode != 0:
_git(clone, "reset", "--hard", base_ref_local)
return {"ok": False, "error": f"push failed: {(push.stderr or '')[:200]}"}
return {
"ok": False,
"error": f"push failed: {redact_via_context(push.stderr or '')[:200]}",
}

return {"ok": True, "fp": fp, "branch": branch, "sha": sha}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
from pathlib import Path
from typing import Any, Callable

from kiro_crew.platform.context import redact_via_context
from kiro_crew.subprocess_utf8 import UTF8_TEXT

from ..spine.git_safety import GIT_SAFE_CONFIG, require_pinned
Expand Down Expand Up @@ -229,7 +230,12 @@ def setup_isolated_clone(
# --local hardlinks the object store: no network, near-instant, cheap on disk.
proc = _git("clone", "--local", shared_clone, dest, timeout=300)
if proc.returncode != 0:
return "", f"git clone --local failed: {(proc.stderr or '').strip()[:200]}"
# Redact BEFORE the bound: a slice can cut a credential in the URL git
# echoes mid-match, leaving a fragment no downstream pass recognises.
return "", (
f"git clone --local failed: "
f"{redact_via_context((proc.stderr or '').strip())[:200]}"
)
if branch:
checkout = _git("-C", dest, "checkout", branch, timeout=60)
if checkout.returncode != 0:
Expand All @@ -244,7 +250,7 @@ def setup_isolated_clone(
shutil.rmtree(dest, ignore_errors=True)
return "", (
f"could not check out the pull request head {branch!r}: "
f"{(checkout.stderr or '').strip()[:160]}"
f"{redact_via_context((checkout.stderr or '').strip())[:160]}"
)
_fetch_base_ref(dest, base_ref)
neutralize_origin(dest)
Expand Down Expand Up @@ -1331,7 +1337,7 @@ def publish_if_authorized(pr: str, status: dict[str, Any]) -> tuple[bool, str]:
proc = _gh("pr", "ready", pr)
if proc.returncode != 0:
tail = (proc.stderr or proc.stdout or "").strip().splitlines()[-1:] or [""]
return False, f"gh pr ready failed: {tail[0][:160]}"
return False, f"gh pr ready failed: {redact_via_context(tail[0])[:160]}"
logger.info("watchers: marked %s ready for review (%s)", pr, reason)
return True, reason

Expand Down
11 changes: 6 additions & 5 deletions src/kiro_crew/apps/builtins/auto_improvement/backend/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1309,11 +1309,12 @@ def _commit() -> dict[str, Any] | object:
sha=str(result.get("sha") or ""),
)
return web.json_response(result, status=200)
# Redacted, like every sibling error response here. `commit.py` builds its `error` from
# `(proc.stderr or '')[:160]` — raw git stderr, which quotes the ref, the path, and
# anything a repository's own hook printed. Latent while nothing rendered it; D-97 started
# showing it at the finding row, which made it a live egress path to the browser.
# Raised by the GPT review.
# Redacted, like every sibling error response here. `commit.py` builds its `error`
# from git stderr — which quotes the ref, the path, and anything a repository's own
# hook printed — scrubbed at the source with `redact_via_context` so its bound can
# never cut a credential mid-match. Latent while nothing rendered it; D-97 started
# showing it at the finding row, which made it a live egress path to the browser, so
# this pass stays as the output-boundary backstop. Raised by the GPT review.
return web.json_response(
{"code": "request_failed", "error": _redact_for_display(str(result.get("error") or ""))},
status=400,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import subprocess
from pathlib import Path

from kiro_crew.platform.context import redact_log_via_context
from kiro_crew.subprocess_utf8 import UTF8_TEXT

from ...spine.git_safety import GIT_SAFE_CONFIG, require_pinned
Expand Down Expand Up @@ -417,7 +418,7 @@ def _push_fix_branch(self, *, branch: str) -> tuple[bool, str]:
"push failed for %s (git exit %s): %s",
branch,
proc.returncode,
(proc.stderr or "").strip()[:200],
redact_log_via_context((proc.stderr or "").strip())[:200],
)
return False, "push failed"
return True, branch
Expand Down Expand Up @@ -504,7 +505,9 @@ def draft(
return f"QUEUED:{fingerprint}"
if proc.returncode != 0:
logger.warning(
"gh pr create failed for %s: %s", fingerprint, (proc.stderr or "").strip()[:200]
"gh pr create failed for %s: %s",
fingerprint,
redact_log_via_context((proc.stderr or "").strip())[:200],
)
return f"QUEUED:{fingerprint}"
return extract_pr_url(proc.stdout or "") or f"QUEUED:{fingerprint}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
from typing import Callable

from kiro_crew import platform_compat
from kiro_crew.platform.context import redact_via_context
from kiro_crew.sandbox import run_limited, sandboxed_spawn_argv

from ...spine import agent_discovery
Expand Down Expand Up @@ -971,7 +972,10 @@ def build_and_test(self, *, worktree: Path, src: Path) -> GateResult:
tail = (proc.stdout or proc.stderr or "").strip().splitlines()[-1:] or [""]
return GateResult(
passed=False,
detail=f"suite red (exit {proc.returncode}): {tail[0][:160]}",
# Redact BEFORE the bound: a candidate's test run can echo a credential,
# and the slice can cut it mid-match into a fragment no downstream
# redaction pass recognises.
detail=f"suite red (exit {proc.returncode}): {redact_via_context(tail[0])[:160]}",
failing_tests=failing,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@

from kiro_crew.config import KiroCrewConfig
from kiro_crew.hooks import TOOL_DENY, HookManager, hooks_config_from_config_dict
from kiro_crew.platform.context import redact_via_context
from kiro_crew.platform_compat import SIGKILL, kill_process_tree
from kiro_crew.sandbox import popen_limited, sandboxed_spawn_argv
from kiro_crew.subprocess_utf8 import UTF8_TEXT
Expand Down Expand Up @@ -893,8 +894,14 @@ def run(

dur = time.monotonic() - t0
if proc.returncode != 0:
# Redact BEFORE the tail cut: a credential straddling the bound keeps its
# right half otherwise, a fragment no downstream pass can match. Tail (not
# a head bound) because the END of stderr carries the
# actionable error; slicing redacted text can at worst split a marker.
return AgentResult(
ok=False, error=f"exit {proc.returncode}: {proc.stderr[-400:]}", duration_s=dur
ok=False,
error=f"exit {proc.returncode}: {redact_via_context(proc.stderr or '')[-400:]}",
duration_s=dur,
)
try:
envelope = json.loads(proc.stdout)
Expand Down Expand Up @@ -1012,7 +1019,8 @@ def _drain_stderr() -> None:
except Exception: # noqa: BLE001
self._terminate_group(popen)
stderr_thread.join(timeout=2.0) # let the drain finish; tail comes from its buffer
stderr_tail = ("".join(stderr_chunks))[-400:]
# Redact BEFORE the tail cut, same reason as the non-streaming path above.
stderr_tail = redact_via_context("".join(stderr_chunks))[-400:]

dur = time.monotonic() - t0
with self._cost_lock:
Expand Down
33 changes: 24 additions & 9 deletions src/kiro_crew/apps/builtins/auto_improvement/spine/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from dataclasses import dataclass
from pathlib import Path

from kiro_crew.platform.context import redact_log_via_context, redact_via_context
from kiro_crew.subprocess_utf8 import UTF8_TEXT

from . import ledger as L
Expand Down Expand Up @@ -1480,14 +1481,23 @@ def _direct_push(self, *, fp: str, kind: str, target: str, sha: str) -> bool | N
head_after = _git(["rev-parse", "HEAD"], self.clone)
self.pushed_sha = (head_after.stdout or "").strip() or sha
if push.returncode != 0:
self.log.error("direct-push FAILED for %s: %s", target, (push.stderr or "")[:300])
# Redact BEFORE the bound (here and at every stderr slice below): git
# echoes the authenticated remote URL on an auth failure, and slicing
# first can cut the credential into a fragment no later pass matches.
# Log lines use the companion-aware log redactor; the persisted ledger
# note keeps the baseline redact-then-bound helper.
self.log.error(
"direct-push FAILED for %s: %s",
target,
redact_log_via_context(push.stderr or "")[:300],
)
self.ledger.record(
L.LedgerEntry(
fp=fp,
kind=kind,
target=target,
status=L.STATUS_ERROR,
note=f"direct-push failed: {(push.stderr or '')[:150]}",
note=f"direct-push failed: {redact_via_context(push.stderr or '')[:150]}",
)
)
return False
Expand Down Expand Up @@ -1520,7 +1530,7 @@ def _discard_staged(self, why: str) -> None:
self.log.error(
"could not discard the staged diff after %s: %s",
why,
(reset.stderr or "")[:200],
redact_log_via_context(reset.stderr or "")[:200],
)
for rel in paths:
try:
Expand Down Expand Up @@ -1559,7 +1569,9 @@ def _stage_winner(self, winner: Proposal) -> bool:
errors="surrogateescape",
)
if ap.returncode != 0:
self.log.error("winner diff did not apply: %s", ap.stderr[:200])
self.log.error(
"winner diff did not apply: %s", redact_log_via_context(ap.stderr or "")[:200]
)
return False
_git(["add", "-A"], self.clone)
return True
Expand Down Expand Up @@ -1600,7 +1612,7 @@ def _commit_winner_provisional(self, winner: Proposal) -> bool:
self.log.error(
"provisional commit failed for %s: %s",
winner.cand_id,
(commit.stderr or "")[:200],
redact_log_via_context(commit.stderr or "")[:200],
)
self._discard_staged(f"a failed provisional commit for {winner.cand_id}")
return False
Expand All @@ -1619,7 +1631,7 @@ def _reset_provisional(self, pre_sha: str) -> None:
self.log.error(
"could not roll back the provisional commit to %s: %s",
pre_sha[:10],
(res.stderr or "").strip()[:160],
redact_log_via_context((res.stderr or "").strip())[:160],
)

def _finalize_winner_commit(
Expand Down Expand Up @@ -1857,11 +1869,14 @@ def _apply(extra: list[str]):
if ap.returncode != 0:
self.log.info(
"bug fix plain-apply failed (%s) — retrying with --3way",
(ap.stderr or "").strip()[:120],
redact_log_via_context((ap.stderr or "").strip())[:120],
)
ap = _apply(["--3way"])
if ap.returncode != 0:
self.log.error("bug fix diff did not apply (even --3way): %s", ap.stderr[:200])
self.log.error(
"bug fix diff did not apply (even --3way): %s",
redact_log_via_context(ap.stderr or "")[:200],
)
return False
_git(["add", "-A"], self.clone)
return True
Expand Down Expand Up @@ -1892,7 +1907,7 @@ def _commit_bug_winner_provisional(self, winner: Proposal) -> bool:
self.log.error(
"provisional bug commit failed for %s: %s",
winner.cand_id,
(commit.stderr or "")[:200],
redact_log_via_context(commit.stderr or "")[:200],
)
self._discard_staged(f"a failed provisional bug commit for {winner.cand_id}")
return False
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import subprocess
from pathlib import Path

from kiro_crew.platform.context import redact_via_context
from kiro_crew.subprocess_utf8 import UTF8_TEXT

from .bug_gate import BugGate
Expand Down Expand Up @@ -102,7 +103,7 @@ def _changed_status_paths(worktree: Path, base_sha: str) -> list[tuple[str, str]
# the error so the caller rejects the candidate instead of admitting it.
raise RuntimeError(
f"git diff failed (rc={r.returncode}) for base {base_sha!r}: "
f"{(r.stderr or '').strip()[:200]}"
f"{redact_via_context((r.stderr or '').strip())[:200]}"
)
out: list[tuple[str, str]] = []
for line in r.stdout.splitlines():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6519,9 +6519,11 @@ def test_the_profile_passes_its_track_to_the_fence(self) -> None:


class TestCommitErrorsReachTheBrowserRedacted:
"""The commit route returned `str(result.get("error"))` verbatim, and `commit.py` builds
that value from `(proc.stderr or '')[:160]` — RAW GIT STDERR, which quotes the ref, the
path, and whatever a repository's own hooks printed.
"""The commit route returned `str(result.get("error"))` verbatim, and `commit.py` built
that value from a raw fixed-bound slice of git stderr — which quotes the ref, the
path, and whatever a repository's own hooks printed. (`commit.py` now scrubs its
stderr at the source with `redact_via_context`; this route-level pass stays as the
output-boundary backstop.)

This was latent while nothing rendered it. D-97 (surfacing a refused commit at the finding
row, so the operator learns WHY) turned it into a live egress path: a failing pre-commit
Expand Down
Loading
Loading