Skip to content

fix(workflows): skip dynamically bound selectors in id validation - #7799

Open
mzxchandra wants to merge 7 commits into
stagingfrom
fix/selector-lint-reference-guard
Open

mzxchandra wants to merge 7 commits into
stagingfrom
fix/selector-lint-reference-guard

Conversation

@mzxchandra

@mzxchandra mzxchandra commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Tier-2 selector validation (collectSelectorFieldsvalidateSelectorIds) is a static id-existence check. A <block.output> or {{ENV_VAR}} bound into a selector only resolves to an id at execution time (the executor resolves every block param before credential authorization), so the advisory graph-write lint reported valid dynamic bindings as resources that do not exist.

  • collectSelectorFields skips dynamically bound values per entry. A multi-select can mix literal ids with references (<a.b>,kb_real,<c.d>), so literal entries are still validated.
  • splitOutsideWorkflowReferences (in @sim/utils/workflow-references, aliased as splitOutsideReferences) splits on commas outside every reference, so <start.pick(a,b)> and <start.pick({{A}},b)> are not torn into fragments that read as bogus ids.
  • One reference scanner. The splitter and findWorkflowReferenceTokens share a single <...> candidate scan instead of a copied loop.
  • findWorkflowReferenceTokens is now linear. Its overlap check compared each workflow span against every prior token (O(tokens²): 8s at 160k tokens). Environment tokens are disjoint and ordered and spans arrive in order, so a forward cursor gives identical output in 21ms. A 300k-input differential fuzz against the previous implementation found zero mismatches. This also benefits the other callers (containsReference, the emcn code highlighter).

Behaviour is unchanged for literal ids of any length — one that does not resolve is still reported.

Scope / risk

The only production consumer is buildWorkflowLintReport (advisory: "Findings never block a write"). Credentials are re-authorized at execution time against the resolved id, so a more lenient lint cannot grant access.

Known limitations (unchanged tokenizer heuristics, not regressions)

  • A stray < before a reference swallows it as a candidate prefix: x<y,<start.pick(a,b)> still splits inside the reference, exactly as .split(',') did.
  • A literal that happens to look like a reference (kb<1,2>) is treated as one.
  • When every selector is reference-bound the lint emits no findings and no "not statically checked" note.

Test plan

  • vitestpackages/utils (229) and apps/sim lib/workflows + reference consumers (2984) pass
  • New guard tests fail with the fix reverted (25 in validation), and the tokenizer linearity test fails (32s) against the old implementation
  • bun run type-check in apps/sim, packages/utils, packages/emcn
  • biome check, bun run check:audits

A `<block.path>` or `{{ENV_VAR}}` token may legitimately contain a comma
(`<start.pick(a,b)>`), and its fragments read as plain literals once split, so a
naive `.split(',')` turns one dynamically bound value into several bogus ids.

Adds `splitOutsideReferences`, which treats only commas outside every reference
token as separators. Token spans are marked once into a lookup rather than
rescanned per comma: a per-comma `tokens.some()` is O(commas x tokens) and took
~2.5s on a 240KB value of repeated `{{A}},`, which is reachable on the 10MB
graph-write paths.

Known limitation, unchanged from the `.split(',')` this replaces and covered by a
characterization test: the tokenizer suppresses a workflow span that overlaps an
environment token, so `<start.body.pick({{A}},b)>` still splits.
Tier-2 selector validation is a static id-existence check against the workspace,
so it cannot evaluate a value whose id only arrives at execution time. A
`<block.output>` or `{{ENV_VAR}}` binding written into a selector field was
therefore reported as a resource that does not exist, on every graph write.

`collectSelectorFields` now skips those values via the existing
`containsReference`, and splits multi-select values with
`splitOutsideReferences` so a reference containing a comma is not torn into
fragments that each get validated as an id.

Filtering is per entry rather than on the whole string: a multi-select can mix
literal ids with dynamic ones, and testing `<a.b>,kb_real,<c.d>` as a whole would
drop `kb_real` along with the references.

Verified end to end against a local dev server: the three reference forms drop
from one unresolved-reference finding each to zero, while a literal id that does
not resolve still reports one.
@vercel

vercel Bot commented Sep 13, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated
docs Skipped Skipped Sep 14, 2026 10:42pm UTC

Request Review

@greptile-apps

greptile-apps Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

The PR appears safe to merge; no outstanding correctness, security, or repository-rule violations remain.

Summary

This PR prevents static selector-ID validation from reporting dynamically bound workflow and environment references as missing resources.

  • Filters dynamic entries individually while preserving validation for literal IDs in mixed selectors.
  • Adds reference-aware comma splitting, including nested environment placeholders.
  • Refactors workflow-reference scanning to linear-time processing.
  • Adds unit and integration coverage for selector filtering, splitting, malformed references, and performance.
  • Changes since the previous review convert the newly added documentation comments to TSDoc blocks.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Selector value] --> B{Comma-separated string?}
  B -->|Yes| C[Split outside workflow and environment references]
  B -->|No| D[Use original value]
  C --> E[Inspect each entry]
  D --> E
  E --> F{Contains dynamic reference?}
  F -->|Yes| G[Skip static ID validation]
  F -->|No| H[Validate literal selector ID]
  H --> I[Report unresolved literal IDs]
Loading

Reviews (6) · Last reviewed commit: "chore(workflows): use TSDoc for the sele..."

Comment thread apps/sim/lib/workflows/sanitization/references.ts Outdated
Comment thread apps/sim/lib/workflows/editing/selector-reference-guard.test.ts Outdated
@mzxchandra

Copy link
Copy Markdown
Contributor Author

@cubic review

@cubic-dev-ai

cubic-dev-ai Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

@cubic review

@mzxchandra I have started the AI code review. It will take a few minutes to complete.

Review round 1.

`findWorkflowReferenceTokens` is contractually non-overlapping, so for
`<a.pick({{B}},c)>` it reports only the inner `{{B}}` and discards the outer
candidate. That is correct for a tokenizer and wrong for a splitter, which needs
the union of protected regions rather than a disjoint set, so the comma was
unprotected and `c)>` was validated as a literal id.

Adds a candidate pass built from the tokenizer's own exported predicates, leaving
the shared package's non-overlapping contract untouched. It runs only when an
environment token is present, since overlap with one is the only reason a
workflow candidate is dropped.

Also caps the value length before tokenizing. Reference detection parses the
whole string and the tokenizer is superlinear in candidate count (795ms for a
240KB value of repeated `<a.b>,`), which this change newly puts on a write path
that admits megabytes. Past the cap the field is skipped rather than parsed; the
lint is advisory, so declining to check is the safe direction.

Adds `as const` to the test context object per the repo's TypeScript conventions.
@mzxchandra

Copy link
Copy Markdown
Contributor Author

@greptile

@mzxchandra

Copy link
Copy Markdown
Contributor Author

@cubic-dev-ai review this PR

@cubic-dev-ai

cubic-dev-ai Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review this PR

@mzxchandra I have started the AI code review. It will take a few minutes to complete.

Comment thread apps/sim/lib/workflows/editing/validation.ts Outdated
…tor value

Review round 2.

The length cap skipped the whole field, so an oversized list of plain literal ids
lost validation it previously had. Literals never needed tokenization, so the cap
was broader than the cost it was there to bound.

It now gives up only the reference-aware split: past the cap the value is split
plainly, and its entries are classified and validated as usual, since they are
short enough that the tokenizer's per-candidate cost does not apply (1MB of
literal ids across 30000 entries measures ~9ms). Only an individual entry past
the cap is skipped, where there is no cheap way to tell a literal from a dynamic
binding.
@mzxchandra

Copy link
Copy Markdown
Contributor Author

@greptile

Comment thread apps/sim/lib/workflows/editing/validation.ts
Review round 3.

An oversized value fell back to plain splitting, which tore a comma-bearing
reference into fragments that were then validated as literal ids. The fallback
existed to avoid `findWorkflowReferenceTokens`, which is superlinear in candidate
count.

That pass was never needed here. It returns contractually NON-overlapping tokens,
and the O(tokens^2) overlap check is the cost of producing that partition. A
splitter only needs to know whether an index sits inside SOME reference, so
scanning environment placeholders and `<...>` candidates independently gives the
union directly - cheaper and more accurate, since nothing is suppressed.

Splitting is now linear and the size fallback is gone, so a comma-bearing
reference survives at any length:

  240KB of `<a.b>,`          682ms -> 15ms
  240KB of `<a.p({{X}},y)>,` 177ms -> 12ms
  1MB of literal ids                   1ms

The length cap now applies to a single ENTRY rather than the whole field, which
is all it was ever needed for: classifying one entry tokenizes it, and there is
no cheap way to tell a literal from a dynamic binding past that size.

Exports `ENV_REFERENCE_PATTERN` from `@sim/utils` rather than duplicating the
pattern, so the two stay in step.
@mzxchandra

Copy link
Copy Markdown
Contributor Author

@greptile

…ng linear

Move the reference-aware list splitter into @sim/utils next to the tokenizer so both
consume a single <workflow.reference> candidate scan instead of a copied loop, and keep
ENV_REFERENCE_PATTERN private again.

findWorkflowReferenceTokens checked each workflow span against every prior token, which
is quadratic on reference-dense values (8s at 160k tokens). Environment tokens are
disjoint and ordered and spans arrive in order, so a forward cursor gives identical
output in linear time (21ms); a 300k-input differential fuzz against the previous
implementation found no mismatches.

With classification linear, the 10k-character selector entry cap is no longer needed,
so oversized literal ids are validated again instead of silently skipped.
@waleedlatif1

Copy link
Copy Markdown
Collaborator

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator

@cubic-dev-ai review this PR

@cubic-dev-ai

cubic-dev-ai Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review this PR

@waleedlatif1 I have started the AI code review. It will take a few minutes to complete.

Comment thread apps/sim/lib/workflows/editing/validation.test.ts Outdated
@waleedlatif1

Copy link
Copy Markdown
Collaborator

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator

@cubic-dev-ai review this PR

@cubic-dev-ai

cubic-dev-ai Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review this PR

@waleedlatif1 I have started the AI code review. It will take a few minutes to complete.

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.

2 participants