Skip to content

perf(pii): mask offloaded refs + parallelize + raise redaction concurrency#5436

Open
TheodoreSpeaks wants to merge 3 commits into
stagingfrom
perf/pii-redaction-throughput
Open

perf(pii): mask offloaded refs + parallelize + raise redaction concurrency#5436
TheodoreSpeaks wants to merge 3 commits into
stagingfrom
perf/pii-redaction-throughput

Conversation

@TheodoreSpeaks

@TheodoreSpeaks TheodoreSpeaks commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Correctness fix: block-output and input redaction stages now hydrate → mask → re-store large-value refs. Function/tool outputs are offloaded to refs before reaching the redactor, and the string walk treats a ref as opaque — so big block outputs were coming back unredacted. Now covered (fails fast on the execution path).
  • Parallelize large-value ref processing: collect refs across the whole payload and hydrate/mask/re-store them with bounded concurrency instead of one sequential pass per key (PII_REF_CONCURRENCY, default 4).
  • Raise throughput: mask-batch chunk concurrency default 4 → 64 to saturate the load-balanced Presidio fleet behind the new internal ALB (PII_MASK_CHUNK_CONCURRENCY, env-tunable).

Type of Change

  • Bug fix
  • Performance improvement

Testing

  • tsc clean, bun run lint, check:api-validation:strict pass
  • 40 unit tests pass (added multi-ref/parallel + throw-mode coverage in pii-large-values.test.ts)
  • Diagnosed against staging run b11e8f19 (function output offloaded to refs, zero Presidio calls → confirmed refs skipped)

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

… raise concurrency

- Block-output and input stages now hydrate → mask → re-store large-value refs
  (function/tool outputs are offloaded to refs before reaching the redactor, which
  treats refs as opaque) — fixes big block outputs coming back unredacted
- Parallelize large-value ref hydrate/mask/re-store: collect refs across the whole
  payload and process them with bounded concurrency instead of one sequential pass
  per key (PII_REF_CONCURRENCY, default 4)
- Raise mask-batch chunk concurrency default 4 -> 64 to saturate the load-balanced
  Presidio fleet behind the internal ALB (PII_MASK_CHUNK_CONCURRENCY, env-tunable)
@vercel

vercel Bot commented Jul 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jul 6, 2026 8:00pm

Request Review

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile review

@cursor

cursor Bot commented Jul 6, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Changes in-flight PII handling on the execution path (inputs, block outputs, resumed state) and raises default Presidio load; mis-tuned concurrency could overload the fleet, but fail-fast throw behavior preserves the prior abort semantics on redaction failure.

Overview
Fixes a PII leak where block outputs (and workflow inputs) already offloaded to large-value refs skipped masking because inline string redaction treats refs as opaque. In-flight stages now run hydrate → mask → re-store refs, then mask inline strings — in block-executor for block outputs and in execution-core for workflow input (resume snapshot block-state re-mask was already on this two-pass model).

redactLargeValueRefs no longer processes payload keys sequentially: it collects all refs once (deduped by identity) and hydrates/masks/re-stores in parallel via resolveReplacements and PII_REF_CONCURRENCY (default 4), with throw-mode still aborting when any ref fails.

Throughput knobs: default app → mask-batch concurrency 4 → 64 (PII_MASK_CHUNK_CONCURRENCY); new PII_REF_CONCURRENCY and PII_SERVICE_CHUNK_CONCURRENCY (Presidio route fan-out, was hardcoded 4). Tests cover multi-key parallel refs and throw-on-partial failure.

Reviewed by Cursor Bugbot for commit 0aa7648. Bugbot is set up for automated code reviews on this repo. Configure here.

@greptile-apps

greptile-apps Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a correctness gap in PII redaction: block outputs that exceed the large-value threshold are offloaded to object-storage refs before reaching the redactor, and the plain string walk treats a ref as opaque — so PII inside those refs was never masked. The fix adds a redactLargeValueRefs hydrate → mask → re-store pass before the string walk at both the block-output and workflow-input redaction points.

  • Bug fix: redactLargeValueRefsInValue is now called ahead of redactObjectStrings in both block-executor.ts and execution-core.ts, ensuring large-value refs are materialized, masked via Presidio, and re-stored before the payload continues downstream.
  • Parallelization: resolveReplacements collects all refs across the whole payload in one shared walk, then hydrates/masks/re-stores them in parallel (bounded by PII_REF_CONCURRENCY, default 4) using mapWithConcurrency with a total mapper — satisfying the utility's "fn must not reject" contract by catching per-ref errors and rethrowing the first one only after the pool drains.
  • Throughput increase: PII_MASK_CHUNK_CONCURRENCY default raised from 4 → 64 to saturate the load-balanced Presidio fleet behind the internal ALB; env-tunable for single-instance deployments.

Confidence Score: 5/5

The change is safe to merge: the correctness fix is well-scoped, the parallel pool uses a total mapper that correctly defers the abort until after all workers drain, and both new test cases pin the security-critical throw-mode path.

All three call sites follow the same hydrate → mask → re-store ordering. The mapWithConcurrency contract is satisfied — the mapper never rejects; errors are captured and rethrown after the pool completes. The raised CHUNK_CONCURRENCY default is env-tunable and the existing comment explicitly calls out the single-instance tuning knob. No data-path regressions were found.

No files require special attention.

Important Files Changed

Filename Overview
apps/sim/lib/logs/execution/pii-large-values.ts Core of the fix: introduces parallel ref collection+resolution with a total mapper that satisfies mapWithConcurrency's no-reject contract; logic and error handling are sound.
apps/sim/executor/execution/block-executor.ts Inserts redactLargeValueRefsInValue before redactObjectStrings for block-output PII redaction; ordering and store context are correct.
apps/sim/lib/workflows/executor/execution-core.ts Same pattern as block-executor: hydrates refs before inline string masking for input redaction; correctly converts nullable userId to undefined.
apps/sim/lib/logs/execution/pii-large-values.test.ts Adds multi-ref parallel test and multi-ref throw-mode abort test; covers the security-critical path where one failing ref must abort the whole run.
apps/sim/lib/guardrails/mask-client.ts Raises CHUNK_CONCURRENCY default from 4 to 64; comment updated to reflect the load-balanced fleet sizing rationale.
apps/sim/lib/core/config/env.ts Adds PII_REF_CONCURRENCY env var and updates PII_MASK_CHUNK_CONCURRENCY comment to reflect new default.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant BE as BlockExecutor / ExecutionCore
    participant LV as redactLargeValueRefsInValue
    participant CR as collectRefs (sync walk)
    participant RR as resolveReplacements (parallel)
    participant S3 as Object Storage
    participant PR as Presidio (maskPIIBatchViaHttp)
    participant RS as redactObjectStrings

    BE->>LV: normalizedOutput (may contain LargeValueRefs)
    LV->>CR: walk payload, collect all refs
    CR-->>LV: refs[]
    LV->>RR: "unique refs, bounded concurrency (PII_REF_CONCURRENCY=4)"
    loop For each ref (parallel)
        RR->>S3: materializeLargeValueRef
        S3-->>RR: materialized value
        RR->>PR: "maskPIIBatchViaHttp (PII_MASK_CHUNK_CONCURRENCY=64)"
        PR-->>RR: masked value
        RR->>S3: compactExecutionPayload (re-store masked)
        S3-->>RR: new LargeValueRef
    end
    RR-->>LV: replacements Map
    LV->>LV: substituteRefs (sync swap)
    LV-->>BE: payload with refs replaced by masked refs
    BE->>RS: redactObjectStrings (mask inline strings)
    RS-->>BE: fully masked output
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant BE as BlockExecutor / ExecutionCore
    participant LV as redactLargeValueRefsInValue
    participant CR as collectRefs (sync walk)
    participant RR as resolveReplacements (parallel)
    participant S3 as Object Storage
    participant PR as Presidio (maskPIIBatchViaHttp)
    participant RS as redactObjectStrings

    BE->>LV: normalizedOutput (may contain LargeValueRefs)
    LV->>CR: walk payload, collect all refs
    CR-->>LV: refs[]
    LV->>RR: "unique refs, bounded concurrency (PII_REF_CONCURRENCY=4)"
    loop For each ref (parallel)
        RR->>S3: materializeLargeValueRef
        S3-->>RR: materialized value
        RR->>PR: "maskPIIBatchViaHttp (PII_MASK_CHUNK_CONCURRENCY=64)"
        PR-->>RR: masked value
        RR->>S3: compactExecutionPayload (re-store masked)
        S3-->>RR: new LargeValueRef
    end
    RR-->>LV: replacements Map
    LV->>LV: substituteRefs (sync swap)
    LV-->>BE: payload with refs replaced by masked refs
    BE->>RS: redactObjectStrings (mask inline strings)
    RS-->>BE: fully masked output
Loading

Reviews (3): Last reviewed commit: "fix(pii): keep resolveReplacements mappe..." | Re-trigger Greptile

Comment thread apps/sim/lib/logs/execution/pii-large-values.ts
Comment thread apps/sim/lib/logs/execution/pii-large-values.test.ts
@greptile-apps

greptile-apps Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a correctness gap where PII inside large-value refs (function/tool outputs offloaded to object storage before reaching the redactor) was never sent to Presidio, and simultaneously raises throughput by parallelizing ref hydration and increasing the mask-batch chunk concurrency default from 4 to 64.

  • Correctness fix: both the block-output and workflow-input redaction stages now run a redactLargeValueRefsInValue pass (hydrate → mask → re-store) before the inline redactObjectStrings walk, closing the gap where opaque refs bypassed PII redaction entirely.
  • Parallelization: refs are now collected across the whole payload first and processed in one bounded-concurrency pass (PII_REF_CONCURRENCY, default 4) instead of one sequential pass per payload key, with a shared seen WeakSet preventing duplicate work on refs that appear under multiple keys.
  • Concurrency raise: PII_MASK_CHUNK_CONCURRENCY default 4 → 64, sized for a load-balanced Presidio fleet behind an internal ALB; multiplied with REF_CONCURRENCY the new ceiling is 256 concurrent Presidio requests per payload (up from 16).

Confidence Score: 4/5

Safe to merge; the correctness fix is sound and tests cover the critical paths. The one rough edge is a contract tension in the concurrency utility that works today but could surprise a future maintainer.

The hydrate→mask→re-store ordering is correct, the shared seen WeakSet prevents duplicate work, and substituteRefs correctly uses Map identity to swap refs. The one concern worth tracking: resolveReplacements passes a mapper that intentionally throws (in throw mode) to mapWithConcurrency, whose documented contract says fn MUST NOT reject. The behavior is currently correct because Promise.all propagates the rejection as intended, but if mapWithConcurrency is ever changed to isolate per-item errors, the abort-on-failure behavior in throw mode would silently stop working.

apps/sim/lib/logs/execution/pii-large-values.ts — the resolveReplacements + mapWithConcurrency interaction in throw mode is the one area that deserves a second look.

Important Files Changed

Filename Overview
apps/sim/lib/logs/execution/pii-large-values.ts Core fix for PII leakage through opaque large-value refs; parallelizes ref hydrate→mask→re-store with bounded concurrency. One contract tension: the mapper passed to mapWithConcurrency can throw in 'throw' mode, which the utility's documented contract prohibits.
apps/sim/executor/execution/block-executor.ts Adds redactLargeValueRefsInValue call before redactObjectStrings so block outputs stored as large-value refs are hydrated, masked, and re-stored before inline string masking. Order is correct; the subsequent compactExecutionPayload pass safely sees already-stored refs as opaque leaf values.
apps/sim/lib/workflows/executor/execution-core.ts Adds the same ref-hydrate→mask→re-store pass for workflow input redaction, mirroring the block-output fix. Store context (workspaceId/workflowId/executionId/userId) is correctly forwarded.
apps/sim/lib/guardrails/mask-client.ts Default CHUNK_CONCURRENCY raised from 4 to 64 to saturate the load-balanced Presidio fleet. The combined ceiling (REF_CONCURRENCY × CHUNK_CONCURRENCY = 4 × 64 = 256 concurrent Presidio requests per payload) is a significant jump from the old 16; appropriate for the described ALB-backed fleet, but self-hosted single-instance users need to set PII_MASK_CHUNK_CONCURRENCY=1.
apps/sim/lib/core/config/env.ts Adds PII_REF_CONCURRENCY env var with a clear description including the multiplicative relationship to PII_MASK_CHUNK_CONCURRENCY; straightforward env addition.
apps/sim/lib/logs/execution/pii-large-values.test.ts Adds parallel cross-key ref test and throw-mode coverage for redactLargeValueRefsInValue; covers materialization failure and re-store errors. Scrub-mode throw coverage for redactLargeValueRefs is absent but that function is only used with the default scrub mode in practice.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant BE as BlockExecutor
    participant LV as redactLargeValueRefsInValue
    participant OS as Object Storage
    participant PR as Presidio
    participant RS as redactObjectStrings

    BE->>LV: normalizedOutput with large-value refs
    LV->>LV: collectRefs sync walk
    LV->>OS: materializeLargeValueRef parallel x REF_CONCURRENCY
    OS-->>LV: materialized values
    LV->>PR: maskPIIBatchViaHttp parallel chunks x CHUNK_CONCURRENCY
    PR-->>LV: masked values
    LV->>OS: compactExecutionPayload re-store as new ref
    OS-->>LV: new masked LargeValueRef
    LV-->>BE: output with fresh masked refs
    BE->>RS: redactObjectStrings inline strings only
    RS-->>BE: fully masked output
    BE->>BE: compactExecutionPayload final pass
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant BE as BlockExecutor
    participant LV as redactLargeValueRefsInValue
    participant OS as Object Storage
    participant PR as Presidio
    participant RS as redactObjectStrings

    BE->>LV: normalizedOutput with large-value refs
    LV->>LV: collectRefs sync walk
    LV->>OS: materializeLargeValueRef parallel x REF_CONCURRENCY
    OS-->>LV: materialized values
    LV->>PR: maskPIIBatchViaHttp parallel chunks x CHUNK_CONCURRENCY
    PR-->>LV: masked values
    LV->>OS: compactExecutionPayload re-store as new ref
    OS-->>LV: new masked LargeValueRef
    LV-->>BE: output with fresh masked refs
    BE->>RS: redactObjectStrings inline strings only
    RS-->>BE: fully masked output
    BE->>BE: compactExecutionPayload final pass
Loading

Reviews (2): Last reviewed commit: "perf(pii): mask offloaded refs in block ..." | Re-trigger Greptile

Comment thread apps/sim/lib/logs/execution/pii-large-values.ts
…rrency contract)

- Catch per-ref errors inside the mapWithConcurrency mapper and rethrow the first
  after the pool drains, instead of letting a throwing mapper reject the pool
  (the util documents fn MUST NOT reject). Preserves fail-fast abort in throw mode.
- Add multi-ref throw-mode test: one of several refs failing aborts the redaction
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile review

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

Both re-raised Greptile P2s are already addressed in 932e2f90d (the re-review re-posted them against the same lines):

  1. mapWithConcurrency contractresolveReplacements now uses a total mapper: it try/catches per-ref errors and records the first, then rethrows after the pool drains (pii-large-values.ts:126–133). The fn no longer rejects, so the util's "fn MUST NOT reject" contract holds, and throw mode still fails fast (an unmaskable ref aborts the run).
  2. Multi-ref throw coverage — added aborts (throws) when one of several refs fails in throw mode in pii-large-values.test.ts, which exercises exactly the parallel-pool + PiiRedactionError propagation path.

…RVICE_CHUNK_CONCURRENCY)

Was hardcoded to 4; now env-tunable (default 4) so the inner route->Presidio
fan-out can scale with the fleet alongside the outer PII_MASK_CHUNK_CONCURRENCY.
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