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.
* feat(delegation): live-viewable subagent transcripts for delegate_task
Each child now streams an append-only, human-readable log to
<hermes_home>/cache/delegation/live/<delegation_id>/task-<n>.log while it
runs, and the dispatch return includes the paths so the caller can tail
them immediately instead of waiting blind for the consolidated summary.
- New tools/delegation_live_log.py: LiveTranscriptWriter (per-event append
+ flush, one-line rendering with truncation, never raises into the agent
loop), wrap_progress_callback (tees the child's existing
tool_progress_callback events into the log, preserves the _flush
contract), dispatch-time creation with pre-headered files so tail -f
attaches immediately, manifest.json (goals/task count/per-task status),
and 7-day retention pruning on new dispatches.
- delegate_task: wraps each child's progress callback with the writer;
sync results and background dispatch responses gain live_transcripts
(+ hint field on dispatch); per-task result entries carry
live_transcript; transcripts finalized with exit-reason markers.
- async_delegation: dispatch_async_delegation_batch accepts an optional
delegation_id so the live/ dir name matches the returned handle; the
completion event carries live_transcripts.
- process_registry: consolidated batch-completion block references each
task's live transcript path.
- Tool schema description documents the live_transcripts return surface;
docs gain a 'Live Transcripts' section with a tail -f example.
Placement under cache/delegation means the logs are mounted read-only
into remote terminal backends for free. Side-channel only: zero changes
to message content, so prompt caching is unaffected. Transcript-OUT only
— no overlap with the subagent control surfaces of PR #66046.
* fix(delegation): label the kickoff transcript line as user — it is the child's one user message