fix(api): redact subagent stream fields + forward child_session_id

Hardening on top of the salvaged #51642: free-text fields
(preview/goal/summary/output_tail) pass redact_sensitive_text(force=True)
before leaving on the public /v1/runs SSE stream — same treatment the API
already applies to error text — and child_session_id survives the
allowlist so clients can correlate the child's session. Both were flagged
in the sweeper review of #51642 and unaddressed.
This commit is contained in:
teknium1 2026-07-26 19:38:34 -07:00 committed by Teknium
parent 4fbb86d2b8
commit 666076d137
2 changed files with 44 additions and 3 deletions

View File

@ -5982,12 +5982,15 @@ class APIServerAdapter(BasePlatformAdapter):
"timestamp": ts,
}
if preview is not None:
event["preview"] = preview
event["preview"] = redact_sensitive_text(
str(preview), force=True
)
for key in (
"goal",
"task_count",
"task_index",
"subagent_id",
"child_session_id",
"parent_id",
"depth",
"model",
@ -6005,8 +6008,16 @@ class APIServerAdapter(BasePlatformAdapter):
"output_tail",
):
value = kwargs.get(key)
if value is not None:
event[key] = value
if value is None:
continue
# Free-text fields can carry child terminal/tool output —
# force the same secret redaction the API applies to error
# text before it leaves the process on a public stream.
if key in ("goal", "summary", "output_tail") and isinstance(
value, str
):
value = redact_sensitive_text(value, force=True)
event[key] = value
_push(event)
# _thinking, subagent.tool, and subagent_progress are intentionally
# not forwarded on the /v1/runs stream: they are high-volume UI

View File

@ -941,6 +941,36 @@ class TestRunEventCallback:
assert adapter._run_statuses[run_id]["last_event"] == "subagent.complete"
@pytest.mark.asyncio
async def test_subagent_events_redact_secrets_and_carry_child_session(self, adapter):
"""Free-text fields (goal/summary/output_tail/preview) must pass the
forced secret redaction before hitting the public /v1/runs stream,
and child_session_id must survive the allowlist so clients can
correlate the child's session."""
run_id = "run_subagent_redact"
loop = asyncio.get_running_loop()
queue = asyncio.Queue()
adapter._run_streams[run_id] = queue
adapter._run_statuses.pop(run_id, None)
callback = adapter._make_run_event_callback(run_id, loop)
secret = "sk-proj-abcdef1234567890abcdef1234567890abcdef12"
callback(
"subagent.complete",
preview=f"leaked {secret}",
goal=f"use key {secret} to fetch data",
subagent_id="deleg_999",
child_session_id="child-sess-42",
status="completed",
summary=f"exported OPENAI_API_KEY={secret} then ran",
output_tail=f"env shows {secret}",
)
event = await asyncio.wait_for(queue.get(), timeout=1.0)
assert event["child_session_id"] == "child-sess-42"
for field in ("preview", "goal", "summary", "output_tail"):
assert secret not in event[field], field
# ---------------------------------------------------------------------------
# /health endpoint