Skip to content

fix(delegation): redact credentials in live subagent transcripts - #67635

Merged
teknium1 merged 1 commit into
NousResearch:mainfrom
Frowtek:fix/delegation-transcript-redaction
Jul 20, 2026
Merged

fix(delegation): redact credentials in live subagent transcripts#67635
teknium1 merged 1 commit into
NousResearch:mainfrom
Frowtek:fix/delegation-transcript-redaction

Conversation

@Frowtek

@Frowtek Frowtek commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

The live transcripts added with delegate_task (#67479) write each child's
events to <hermes_home>/cache/delegation/live/<id>/task-<n>.log. Nothing on
that path is redacted, and the rendered events are precisely the secret-bearing
surfaces — tool args, tool results, streamed assistant text.

The location is not incidental. tools/delegate_tool.py:1620 documents
cache/delegation as "mounted read-only into remote backends", so every
line lands in a file readable from inside the sandbox.

Observed on the writer as it stands:

tool   | -> terminal(curl -H "Authorization: Bearer sk-ant-api03-...")
result | terminal ok 0.4s: OPENAI_API_KEY=sk-proj-... AWS_SECRET_ACCESS_KEY=wJalr...

The same three values through the canonical redactor:

curl -H "Authorization: Bearer ***" https://api.internal
OPENAI_API_KEY=*** AWS_SECRET_ACCESS_KEY=***

Every other sink for this data already routes through that redactor — search
results via redact_sensitive_text(file_read=True), terminal output via
redact_terminal_output. The transcript was the one place an operator's keys
reached disk in the clear.

Related Issue

Fixes #

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)

Changes Made

  • tools/delegation_live_log.py — redact at event(). Every typed helper
    (assistant_text, thinking, tool_start, tool_result, marker,
    finalize, the stream flush) funnels through it, so one call covers them all
    and a helper added later can't bypass it. The header is written directly
    rather than through event(), so the goal string is redacted there too — a
    caller can paste a key into the task text.
    force=True because this is a safety boundary and must redact even with the
    global toggle off. On an import failure the line is withheld rather than
    written raw: losing a debug line costs less than writing a live credential
    into a sandbox-readable file.
  • tests/tools/test_delegation_live_log.py — coverage for args, results,
    streamed text, the goal header, thinking, an all-helpers sweep proving the
    choke point holds, plus a benign-content test showing redaction doesn't gut
    the transcript.

Testing

Six of the seven new tests fail on main; the seventh (benign content
untouched) passes both ways, which is what shows the fix doesn't trade a leak
for a mangled log.

Key names, tool names, statuses, durations and ordinary prose survive — tail -f
stays as useful as before:

tool   | -> read_file(src/parser.py)
result | read_file ok 1.5s: def parse(x): ...
tests/tools/test_delegation_live_log.py .......... 30 passed

Delegation/redaction/process-registry selection baseline-compared against a
clean origin/main worktree: identical 4 pre-existing failures (all
test_process_registry.py, Windows process handling, unrelated), 402 → 409
passed.

Checklist

  • Bug is reproducible on main and covered by a failing-before/passing-after test
  • No regressions in the surrounding suite (baseline-compared against origin/main)
  • Change is scoped to the defect — no unrelated refactoring
  • Tested on Ubuntu 24.04

@alt-glitch alt-glitch added type/security Security vulnerability or hardening P2 Medium — degraded but workaround exists tool/delegate Subagent delegation sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data labels Jul 19, 2026

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for addressing a real credential boundary. Remote current main still creates and tees child progress into LiveTranscriptWriter (tools/delegate_tool.py:2563-2622), while its header and event() sinks write raw text (tools/delegation_live_log.py:106,125). The proposed force=True choke point is appropriate for that path.

Problems

  • tools/delegation_live_log.py:329 still writes each task goal verbatim into manifest.json. That file sits beside the transcript logs under cache/delegation/live/, and cache/delegation is mounted into remote backends (tools/credential_files.py:353). A credential in a task goal remains exposed despite the header redaction.

Suggested changes

  • Redact or omit the manifest goal using the same force-mode policy, and add a test covering both the log header and manifest.

Automated hermes-sweeper review.

f"goal: {_one_line(goal, _KICKOFF_MAX)}",
# Header bypasses event(), so redact here too — a goal string
# can carry a key the caller pasted into the task.
f"goal: {_redact(_one_line(goal, _KICKOFF_MAX))}",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This protects the log header, but _write_manifest() still serializes the same task goal raw at line 329. Because manifest.json is in the same mounted cache/delegation/live/<id>/ directory, redact or omit that manifest field too and add coverage.

The live transcripts added with delegate_task write each child's events to
<hermes_home>/cache/delegation/live/<delegation_id>/. Nothing on that path is
redacted, and the rendered events are precisely the secret-bearing surfaces:
tool args, tool results and streamed assistant text.

That location is not incidental — delegate_tool.py:1620 documents cache/
delegation as "mounted read-only into remote backends", so every line lands
in a file readable from inside the sandbox.

Observed on the writer today:

    tool   | -> terminal(curl -H "Authorization: Bearer sk-ant-api03-...")
    result | terminal ok 0.4s: OPENAI_API_KEY=sk-proj-... AWS_SECRET_ACCESS_KEY=wJalr...

The same three values through the canonical redactor:

    curl -H "Authorization: Bearer ***" https://api.internal
    OPENAI_API_KEY=*** AWS_SECRET_ACCESS_KEY=***

Every other sink for this data already routes through that redactor — search
results via redact_sensitive_text(file_read=True), terminal output via
redact_terminal_output — so the transcript was the one place an operator's
keys reached disk in the clear.

Redact at three points, covering every artefact the dispatch writes into that
directory:

  * event() — every typed helper (assistant_text, thinking, tool_start,
    tool_result, marker, finalize, the stream flush) funnels through it, so
    one call covers them all and a helper added later cannot bypass it.
  * the .log header — written directly rather than through event(), and a
    caller can paste a key into the task text.
  * manifest.json — _write_manifest serialises the same goal, and the manifest
    sits in the same mounted directory, so redacting only the header would
    have left the credential exposed one file over.

force=True because this is a safety boundary and must redact even with the
global toggle off. On the (impossible-in-practice) import failure the line is
withheld instead of written raw: losing a debug line costs less than writing
a live credential into a sandbox-readable file.

Key names, tool names, statuses, durations and ordinary prose are untouched,
so tail -f stays as useful as before — pinned by its own test, alongside a
whole-directory sweep asserting no file under live/<id>/ carries the raw key.
@Frowtek
Frowtek force-pushed the fix/delegation-transcript-redaction branch from 07a4038 to 21a3c7b Compare July 20, 2026 12:29
@teknium1
teknium1 merged commit 183712a into NousResearch:main Jul 20, 2026
33 checks passed
@teknium1

Copy link
Copy Markdown
Contributor

Infographic — credential-hygiene cluster (merged)

Covers the three directly-merged PRs from this cluster: #67806 (explicit api_key precedence), #67640 (master credential stores never mountable), #67635 (live transcript redaction). The remaining two (#67802, #67797) landed via salvage PR #68074.

credential-hygiene-triple-drop

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

Labels

P2 Medium — degraded but workaround exists sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data tool/delegate Subagent delegation type/security Security vulnerability or hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants