Skip to content

fix: redact cron stderr/stdout before truncating, report stderr tail - #5574

Merged
bolichen97 merged 1 commit into
mainfrom
fix/cron-stderr-tail-redact-order-5547
Aug 25, 2026
Merged

fix: redact cron stderr/stdout before truncating, report stderr tail#5574
bolichen97 merged 1 commit into
mainfrom
fix/cron-stderr-tail-redact-order-5547

Conversation

@chenmingwei23

Copy link
Copy Markdown
Contributor

Problem / Motivation

Two sibling sites in src/kiro_crew/cron_script.py retain the truncation patterns that the fix to run_script_sandboxed's failure branch (#4402) removed, and were deliberately left out of that PR's scope:

  1. run_command_sandboxed failure branch appended stderr_out[:1000] -- the HEAD of stderr, unredacted. A command cron that emits a chatty startup warning before crashing reports the warning instead of the terminal error, and a credential anywhere in that head reaches the cron result raw.
  2. run_script_sandboxed's bad-output diagnostic built redact(stdout[:200]) -- truncating BEFORE redacting. A credential straddling the 200-char boundary loses the tail the redaction pattern needs, so its unredacted head leaks into the diagnostic.

Why it matters

Cron failure output flows to alert surfaces (Slack, dashboard notifications, logs). The head slice makes the report useless exactly when it is needed (the terminal error is displaced by startup noise), and both slice-before-redact orderings defeat credential redaction at the truncation boundary -- redact()'s patterns (e.g. AKIA-prefixed access-key ids) cannot match a string the slice already cut in half.

What changed (motivation → approach → change)

Symptom, root cause, then the change, mirroring the ordering #4402 established:

  • run_command_sandboxed failure branch: stderr_out[:1000] becomes redact(stderr_out.rstrip())[-1000:]. Redact the complete stderr first (so no boundary can strand a secret), then take the TAIL -- a process that dies hard leaves its diagnosis last, so the tail carries the traceback, not the startup warning.
  • run_script_sandboxed bad-output diagnostic: redact(stdout[:200]) becomes redact(stdout)[:200]. Same 200-char window, but redaction now sees the whole stream before the slice.

Deliberately NOT touched: the nonzero-exit/empty-stdout branch a few lines above the diagnostic (stderr[:500]) -- that hunk is exactly the diff of the still-open #4402 and is fixed there; changing it here would collide with that PR. This PR covers only the two sites #5547 names.

Tests

Four new tests, each mutation-verified red against the pre-fix code:

  • TestRunCommandSandboxed::test_nonzero_exit_reports_stderr_tail_not_head -- a >1000-char leading warning must not displace the terminal error.
  • TestRunCommandSandboxed::test_nonzero_exit_redacts_stderr_before_truncating -- an AWS-key-shaped credential laid so the 1000-char tail window starts 2 chars into it: slice-then-redact leaks the 16-char tail, redact-then-slice does not. Positive assertions pin that the redaction marker and surrounding context survive (no vacuous pass).
  • TestRunCommandSandboxed::test_nonzero_exit_short_stderr_stays_whole -- a short stderr is reported whole (pins the rstrip behavior).
  • TestRunScriptSandboxedErrorPaths::test_bad_json_output_redacts_before_truncating -- a credential laid so the 200-char head window ends 10 chars into it: slice-then-redact leaks the raw head, redact-then-slice shows the marker's head instead.

Local gates: black gate, subprocess-encoding gate, isort, flake8, mypy src/kiro_crew (1088 files clean), test/test_cron_script.py 105 passed / 1 skipped. Full suite run; the only failures are this host's pre-existing real-home-layout floor failures, reproduced identically on a pristine main worktree (zero cron-related).

Manual verification

N/A -- unit coverage sufficient: both changes are pure string-ordering fixes on already-tested branches, and the tests pin the exact byte layouts of the leak.

Closes #5547

run_command_sandboxed's failure branch took the HEAD of stderr, so a
chatty startup warning displaced the terminal error, and sliced before
redacting. run_script_sandboxed's bad-output diagnostic truncated
stdout before redacting, leaking the unredacted head of a credential
straddling the 200-char boundary. Redact the complete stream first,
then slice; the stderr report takes the tail, where a crashing
process leaves its diagnosis.

Closes #5547
@chenmingwei23
chenmingwei23 requested a review from a team as a code owner August 24, 2026 11:31
@github-actions github-actions Bot added the readiness: checking Automated validation is still running label Aug 24, 2026
@github-actions

Copy link
Copy Markdown
Contributor

GPT 5.6 Review — ✅ no blocking findings

GPT 5.6 completed its review of 93d288d0f5baa2310c8c8f00f4a9cd31feec84ed and found no blocking issues.

This comment is updated in place on each push.

Review details

No findings.
[GPT-REVIEWED] 93d288d

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

@github-actions

Copy link
Copy Markdown
Contributor

Design Review (Fable 5) — ✅ PASS

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

Design-Verdict: PASS

Root-cause fix (redact whole stream, then slice) at both named sites, boundary layouts pinned byte-exactly by tests; scope discipline vs #4402 is sound.

Suggestions

  • The Bad output diagnostic parses stdout's last line, yet still shows the head slice — redact(stdout)[-200:] would surface the line that actually failed to parse, the same displacement logic this PR applies to stderr; fine as a follow-up if kept out of scope here.

[DESIGN-REVIEWED] 93d288d

@github-actions

Copy link
Copy Markdown
Contributor

First Principles Review (Fable 5) — 🟡 CONCERNS

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

All evidence is in. The diff does exactly what the description says (two ordering fixes plus tests), so the review turns on two counted facts: the head-slice fix re-spells an existing helper, and the same root cause has unfixed siblings elsewhere.

First-Principles-Verdict: CONCERNS

redact(stdout)[:200] re-spells the existing security.redact_and_truncate, and five counted slice-before-redact siblings survive this fix elsewhere.

What this change ships

Intent: stop failed-cron reports from leaking boundary-cut credentials and from showing startup noise instead of the terminal error — a FIX.

  1. Failed command crons report the stderr tail, not the head — justified (named harm: displaced terminal error).
  2. Command-cron stderr is redacted whole before the 1000-char cut — justified (boundary-strand leak, derived from the redaction patterns' need for the full match).
  3. Trailing stderr whitespace is stripped before reporting — declared, serves the tail window, harmless.
  4. Script-cron bad-output diagnostic redacts full stdout before its 200-char slice — right fix, duplicate of security.redact_and_truncate (security.py:10153).

Watch

  • Same root cause, counted: grep redact\([^)]*\[[:\-] finds 6 slice-before-redact sites outside this diff; 5 are leak-relevant siblings — subagent.py:3021 (task[:80]), dashboard/handlers_project.py:29 (desc[:4000]), agent.py:1300 (command[:200]), spec_builder/backend/routes.py:2233 and :3127. (dashboard/handlers/files.py:2768 is exempt — input is regex-verified hex.) The in-file stderr[:500] sibling is declared and deferred to fix(cron): report the terminal stderr, not the leading one #4402; these five are not. Accepted-and-deferred, but the fix pattern is one line per site.

Subtractions

  • Replace redact(stdout)[:200] (cron_script.py:728) with the existing redact_and_truncate(stdout, 200) — byte-identical passes and order (redact_exfiltration_urlsredact_credentials → head slice), 6 real call sites already depend on it; the inline spelling is a second copy of its ordering invariant, and its comment restates that helper's docstring.

[FIRST-PRINCIPLES-REVIEWED] 93d288d

@github-actions

Copy link
Copy Markdown
Contributor

Opus 4.8 Review — ✅ no blocking findings

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

Review details

Based on my analysis, I've confirmed the structural facts without needing the benchmark. Let me finalize.

Structural verification of the candidate:

Reading redact_credentials (security.py:9027): pass 1 creates _CREDENTIAL_PATTERNS.finditer(result) over the original text, then for each of k matches does result.replace(matched, tag, 1), each O(n). That is O(k·n) — quadratic when matches are dense (dense AKIA+16 tokens give k ≈ n/20). Pass 2 does the same for base64 chunks that decode to credentials. Confirmed quadratic by reading the code, not asserted.

The diff moves redact from a bounded input to an unbounded one:

  • Line 924: was stderr_out[:1000] (≤1000 chars into redact) → now redact(stderr_out.rstrip())[-1000:] (full stderr; stderr_out is never capped — only output/stdout is, at line 913).
  • Line 728: was redact(stdout[:200]) (≤200 chars) → now redact(stdout)[:200] (full uncapped stdout).

Command/stderr output is untrusted external content; an attacker who shapes a failing cron command's output can emit multi-MB of dense credential tokens, and redaction now processes the whole buffer superlinearly. Diff-introduced (bounded→unbounded), so a bounded-window redact is an in-diff fix.

Magnitude requires adversarially-crafted, multi-MB, dense-credential command output on a failing cron — real but a specific multi-step chain, and normal operation is unaffected. That places it below the clean-crash/DoS bar for BLOCKING; advisory is the honest call.

Redact before truncate is a genuine security improvement (proven by the added tests); the only downside is the unbounded redact input.

FINDING.

FINDING — src/kiro_crew/cron_script.py:924,728 — redact(stderr_out.rstrip())[-1000:] / redact(stdout)[:200] now run redaction on the full uncapped subprocess buffer where the prior slice bounded it to ≤1000/≤200 chars; adversarial command output densely packed with credential-shaped tokens makes redact_credentials' per-match result.replace(...) loop O(n²), burning CPU on a failed cron → Fix: bound the redact input to a generous window that still spans the truncation boundary, e.g. redact(stderr_out.rstrip()[-4096:])[-1000:] and redact(stdout[:4096])[:200], preserving the detectable-prefix-survives-truncation property.

[OPUS-REVIEWED] 93d288d

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

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

@github-actions github-actions Bot added readiness: passed Eligible automated validation passed for the current revision and removed readiness: checking Automated validation is still running labels Aug 24, 2026
@chenmingwei23

Copy link
Copy Markdown
Contributor Author

Disposition of the First Principles CONCERNS (advisory, verdict pinned to 93d288d0f):

Watch item (five slice-before-redact siblings outside this diff): accepted and deferred to #5582. All five sites verified real as counted (subagent.py:3021, handlers_project.py:29, agent.py:1300, spec_builder/backend/routes.py:2233 and :3127; files.py:2768 confirmed exempt as regex-verified hex). Filed #5582 to route them through redact_and_truncate, folding in the harder _stderr_tail seek-window variant of the same class. They stay out of this PR for the same scope discipline that kept the stderr[:500] branch out: that branch is the open #4402's diff, and this PR covers exactly the two sites #5547 names.

Subtraction (redact(stdout)[:200] -> redact_and_truncate(stdout, 200)): accepted and deferred to the same #5582 pass. Verified byte-identical (same composition and order: exfiltration URLs, then credentials, then slice). Not pushed here because the PR is fully converged (63/63 check-runs green on this head) and a cosmetic-only push re-runs every review lane for zero behavior change; #5582's uniform-routing pass consolidates this call site along with the five it fixes.

dwu96 added a commit that referenced this pull request Aug 24, 2026
…ites

Each site spelled the composition as _redact(x[:n]) — slice first, redact
second — so a credential or exfiltration URL straddling the truncation
boundary was cut before the redaction regexes saw it, and the surviving raw
fragment escaped into SEL audit rows, logs, and dashboard payloads. The
prefix-anchored, fixed-width credential patterns (e.g. AKIA/ASIA + 16) cannot
match a fragment missing its prefix or tail.

Route all five sites through security.redact_and_truncate (redact the FULL
text, then slice), keeping each module's existing redaction entry-point
convention: subagent.py, dashboard/handlers_project.py and the spec_builder
routes gain a thin _redact_and_truncate sibling next to their _redact wrapper
(the routes variant keeps _redact's fail-closed guard), and agent.py truncates
the interpolated command through the helper inside the f-string so the
boundary no longer sits mid-expression.

No max_chars value changes (80/4000/200/64/200 preserved); secret-free inputs
produce byte-identical output. One boundary-straddling regression test per
site, each mutation-checked against the unfixed spelling.

Refs #5582 (the two cron_script.py items are deferred behind open PR #5574)
dwu96 added a commit that referenced this pull request Aug 24, 2026
…ites

Each site spelled the composition as _redact(x[:n]) — slice first, redact
second — so a credential or exfiltration URL straddling the truncation
boundary was cut before the redaction regexes saw it, and the surviving raw
fragment escaped into SEL audit rows, logs, and dashboard payloads. The
prefix-anchored, fixed-width credential patterns (e.g. AKIA/ASIA + 16) cannot
match a fragment missing its prefix or tail.

Four sites now route through security.redact_and_truncate (redact the FULL
text, then slice), keeping each module's redaction entry-point convention:
subagent.py, dashboard/handlers_project.py and the spec_builder routes gain a
thin _redact_and_truncate sibling next to their _redact wrapper (the routes
variant keeps _redact's fail-closed guard). agent.py's site interpolates the
command through the module's context-aware shim before slicing —
redact(command)[:200] — because redact_and_truncate applies only baseline
redaction and would still cut a companion-only token before the companion
regexes see it (server GPT review catch).

No max_chars value changes (80/4000/200/64/200 preserved); secret-free inputs
produce byte-identical output. One boundary-straddling regression test per
site, each mutation-checked against the unfixed spelling. Test fixtures inline
the fabricated AKIA literal rather than binding a secret-named variable, which
tripped CodeQL's name-based source heuristic into flagging 10 unchanged
production log lines.

Refs #5582 (the two cron_script.py items are deferred behind open PR #5574)
dwu96 added a commit that referenced this pull request Aug 24, 2026
…ites

Each site spelled the composition as _redact(x[:n]) — slice first, redact
second — so a credential or exfiltration URL straddling the truncation
boundary was cut before the redaction regexes saw it, and the surviving raw
fragment escaped into SEL audit rows, logs, and dashboard payloads. The
prefix-anchored, fixed-width credential patterns (e.g. AKIA/ASIA + 16) cannot
match a fragment missing its prefix or tail.

Four sites now route through security.redact_and_truncate (redact the FULL
text, then slice), keeping each module's redaction entry-point convention:
subagent.py, dashboard/handlers_project.py and the spec_builder routes gain a
thin _redact_and_truncate sibling next to their _redact wrapper (the routes
variant keeps _redact's fail-closed guard). agent.py's site interpolates the
command through the module's context-aware shim before slicing —
redact(command)[:200] — because redact_and_truncate applies only baseline
redaction and would still cut a companion-only token before the companion
regexes see it (server GPT review catch).

No max_chars value changes (80/4000/200/64/200 preserved); secret-free inputs
produce byte-identical output. One boundary-straddling regression test per
site, each mutation-checked against the unfixed spelling. Test fixtures inline
the fabricated AKIA literal rather than binding a secret-named variable, which
tripped CodeQL's name-based source heuristic into flagging 10 unchanged
production log lines.

Refs #5582 (the two cron_script.py items are deferred behind open PR #5574)
aniruddhaadak80 added a commit to aniruddhaadak80/KiroCrew that referenced this pull request Aug 24, 2026
Five sites sliced text to a logging budget BEFORE redacting, so a
credential straddling the cut survived as a fragment no credential
regex matches: subagent run summaries (task[:80]), project run
descriptions (desc[:4000]), the rejected-hook SEL audit
(command[:200]), and two spec-builder surfaces (name[:64],
first[:200]). Each now routes through security.redact_and_truncate,
which redacts the full text first; the spec-builder module keeps its
own fail-closed wrapper (_redact_truncated) so the no-security-module
fallback still withholds rather than serves.

cron_script._stderr_tail had the harder variant: it seeked to
size - limit and redacted only what it read, so a key spanning the
window START lost its anchoring prefix. The read now reaches back
_STDERR_TAIL_OVERLAP bytes before redacting and tail-slices after.
The redact(stdout[:200]) site from kirodotdev#5574 is consolidated onto
redact_and_truncate while here.
aniruddhaadak80 added a commit to aniruddhaadak80/KiroCrew that referenced this pull request Aug 24, 2026
Five sites sliced text to a logging budget BEFORE redacting, so a
credential straddling the cut survived as a fragment no credential
regex matches: subagent run summaries (task[:80]), project run
descriptions (desc[:4000]), the rejected-hook SEL audit
(command[:200]), and two spec-builder surfaces (name[:64],
first[:200]). Each now routes through security.redact_and_truncate,
which redacts the full text first; the spec-builder module keeps its
own fail-closed wrapper (_redact_truncated) so the no-security-module
fallback still withholds rather than serves.

cron_script._stderr_tail had the harder variant: it seeked to
size - limit and redacted only what it read, so a key spanning the
window START lost its anchoring prefix. The read now reaches back
_STDERR_TAIL_OVERLAP bytes before redacting and tail-slices after.
The redact(stdout[:200]) site from kirodotdev#5574 is consolidated onto
redact_and_truncate while here.
aniruddhaadak80 added a commit to aniruddhaadak80/KiroCrew that referenced this pull request Aug 24, 2026
Five sites sliced text to a logging budget BEFORE redacting, so a
credential straddling the cut survived as a fragment no credential
regex matches: subagent run summaries (task[:80]), project run
descriptions (desc[:4000]), the rejected-hook SEL audit
(command[:200]), and two spec-builder surfaces (name[:64],
first[:200]). Each now routes through security.redact_and_truncate,
which redacts the full text first; the spec-builder module keeps its
own fail-closed wrapper (_redact_truncated) so the no-security-module
fallback still withholds rather than serves.

cron_script._stderr_tail had the harder variant: it seeked to
size - limit and redacted only what it read, so a key spanning the
window START lost its anchoring prefix. The read now reaches back
_STDERR_TAIL_OVERLAP bytes before redacting and tail-slices after.
The redact(stdout[:200]) site from kirodotdev#5574 is consolidated onto
redact_and_truncate while here.

@bolichen97 bolichen97 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving as requested

@bolichen97
bolichen97 merged commit 6f8a645 into main Aug 25, 2026
64 checks passed
@bolichen97
bolichen97 deleted the fix/cron-stderr-tail-redact-order-5547 branch August 25, 2026 05:56
@github-actions github-actions Bot removed the readiness: passed Eligible automated validation passed for the current revision label Aug 25, 2026
aniruddhaadak80 added a commit to aniruddhaadak80/KiroCrew that referenced this pull request Aug 25, 2026
Five sites sliced text to a logging budget BEFORE redacting, so a
credential straddling the cut survived as a fragment no credential
regex matches: subagent run summaries (task[:80]), project run
descriptions (desc[:4000]), the rejected-hook SEL audit
(command[:200]), and two spec-builder surfaces (name[:64],
first[:200]). Each now routes through security.redact_and_truncate,
which redacts the full text first; the spec-builder module keeps its
own fail-closed wrapper (_redact_truncated) so the no-security-module
fallback still withholds rather than serves.

cron_script._stderr_tail had the harder variant: it seeked to
size - limit and redacted only what it read, so a key spanning the
window START lost its anchoring prefix. The read now reaches back
_STDERR_TAIL_OVERLAP bytes before redacting and tail-slices after.
The redact(stdout[:200]) site from kirodotdev#5574 is consolidated onto
redact_and_truncate while here.
aniruddhaadak80 added a commit to aniruddhaadak80/KiroCrew that referenced this pull request Aug 25, 2026
Five sites sliced text to a logging budget BEFORE redacting, so a
credential straddling the cut survived as a fragment no credential
regex matches: subagent run summaries (task[:80]), project run
descriptions (desc[:4000]), the rejected-hook SEL audit
(command[:200]), and two spec-builder surfaces (name[:64],
first[:200]). Each now routes through security.redact_and_truncate,
which redacts the full text first; the spec-builder module keeps its
own fail-closed wrapper (_redact_truncated) so the no-security-module
fallback still withholds rather than serves.

cron_script._stderr_tail had the harder variant: it seeked to
size - limit and redacted only what it read, so a key spanning the
window START lost its anchoring prefix. The read now reaches back
_STDERR_TAIL_OVERLAP bytes before redacting and tail-slices after.
The redact(stdout[:200]) site from kirodotdev#5574 is consolidated onto
redact_and_truncate while here.
aniruddhaadak80 added a commit to aniruddhaadak80/KiroCrew that referenced this pull request Aug 25, 2026
Five sites sliced text to a logging budget BEFORE redacting, so a
credential straddling the cut survived as a fragment no credential
regex matches: subagent run summaries (task[:80]), project run
descriptions (desc[:4000]), the rejected-hook SEL audit
(command[:200]), and two spec-builder surfaces (name[:64],
first[:200]). Each now routes through security.redact_and_truncate,
which redacts the full text first; the spec-builder module keeps its
own fail-closed wrapper (_redact_truncated) so the no-security-module
fallback still withholds rather than serves.

cron_script._stderr_tail had the harder variant: it seeked to
size - limit and redacted only what it read, so a key spanning the
window START lost its anchoring prefix. The read now reaches back
_STDERR_TAIL_OVERLAP bytes before redacting and tail-slices after.
The redact(stdout[:200]) site from kirodotdev#5574 is consolidated onto
redact_and_truncate while here.
aniruddhaadak80 added a commit to aniruddhaadak80/KiroCrew that referenced this pull request Aug 25, 2026
Five sites sliced text to a logging budget BEFORE redacting, so a
credential straddling the cut survived as a fragment no credential
regex matches: subagent run summaries (task[:80]), project run
descriptions (desc[:4000]), the rejected-hook SEL audit
(command[:200]), and two spec-builder surfaces (name[:64],
first[:200]). Each now routes through security.redact_and_truncate,
which redacts the full text first; the spec-builder module keeps its
own fail-closed wrapper (_redact_truncated) so the no-security-module
fallback still withholds rather than serves.

cron_script._stderr_tail had the harder variant: it seeked to
size - limit and redacted only what it read, so a key spanning the
window START lost its anchoring prefix. The read now reaches back
_STDERR_TAIL_OVERLAP bytes before redacting and tail-slices after.
The redact(stdout[:200]) site from kirodotdev#5574 is consolidated onto
redact_and_truncate while here.
aniruddhaadak80 added a commit to aniruddhaadak80/KiroCrew that referenced this pull request Aug 25, 2026
Five sites sliced text to a logging budget BEFORE redacting, so a
credential straddling the cut survived as a fragment no credential
regex matches: subagent run summaries (task[:80]), project run
descriptions (desc[:4000]), the rejected-hook SEL audit
(command[:200]), and two spec-builder surfaces (name[:64],
first[:200]). Each now routes through security.redact_and_truncate,
which redacts the full text first; the spec-builder module keeps its
own fail-closed wrapper (_redact_truncated) so the no-security-module
fallback still withholds rather than serves.

cron_script._stderr_tail had the harder variant: it seeked to
size - limit and redacted only what it read, so a key spanning the
window START lost its anchoring prefix. The read now reaches back
_STDERR_TAIL_OVERLAP bytes before redacting and tail-slices after.
The redact(stdout[:200]) site from kirodotdev#5574 is consolidated onto
redact_and_truncate while here.
bolichen97 pushed a commit that referenced this pull request Aug 25, 2026
…ites (#5599)

Each site spelled the composition as _redact(x[:n]) — slice first, redact
second — so a credential or exfiltration URL straddling the truncation
boundary was cut before the redaction regexes saw it, and the surviving raw
fragment escaped into SEL audit rows, logs, and dashboard payloads. The
prefix-anchored, fixed-width credential patterns (e.g. AKIA/ASIA + 16) cannot
match a fragment missing its prefix or tail.

Four sites now route through security.redact_and_truncate (redact the FULL
text, then slice), keeping each module's redaction entry-point convention:
subagent.py, dashboard/handlers_project.py and the spec_builder routes gain a
thin _redact_and_truncate sibling next to their _redact wrapper (the routes
variant keeps _redact's fail-closed guard). agent.py's site interpolates the
command through the module's context-aware shim before slicing —
redact(command)[:200] — because redact_and_truncate applies only baseline
redaction and would still cut a companion-only token before the companion
regexes see it (server GPT review catch).

No max_chars value changes (80/4000/200/64/200 preserved); secret-free inputs
produce byte-identical output. One boundary-straddling regression test per
site, each mutation-checked against the unfixed spelling. Test fixtures inline
the fabricated AKIA literal rather than binding a secret-named variable, which
tripped CodeQL's name-based source heuristic into flagging 10 unchanged
production log lines.

Refs #5582 (the two cron_script.py items are deferred behind open PR #5574)

Co-authored-by: dwu96 <dwu96@users.noreply.github.com>
aniruddhaadak80 added a commit to aniruddhaadak80/KiroCrew that referenced this pull request Aug 25, 2026
Five sites sliced text to a logging budget BEFORE redacting, so a
credential straddling the cut survived as a fragment no credential
regex matches: subagent run summaries (task[:80]), project run
descriptions (desc[:4000]), the rejected-hook SEL audit
(command[:200]), and two spec-builder surfaces (name[:64],
first[:200]). Each now routes through security.redact_and_truncate,
which redacts the full text first; the spec-builder module keeps its
own fail-closed wrapper (_redact_truncated) so the no-security-module
fallback still withholds rather than serves.

cron_script._stderr_tail had the harder variant: it seeked to
size - limit and redacted only what it read, so a key spanning the
window START lost its anchoring prefix. The read now reaches back
_STDERR_TAIL_OVERLAP bytes before redacting and tail-slices after.
The redact(stdout[:200]) site from kirodotdev#5574 is consolidated onto
redact_and_truncate while here.
@bolichen97

Copy link
Copy Markdown
Collaborator

Open PR relationship audit

This is a consolidated, point-in-time code-level audit note. It compares complete merge-base diffs and current/merged code; it does not treat a shared topic as duplication or partial coverage as completion.

Relationship findings

  • PR #5603 is PARTIALLY_COVERED relative to this PR. Coverage is explicitly incomplete; this finding is not a completion or closure claim. Recommended action for PR #5603: CONTINUE_DEVELOPMENT. Merged code covers one of 5603's two production hunks behaviourally and none of the _stderr_tail fix, so closure as completed is not available. Files: src/kiro_crew/cron_script.py.

No PR, Issue, label, branch, or review state was changed by the relationship-note portion of this audit.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cron: apply #4402's tail-slice + redact-before-truncate pattern to run_command_sandboxed and the stdout diagnostic

2 participants