Skip to content

🏷️ fix: Keep Redacted Child Labels in Activity Phase Prompts - #573

Closed
danny-avila wants to merge 1 commit into
mainfrom
fix/phase-label-scoped-redaction
Closed

danny-avila wants to merge 1 commit into
mainfrom
fix/phase-label-scoped-redaction

Conversation

@danny-avila

Copy link
Copy Markdown
Collaborator

What breaks

With any tool-output redaction policy active, Run.generateActivityPhaseLabel never produces a phase title for phases whose activities carry committed child labels, which is the normal case once activity labels are on. The model is not called and the host's parent header falls back to the last activity's label.

A deployment that redacts a single tool it rarely uses (for example redactedToolNames: ['run_select_query']) loses phase titles on every run, including runs that never touch that tool.

Cause

buildActivityPhaseLabelPrompt treats any active policy as suppressing all free-form evidence:

const freeFormSuppressed =
  redaction != null &&
  (redaction.enabled === false || redaction.redactedToolNames.size > 0);

That gate drops committed child labels along with reasoning excerpts and assistant commentary. Hosts replace an activity's raw tool entries with its child label once the label commits, so a phase of labeled activities has no remaining evidence, the prompt is empty, and the early if (userPrompt === '') return {} fires.

Reasoning and commentary are rightly suppressed: they can quote output from an earlier call to a redacted tool. Child labels are different. generateActivityLabel builds each one from redacted evidence under its agent's own resolved policy, replacing redacted outputs with the redaction text and dropping free-form prose, so a child label cannot carry output that policy hides.

Change

A child label may stand in the phase prompt when the policy it was generated under covers the merged phase policy. generateActivityPhaseLabel already resolves each contributing agent's policy to build the merged one; it now also checks that every one of those policies covers the merge:

const childLabelsRedacted =
  redaction != null &&
  redactionContexts.length > 0 &&
  redactionContexts.every((context) =>
    coversToolOutputRedaction(
      hasToolOutputTracingConfig(this.langfuse, context.langfuse)
        ? resolveToolOutputTracingConfig(this.langfuse, context.langfuse)
        : undefined,
      redaction
    )
  );

coversToolOutputRedaction(candidate, required) is true when candidate globally disables tool-output tracing, or when it names every tool required names and matches at least as broadly (an exact candidate does not cover a partial requirement). A context with no policy never covers an active one.

buildActivityPhaseLabelPrompt takes the result as childLabelsRedacted and uses it only for committed labels:

+  const labelsAllowed = !freeFormSuppressed || childLabelsRedacted;
   ...
-      if (!freeFormSuppressed && activity.label != null && activity.label.trim() !== '') {
+      if (labelsAllowed && activity.label != null && activity.label.trim() !== '') {

Reasoning excerpts and assistant commentary stay gated on freeFormSuppressed exactly as before. Raw tool entries are unchanged and still redact per tool.

What stays suppressed

  • Labels from an agent whose own policy is weaker than the merged phase policy, including an agent with no policy alongside one that has a policy. This is the cross-agent overlay case the child-label prompt tests already guard.
  • Labels when the phase includes unattributed activities or omitted activities without a complete agent list: redactionContexts then spans every agent, so one weaker agent anywhere suppresses them.
  • Reasoning and commentary under any active policy.

Tests

  • activity-phase-label.test.ts: a run with a single run-level policy summarizes two labeled activities through the model, with the labels in the prompt and commentary absent; a multi-agent run where one contributing agent has no policy returns {} without a model call.
  • activity-label-prompt.test.ts: childLabelsRedacted admits labels but not reasoning or commentary; without it, a labels-only phase under an active policy still builds an empty prompt; coversToolOutputRedaction cases for a missing candidate, global disable in both directions, name subsets, and exact versus partial matching.

Both new behavior tests fail without the source change. The 13 Langfuse and activity suites pass (239 tests), and tsc --noEmit and ESLint are clean on the changed files.

Built into a LibreChat checkout that sets a uniform run_select_query / run_tools_with_bash redaction policy, this change turns the activity-fold.spec.ts e2e suite from 2 failed to 2 passed with the policy still in place.

@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-28T01:39:30.056980Z 8d9124a Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8d9124ae27

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/run.ts
Comment on lines +3186 to +3195
const childLabelsRedacted =
redaction != null &&
redactionContexts.length > 0 &&
redactionContexts.every((context) =>
coversToolOutputRedaction(
hasToolOutputTracingConfig(this.langfuse, context.langfuse)
? resolveToolOutputTracingConfig(this.langfuse, context.langfuse)
: undefined,
redaction
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Verify each label's redaction provenance before reuse

childLabelsRedacted is inferred only from the agents' current configurations, but ActivityPhaseEntry.label is a host-supplied string with no guarantee or metadata proving it was produced by generateActivityLabel under that policy. When a host supplies a legacy, manual, or otherwise unredacted committed label and all contributing agents share the active policy, this becomes true and the label—including any copied sensitive tool output—is sent to the phase model and recorded as generation input. Require per-label redaction provenance rather than treating matching agent configuration as proof.

AGENTS.md reference: AGENTS.md:L147-L147

Useful? React with 👍 / 👎.

Comment on lines +183 to +185
for (const toolName of required.redactedToolNames) {
if (!candidate.redactedToolNames.has(toolName)) {
return false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Compare partial patterns by coverage, not equality

With partial matching, set membership is not equivalent to redaction coverage: a candidate pattern such as run_ redacts every tool matched by a required pattern run_select_query, but this check returns false because the longer string is not literally in the candidate set. This occurs, for example, when the phase destination has the narrower partial rule and the contributing agent has the broader one; childLabelsRedacted remains false and a labels-only phase still returns {} despite the labels having been generated under a strictly stronger policy.

Useful? React with 👍 / 👎.

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.

1 participant