Skip to content

[Inference][Workflows] 09 - Add managed PII anonymization workflow and per-space installer - #288787

Draft
jonwalstedt wants to merge 55 commits into
elastic:mainfrom
jonwalstedt:anon/09-managed-pii-workflow
Draft

[Inference][Workflows] 09 - Add managed PII anonymization workflow and per-space installer#288787
jonwalstedt wants to merge 55 commits into
elastic:mainfrom
jonwalstedt:anon/09-managed-pii-workflow

Conversation

@jonwalstedt

@jonwalstedt jonwalstedt commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Summary

What

Ships the managed inference-pii-anonymization workflow definition (enabled: false by default) and the per-space installer that deploys it automatically when workflowDrivenEnabled is true. Adds the single-enabled conflict check for the inference.aroundCompletion trigger in workflows_management.

Why

This is the first point where the feature can actually be enabled. The pipeline is wired (PR #288786), but without a workflow in a space that matches the inference.aroundCompletion trigger, no anonymization runs. The managed workflow provides the out-of-the-box experience: install Kibana, enable the feature flag, and the anonymization workflow appears per-space automatically.

Two independent gates before anything is anonymized:

  1. xpack.inference.anonymization.workflowDriven: true (config flag, default false)
  2. The managed workflow is enabled in that space (default false)

Both must be on. This is deliberate — operators can enable the config globally and roll out to spaces incrementally by enabling the workflow there.

All shipped patterns are RE2-clean. Phase 1 covers IP addresses, email addresses, hostnames, and usernames. Every pattern is verified by assertRe2Compilable in the test suite — the same guard that prevents an RE2-incompatible pattern from ever shipping in the managed YAML again.

How it fits in the stack

Wave 1, depends on #288759 (Liquid template support for the YAML) and #288786 (pipeline integration). This is the final PR in the critical path. After it merges, the feature is live and operators can enable anonymization per-space through the Workflows UI.

Testing

node scripts/jest x-pack/platform/plugins/shared/kbn-workflows/managed/definitions/inference_pii_anonymization/

Confirm: all regex patterns compile under RE2; managed workflow ships with enabled: false; installer registers the workflow at plugin start when workflowDrivenEnabled is true.

Smoke test (requires full stack):

# kibana.dev.yml
xpack.workflowsExecutionEngine.syncExecution.enabled: true
xpack.inference.anonymization.workflowDriven: true
xpack.inference.anonymization.encryptionKey: <32-byte key>

Navigate to Workflows in Stack Management → confirm inference-pii-anonymization appears → enable it → fire an Agent Builder call with a known PII string → confirm the raw PII does not appear in the connector request log.

Tip

Testing the full flow: A non-merge umbrella branch combining all changes in this stack is available at #289950. Check out anon/umbrella to test the complete end-to-end anonymization pipeline without checking out individual PRs.

Stack status

# PR Title Merge order Wave Owner Depends on Status
1 #288758 Standalone PII detection runtime 1st · any order 0 search-ml-ux draft
2 #288759 kbn-workflows sync types + YAML 1st · any order 0 workflows-eng draft
3 #288760 Execution persistence abstraction 1st · any order 0 workflows-eng draft
4 #288761 Anonymization metadata propagation 1st · any order 0 search-ml-ux + workchat-eng draft
6 #288762 Inference anon config + decoupling 1st · any order 0 search-ml-ux — (before #7) draft
5 #288769 Synchronous workflow execution mode 2nd 1 workflows-eng #288759, #288760 draft
7 #288780 Inference contract + step handlers 3rd 1 search-ml-ux #288758, #288769, #288762 draft
10 #288781 Synchronous event log drain 3rd or later 1 workflows-eng #288769 draft
8 #288786 Pipeline integration + e2e test 4th 1 search-ml-ux #288780 draft
9 #288787 ← this PR Managed PII workflow + installer 5th (last) 1 workflows-eng + search-ml-ux #288759, #288786 draft

Critical path: #288759, #288760 → #288769 → #288780 → #288786 → #288787

@infra-vault-gh-plugin-prod

Copy link
Copy Markdown
🤖 Jobs for this PR can be triggered through checkboxes. 🚧

ℹ️ To trigger the CI, please tick the checkbox below 👇

  • Click to trigger kibana-pull-request for this PR!
  • Click to trigger kibana-deploy-project-from-pr for this PR!
  • Click to trigger kibana-deploy-cloud-from-pr for this PR!
  • Click to trigger kibana-entity-store-performance-from-pr for this PR!
  • Click to trigger kibana-storybooks-from-pr for this PR!

@jonwalstedt jonwalstedt changed the title [Inference][Workflows] PR9: Add managed PII anonymization workflow and per-space installer [Inference][Workflows] 09 - Add managed PII anonymization workflow and per-space installer Sep 7, 2026
@jonwalstedt
jonwalstedt force-pushed the anon/09-managed-pii-workflow branch 2 times, most recently from fc54c23 to 1d112a8 Compare September 8, 2026 10:43
@jonwalstedt
jonwalstedt force-pushed the anon/09-managed-pii-workflow branch from d3c6042 to 2d54720 Compare September 9, 2026 11:17
@jonwalstedt jonwalstedt self-assigned this Sep 9, 2026
@jonwalstedt jonwalstedt added backport:skip This PR does not require backporting Team: SecuritySolution Security Solutions Team working on SIEM, Endpoint, Timeline, Resolver, etc. release_note:feature Makes this part of the condensed release notes v9.6.0 Team: Security Investigations Security solution alert triage & investigations labels Sep 9, 2026
@jonwalstedt
jonwalstedt force-pushed the anon/09-managed-pii-workflow branch from 7bc3b6f to fc01ee7 Compare September 10, 2026 10:41
jonwalstedt and others added 2 commits September 11, 2026 11:02
…rt to kbn-workflows

Adds the schema primitives that synchronous execution and template-aware YAML validation depend on.

step_definition_types.ts:
- `StepExecutionMode = 'async' | 'sync'` union type
- `BaseStepDefinition.supportedExecutionModes?: readonly StepExecutionMode[]` optional field;
  absence means both modes are supported

builtin_step_definitions.ts:
- Marks the 5 flow-control steps (Wait, Execute Workflow, Wait For Input, Wait For Approval,
  Execute Workflow Async) as `supportedExecutionModes: ['async']`. These steps resume via
  Task Manager callbacks and cannot run in the synchronous execution path.
- These annotations are inert until PR 5's `validateSyncWorkflow` consumes them.

generate_yaml_schema_from_connectors.ts:
- `withTemplateStringSupport(paramsSchema)` widens top-level ZodArray params to also accept
  a string, so Liquid expressions like `"${{ event.request.messages }}"` are not flagged as
  invalid in the YAML editor. Only the editor-facing schema is affected — runtime validation
  uses the original strict Zod schemas.
- Only top-level children of the params ZodObject are widened (no recursive descent), so deeply
  nested schemas such as the ES API's MappingTypeMapping are not disturbed.
  (Earlier approach that recursed into nested ZodObjects broke the ES indices.create test.)

Note: withTemplateStringSupport affects every connector's editor schema, not just the
anonymization workflow's. It is a prerequisite for the Liquid templates in the managed
PII anonymization workflow YAML (PR 9).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ort and supportedExecutionModes

withTemplateStringSupport():
- Unwrap ZodDefault (in addition to ZodOptional) before checking for ZodArray,
  so array params wrapped in z.default() also receive template-string widening
- Use paramsSchema.extend(modifications) instead of z.object(newShape) to
  preserve the original ZodObject's unknownKeys, catchall, and refinement config

supportedExecutionModes: tighten from StepExecutionMode[] to
[StepExecutionMode, ...StepExecutionMode[]] to disallow the empty-array case

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
jonwalstedt and others added 29 commits September 11, 2026 11:02
…trolled thread pre-warming

Replaces the implicit minThreads override (which silently forced minThreads to maxThreads when
workflowDriven was enabled) with an explicit xpack.inference.workers.anonymization.workflowDrivenMinThreads
config key (default 3). Operators running workflowDriven: false are unaffected. Operators running
workflowDriven: true can lower the value to allow partial thread scaling, or set it to 0 to accept
cold-start risk. Values above maxThreads produce a startup warning and are clamped.

Also removes unsupported `sensitive: true` from encryptionKey schema.string() options and drops
the unused workflowAnonymizationOptions class field.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…_workflows integration

Adds the public server contract for workflow-driven anonymization:
- Exports capability helpers (createPiiTokenizationCapabilityValue,
  createInferenceProceedCapabilityValue, resolve variants) from inference plugin server index
- New inference_workflows step handlers: ai.pii, call_site.proceed, transform.pii_restore
- Token map, message records, anonymization metrics, capabilities helpers
- inference_workflows plugin wired to register the anonymization provider with the
  inference setup contract
- inference_workflows kibana.jsonc gains required plugin deps

Runtime no-op until the pipeline integration (PR 8) wires the around-completion hook.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ependency

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Kibana config schema properties are camelCase; snake_case in the YAML
path reference was incorrect.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ete path

Wire up the aroundCompletion hook's anonymization provider to the callback
API: detect PII via the standalone PiiRegexWorkerService, tokenize with
HMAC-SHA256 entity tokens, stream-restore content and tool-call arguments,
and fall back (or block) based on the configured failureMode.

Includes the streaming content restorer (with MIN_PREFIX_HOLDBACK=2
trade-off documented in tests), OTel metrics for first-chunk latency and
request outcomes, and the inference_workflows integration test as a
release gate.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…adata field

PR 04 introduced WorkflowAnonymizationContext on ChatCompleteMetadata.workflowAnonymization
to avoid coupling to ChatCompleteAnonymizationMetadata (Steph's Anonymization Platform
Service, scheduled for deletion). Update the pipeline read path accordingly.
… options in pipeline

PR 04 adds agentId flat on ChatCompleteMetadata (no wrapper). sessionId is already
a top-level ChatCompleteOptions field available in scope. Update the pipeline read
path accordingly.
…se path

Mirror of the anon/06 fix: thread encryptionKey through
WorkflowAnonymizationOptions and read it directly in the pipeline instead
of pulling it from anonymization.saltPromise (which is always undefined
while ANONYMIZATION_FEATURE_ACTIVE is hardcoded false). Removes saltPromise
from the workflow pipeline interface entirely — the legacy path keeps its
own saltPromise marked for deletion with the anonymization plugin.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Marks the config value as sensitive so it is redacted from diagnostics,
and adds a 512-character maxLength as an operator sanity guard.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…trolled thread pre-warming

Replaces the implicit minThreads override (which silently forced minThreads to maxThreads when
workflowDriven was enabled) with an explicit xpack.inference.workers.anonymization.workflowDrivenMinThreads
config key (default 3). Operators running workflowDriven: false are unaffected. Operators running
workflowDriven: true can lower the value to allow partial thread scaling, or set it to 0 to accept
cold-start risk. Values above maxThreads produce a startup warning and are clamped.

Also removes unsupported `sensitive: true` from encryptionKey schema.string() options.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
sessionId is a top-level ChatCompleteOptions field; agentId is flat on
ChatCompleteMetadata. The test was written against the original
metadata.anonymization.* shape that was refactored away by two
successive fix commits.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…aller

Registers the inference_pii_anonymization managed workflow definition in
kbn-workflows and wires a per-space installer in the inference_workflows
plugin so the workflow is available in every space when
workflowDrivenEnabled is true.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…w installer

Replaces the single find({ perPage: 10_000 }) call with a do-while loop
(1 000 per page) so the managed workflow is installed in all spaces even
when a deployment has more than 10 000 spaces.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… and workflowAnonymization blocks

Follows the same split as 06-inference-anon-config. Each service now has
independent config; enabling/disabling one pool does not affect the other.
PiiRegexWorkerService uses WorkflowAnonymizationWorkerConfig from
workers.workflowAnonymization; RegexWorkerService keeps workers.anonymization.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In Zod v4 classic, `.unwrap()` and `.removeDefault()` return `core.$ZodType`
(the raw base type), not `z.ZodType` (the extended classic layer). The reassignment
`value = value.unwrap()` therefore fails the type checker.

Fix by casting to `z.ZodType` (accurate at runtime — all Kibana schemas use the
classic layer) and replace the deprecated `.removeDefault()` with `.unwrap()`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…+ add tests

withTemplateStringSupport() was accepting any string for array params via
z.string(), which silently passes plain mistyped values through the YAML editor
schema. Replace with a bounded regex that only accepts whole-value Liquid
expressions ({{ expr }} / ${{ expr }}), matching all real-world usage seen in
workflow YAML examples.

Also adds TEMPLATE_EXPRESSION_MAX_LENGTH = 500 to constants, and a regression
test suite covering: valid templates, rejected plain/partial strings, length
limit, optional and default-wrapped arrays, non-array fields unchanged, and
object unknownKeys policy preservation after .extend().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
extend() throws for ZodObject schemas that contain object-level refinements
(.refine / .superRefine). Because withTemplateStringSupport() calls extend()
on the connector's paramsSchema unconditionally (when array fields exist), a
single connector with a refined paramsSchema caused the entire workflow schema
construction to throw, rather than just losing that one connector's widening.

Fix by switching to safeExtend(), which preserves refinements and the
unknownKeys policy (strict/passthrough). Also add an early return when there
are no array fields to widen, avoiding any extend call when no changes are
needed. Adds a regression test covering: schema construction does not throw
for a refined paramsSchema, template strings still accepted, and the
object-level refinement is preserved after widening.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ness

- Extract WHOLE_VALUE_TEMPLATE_EXPRESSION_REGEX + isWholeValueTemplateExpression
  to common/template_expressions.ts; move TEMPLATE_EXPRESSION_MAX_LENGTH there.
  Single definition shared by schema and the new cross-package test.

- Fix regex: bare {{ }} and padded ${{ }} forms can never resolve to an array
  at runtime (templating_engine.ts:98 checks startsWith/endsWith without trim),
  and multi-expression concatenations throw mid-execution. New regex requires
  ${{, forbids inner }}, and drops the \s* tolerance that diverged from runtime.

- Fix withTemplateStringSupport: .default() was unwrapped but never re-applied,
  turning z.array(...).default([]) into a required field. Captured and restored.
  Loop replaces if-chain so stacked .optional().default() in any order also works.
  Verified against the real InferenceRerankParamsSchema shape.

- Hoist LIQUID_TEMPLATE_SCHEMA to module scope (was rebuilt per field per connector).

- Correct withTemplateStringSupport doc comment: this schema gates workflow
  create/update in workflows_management server-side, not just the Monaco editor.

- Expand supportedExecutionModes JSDoc: broaden the async-only criterion to cover
  scheduling work (workflow.executeAsync), document the fail-open default with
  explicit reasoning and the trade-off.

- Add builtin_step_definitions.test.ts: pins all five async-only steps and the
  completeness set, so a dropped annotation fails CI rather than hanging a request.

- Add template_expressions_runtime.test.ts in workflows_execution_engine: drives
  each accepted template form through WorkflowTemplatingEngine and asserts the
  result is an array; pins rejected forms with per-case rationale. Cross-package
  invariant that prevents schema and runtime from drifting.

- Update PR description: fix inverted validateSyncWorkflow sentence, correct
  blast-radius note, add what the new tests establish.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The test claimed to verify the advance-one-char guard for zero-length
matches, but a+ can never produce a zero-length match so the guard was
never exercised. Switching to a* causes RE2 to emit zero-length hits
between non-'a' characters; the guard now fires and both 'aaa' runs are
still found, as asserted.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rkerService

Adds RE2JS.compile() guard in runSync so patterns requiring native RegExp
(lookahead/lookbehind/backreferences) throw before execution when the worker
pool is disabled. The worker pool provides ReDoS containment via isolation and
task timeout; the sync path has neither, so catastrophic backtracking on the
event loop must be prevented at the gate.

Adds test asserting the sync path rejects a positive-lookahead pattern.
Renames AnonymizationWorkerConfig to WorkflowAnonymizationWorkerConfig to
reflect the dedicated pool; the placeholder alias in config.ts is superseded
when merged with 06-inference-anon-config.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… rebuild

Address review feedback: on task timeout, destroying and recreating the
whole Piscina pool rejects all other in-flight detection tasks sharing
the pool (up to maxThreads concurrently), not just the timed-out one.
Piscina already terminates the specific worker thread when the task's
AbortSignal fires, so relying on that signal alone contains the runaway
task without collateral impact to sibling requests.

Reviewed-at: elastic#288758 (comment)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- entity_mask.test.ts: compare hash portions only in delimiter-collision
  test; full-token comparison always passes when entityClass prefixes differ
- types.ts: correct allow_unsafe doc to reflect whole-payload fail-open
  behavior (returns [] for entire payload, not per-rule skipping)

Finding 1 (executeRegexRules exported) was already resolved before review.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove executeRegexRules from index.ts — it is a worker-internal
  function, not a public API; exporting it invited callers to bypass the
  pool, failureMode, and the RE2 sync gate
- Consolidate sync-path RE2 enforcement: add re2Only flag to compileRule
  and executeRegexRules so runSync no longer pre-compiles each pattern
  twice; a single pass now both validates and compiles
- Drop typeof value !== 'string' guard in executeRegexRules — Record<string,string>
  guarantees string values by type; the check was contradicting the contract
- Add distinct error message for Piscina queue-at-capacity errors so
  operators can tell queue saturation apart from bad-pattern failures
- Validate non-empty entityClass in generateEntityToken
- Use WorkflowAnonymizationWorkerConfig in test (was AnonymizationWorkerConfig,
  will diverge when elastic#288762 lands)
- Use service.stop() in afterEach instead of casting to any
- Add assert_re2_compilable.test.ts covering valid patterns, RE2-unsupported
  constructs, invalid syntax, and error message content
- Add maxQueue overflow tests using a spy on the internal Piscina run method
- Update config.ts placeholder comment with TODO(elastic#288762)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ce.run()

Address review feedback: the JSDoc said "logs and skips the offending rule"
but the implementation catches any payload failure and returns [] for the
entire payload, not per-rule. Updated to "logs and returns no matches for
the entire payload".

Reviewed-at: elastic#288758 (comment)
…RegexWorkerService

Address review feedback: the service matched 'Task queue is at capacity' but
Piscina 5.3.1 throws 'Task queue is at limit', so the saturation branch never
fired. Fixed the string in the service and the mock in the test. Piscina does
not expose a stable error code, so message matching is the only option;
added a comment pinning the verified version.

Reviewed-at: elastic#288758 (comment)
…es.test.ts

Address review feedback: the describe block was titled "skips empty and
non-string field values" but the payload type is Record<string, string> so
non-string values are impossible, and the test only covers empty strings.
Renamed to "skips empty string fields".

Reviewed-at: elastic#288758 (comment)
…RegexWorkerService

Previously allow_unsafe returned [] for the entire payload when any rule
failed to compile — so one broken rule silenced all other rules' matches,
letting all PII through unmasked. Now invalid rules are filtered and logged
individually before dispatch; surviving rules still execute and return their
matches. Infrastructure failures (timeout, queue saturation, worker crash)
still produce a whole-payload [] because there are no partial results to save.

compileRule is exported from execute_regex_rules.ts so the pre-filter reuses
the same RE2-first → native-fallback logic without duplication. On the sync
path re2Only=true is forwarded, so non-RE2 patterns are also skipped (they
would block the event loop).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…d allow_unsafe semantics

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@jonwalstedt
jonwalstedt force-pushed the anon/09-managed-pii-workflow branch from 5648958 to 2cf1f9a Compare September 11, 2026 09:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport:skip This PR does not require backporting release_note:feature Makes this part of the condensed release notes Team: Security Investigations Security solution alert triage & investigations Team: SecuritySolution Security Solutions Team working on SIEM, Endpoint, Timeline, Resolver, etc. v9.6.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant