Repository navigation
Keep secrets out of run records at cancels, cuts and flushes - #2110
Merged
Merged
Conversation
ppXD
changed the base branch from
fix/carry-secret-taint-across-suspends-agents-and-subworkflows
to
main
October 12, 2026 17:37
Several writes put a team secret, or a fragment of one, where every member of the team (Viewers included) can read it, or into storage that redaction is supposed to keep clean. A cancelled external call wrote its raw target to external_call.failed (the records endpoint, the SSE tail, the timeline). Every other branch already wrote the redacted target. Both cancellation branches of TraceExternalCallAsync and TraceExternalStreamAsync now do the same. agent.run_command cut its output (the inline cap, or the runner's capture budget) before the engine's exact-match redaction. A cut inside a secret left a fragment that no exact match recognises, and the artifact stored the raw output with no encrypted sidecar. The engine now hands each node its scope redactor on NodeRunContext. The node redacts first and cuts second, and masks a trailing fragment of a budget-cut capture that could start a secret. An uncut capture keeps its runtime value: the engine still masks it in node.completed, and downstream nodes, resumes and reruns read the real value from the sidecar. PersistArtifactAsync masks text and JSON for the same reason. Streamed interaction.delta rows were redacted one at a time, so a secret split across a 32 KiB or one-second flush was stored in two halves and the live view joined them back together. A flush now releases only text that no secret can still run across, and carries the rest. When the stream ends, the held text is released, so the rows still join up to the masked output. When a cancel or a provider fault cuts the stream, the held text is masked instead: it is exactly the possible start of a secret whose rest never arrived. The call's presence delta reports that its capture was masked. The agent plane had the same gap at its reader's 8 MiB pass. A stdout or stderr line longer than one pass arrives as a cut frame plus its continuation, and each was redacted on its own. A key that the cut landed in was stored as a head in one native record (and transcript line) and a tail in the next. The executor now carries the end of a cut frame that could start a secret into the frame that continues it, on the live tail, the re-attached tail and the diagnostics drain. Both redactors now take the settle rule from one shared SecretBoundary helper. http.request logged URL userinfo before any redaction ran, and the startup placeholder guard printed the whole value of a partly rendered setting to stderr. Both now name only what is needed: the scheme, host and port; the key and its placeholders.
ppXD
force-pushed
the
fix/close-node-record-redaction-gaps
branch
from
October 12, 2026 17:37
44372ce to
0bf1f84
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
external_call.failedwritten on a cancel or shutdown named the raw target, including a{{secret}}query value. Both cancellation branches ofTraceExternalCallAsyncandTraceExternalStreamAsyncnow write the redacted target, like every other branch (backend/src/CodeSpace.Core/Services/Workflows/Nodes/NodeObservability.cs:105).agent.run_command: the node now redacts first and cuts second. The inline excerpt and the artifacts come from redacted text, and a budget-cut capture masks a trailing fragment that could start a secret (PersistenceSecretRedactor.RedactCut).NodeRunContext.PersistenceRedactor.PersistArtifactAsyncmasks text and JSON.interaction.deltarows:RecordingStreamingStructuredLLMClientDecorator.cs:94).SecretRedactor.CreateFrameStream()carries the end of a cut frame into the frame that continues it. This applies on the live tail, the re-attached tail and the diagnostics drain (AgentRunExecutor.cs:1078,4500,5684). A key the cut lands in is no longer stored as a head in one native record or transcript line and a tail in the next. Both redactors take the settle rule from one shared helper,Services/Redaction/SecretBoundary.cs.HttpRequestNode.SafeLogTargetlogs only the scheme, host and port.ConfigurationPlaceholderGuardreports only the key and its placeholders, never the partly rendered value.Test plan
["progress ", "[REDACTED]"]and the call is reported Masked.http.request.GET /recordsand the SSE tail after a cancel mid-stream, a cancel mid-http.request, a cut command output, and deltas streamed across a flush. No 6-character fragment of the secret appears.