Skip to content

fix(core): guard filter-token near-miss lookup against Object.prototype (objectui#9129) - #9396

Merged
os-tesla merged 1 commit into
mainfrom
claude/issue-9129-filter-token-proto-guard
Sep 13, 2026
Merged

fix(core): guard filter-token near-miss lookup against Object.prototype (objectui#9129)#9396
os-tesla merged 1 commit into
mainfrom
claude/issue-9129-filter-token-proto-guard

Conversation

@os-tesla

Copy link
Copy Markdown
Collaborator

Fixes #9129

resolveContextTokens (packages/core/src/utils/filter-tokens.ts) looked a near-miss
placeholder spelling up in @objectstack/spec's CONTEXT_TOKEN_SUGGESTIONS map with a
plain bracket index keyed on token.toLowerCase(). Two spellings that survive
WHOLE_TOKEN_RE ([a-zA-Z0-9_]+) lower-case to inherited Object.prototype member
names — constructor and __proto__ — so the index resolved to
Object.prototype.constructor / Object.prototype.__proto__ instead of undefined,
and the resulting warning asserted a "suggestion" that was native-code / object text:
not a token, not spellable, not anything an author can act on.

This is not prototype pollution, and this PR does not describe it as one. The index
is a read, never an assignment; the resolved filter value is passed through untouched
either way, so no filter is ever widened or narrowed and no record is ever mis-matched.
The only observable effect of the bug was a confusing string inside a single
console.warn call.

Fix

Per the card's own framing, this closes the class rather than special-casing the two
names measured today (which would just reproduce the defect the next time a future
lower-case Object.prototype member is added): the lookup now runs over a
null-prototype copy of the suggestion map —

const NEAR_MISS_SUGGESTIONS: Readonly<Record<string, ContextTokenName>> = Object.assign(
  Object.create(null),
  CONTEXT_TOKEN_SUGGESTIONS,
);

— built once, local to this one call site. @objectstack/spec's own map is untouched;
the identical shape in its classifyFilterToken (same map, wider token regex, and a
typed return field rather than a log string) is objectstack#17762, filed separately by
triage and out of scope here — fixing this side does not cover that one.

Tests

Pin added beside the existing near-miss cases in
packages/core/src/utils/__tests__/filter-token-proto-guard-9129.test.ts, asserting
silence for {constructor} and {__proto__} (never today's [native code] /
[object Object] strings — pinning those would only prove the two names are
special-cased) plus a lit near-miss control ({user_id} still warns with a real
suggestion), proving the suggestion feature itself is not broken by the fix.

Ablation (mutation + restore, on a committed tree)

Fix committed first, then mutated NEAR_MISS_SUGGESTIONS[...] back to
CONTEXT_TOKEN_SUGGESTIONS[...] under a trap … EXIT INT TERM restoring from HEAD:

  • Mutated → RED, reproducing the exact defect text:
    FAIL … "constructor" … did you mean "{function Object() { [native code] }}"?
    FAIL … "__proto__" … did you mean "{[object Object]}"?
    2 failed | 1 passed (3)
    
  • Restored → GREEN: git diff HEAD empty, 3 passed (3).

Verification

Gate family derived by hand from package.json (no dispatch-gates.mjs in this repo).
Dependency closure (pnpm --filter '@object-ui/core^...' build) built first
(@object-ui/types, @object-ui/test-support), then:

check result
pnpm --filter @object-ui/core build (tsc + dist-completeness) exit 0 — 204 files verified
pnpm exec vitest run packages/core/ 154 files / 3259 tests passed
packages/core type-check (tsc --noEmit + tsc -p tsconfig.test.json) exit 0, exit 0
pnpm exec eslint on the 2 touched source/test files exit 0, 0 errors (6 pre-existing any warnings, none on touched lines)
node scripts/check-changeset-presence.mjs ✅ 1 changeset declared for 1 released package
node scripts/check-changeset-no-major.mjs ✅ no major bump
control-byte self-scan (grep -naP on the 3 changed files) clean, 0 hits

No public export of @object-ui/core changed (NEAR_MISS_SUGGESTIONS is a private
module-level const), so per the repo's own scope rule for byte-identical public
surfaces, no downstream-consumer sweep or added consumer tests are needed.

Lint (full), Type Check (full), Test (4 shards), Build & E2E, Build Docs are left to CI
per this repo's "local runs targeted gates, CI runs the farm" convention — none of them
is implicated by a change confined to one private lookup inside @object-ui/core.

Acceptance notes

Nothing found outside this card's fence during the fix.


🤖 Generated with Claude Code

https://claude.ai/code/session_011QreXiyMEqKLN4U5daMPVa


Generated by Claude Code

resolveContextTokens indexed the spec's near-miss suggestion map with a
plain bracket index, so a lower-cased token that happens to be an
inherited Object.prototype member name (constructor, __proto__) resolved
to that member instead of undefined, and the warning asserted a
"suggestion" that was native-code / object text rather than a real token.

The lookup is read-only and the resolved value is always passed through
untouched, so no filter widens or narrows and no record is mis-matched by
this -- the only observable effect was a confusing console.warn string.

Fix: build the lookup over a null-prototype copy of the suggestion map
instead of special-casing the two known spellings, closing the whole
class of collisions (any inherited member, present or future).

Pin asserts silence for `constructor` / `__proto__` plus a lit near-miss
control, beside the existing near-miss cases.

Part of #9129

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011QreXiyMEqKLN4U5daMPVa
@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Console Performance Budget — gauge not trustworthy

The eager closure was measured, but one of the ceilings it is measured against no longer means what it names, so this run carries no pass/fail verdict for the performance budget.

This is not a budget violation. Nothing grew: the half marked below is a verdict about the gauge, and a ceiling that has stopped measuring anything can neither clear a bundle nor condemn one.

Step Outcome
Build packages success
Check console performance budget failure

Which half objected:

Eager-closure half Verdict
Aggregate closure ceiling ✅ pass
Per-chunk ceilings ✅ pass
Ceiling sensitivity (headroom) ⚠️ broken gauge
Ceiling freshness (checkout vs. base branch) ✅ pass

⚠️ A broken gauge half is a verdict about the ceiling, not about the bundle: that line has drifted out of range of the regression it exists to catch, or the report behind it cannot be trusted. It does not say anything grew. The Check console performance budget step log carries the ceiling and the number it was compared against.

Reason: The entry chunk measured 144.3 KB, but the eager-closure half of this gate returned no trustworthy VERDICT: the report could not be read, a ceiling has drifted out of range of the regression it must catch, or (objectui#6245) a ceiling was replaced on the base branch after this checkout was made. The step log says which. This is not a passing budget — and it is not a size regression either.

See the workflow run for details.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 502.02KB 115.16KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 211.58KB 58.68KB
fields (index.js) 247.89KB 62.50KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 32.15KB 10.49KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.83KB 10.95KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.25KB 13.99KB
plugin-charts (index.js) 71.34KB 19.90KB
plugin-chatbot (index.js) 195.34KB 46.51KB
plugin-dashboard (index.js) 131.22KB 34.59KB
plugin-designer (index.js) 215.94KB 44.33KB
plugin-detail (index.js) 253.46KB 65.85KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.77KB 34.17KB
plugin-gantt (index.js) 166.95KB 41.04KB
plugin-grid (index.js) 211.66KB 57.50KB
plugin-kanban (index.js) 46.00KB 14.30KB
plugin-list (index.js) 112.58KB 27.65KB
plugin-map (index.js) 20.64KB 6.86KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.41KB 11.93KB
plugin-timeline (index.js) 30.07KB 8.74KB
plugin-tree (index.js) 9.55KB 3.32KB
plugin-view (index.js) 84.42KB 20.79KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 96.00KB 31.71KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.04KB 5.36KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Copy link
Copy Markdown
Collaborator Author

Bundle Analysis is red on this head, and it is ⛔ not this PR's to fix — the gate says so itself

domain:ui PM seat, session_011QreXiyMEqKLN4U5daMPVa (os-tesla), reviewing this PR. Recording the gate and the reason, because entering the merge queue with a red non-required check is only permissible with this note on the PR.

Every budget in the job passed. Read from the job log, ⛔ not from the check summary:

measurement reading
entry chunk ✅ 144.3 KB gzip / 350 KB
aggregate eager closure ✅ 3106.1 KB / 3134.8 KB (52 of 528 chunks)
per-chunk ceilings (4 weighed) ✅ all four — vendor-objectstack, ui-components, framework, i18n-locale-en
ceiling freshness ✅ all 3 constants unchanged on main since this checkout's base

The single ❌ is a verdict about the gauge, not about the bundle — the checker exited 2 (BUDGET_CLOSURE_HEADROOM_STATUS: error, rendered kind: not-measured), because the ui-components row's headroom has fallen to 1.9 KB = 0.02x of the 89.0 KB regression the gate exists to catch, under its 0.10x floor.

The gate's own output, verbatim:

⚠️ READ THIS BEFORE AUDITING YOUR OWN DIFF. This row's headroom is a standing debt that predates this change, and it moves under traffic that has nothing to do with the chunk — measured at three gzipped bytes across five unrelated merges. So this verdict is NOT an accusation that your diff spent the bytes, and the amount it names is very likely not yours. What it asserts is only that the row is tighter than the day it was pinned.

⛔ There is therefore nothing here for this pull request to "fix", and the two edits that would turn this green are both forbidden: ⛔ never raise the ceiling, and ⛔ never raise the allowance. Paying the row down is the open decision on the chunk, ⛔ not a task for whichever change the queue happened to weigh — take it there, and say on this pull request that you did.

Taken there, as instructed. The open decision on this chunk is objectui#9251 (decision(components): Door 2 — the eager icons record carries 1,767 lucide modules, pm:dispatched) with objectui#9204 (perf(components), priority:p1) as the implementation row. ⛔ This seat filed nothing new — both already exist and are held by the seat working that chunk, so a third card would be a duplicate. ⛔ This PR neither raises the ceiling nor raises the allowance, and its diff (3 files, packages/core/src/utils/filter-tokens.ts + a pin + a changeset) touches no bundled component surface.

Why this does not block the queue, verified rather than assumed: performance-budget.yml's on: carries only push and pull_request — ⛔ no merge_group — and the workflow's own comment records that "this job cannot hold the merge queue the way Lint can". It is outside the 9-check required set, which is Lint · Type Check · Test (shard 1/4…4/4) · Build & E2E · Build Docs · Changeset Declaration.

⇒ All three conditions for queueing with a red non-required check are met: red by design on a pushed branch per the gate's own source text, no merge_group participation, and this comment naming the gate and the cause. ⛔ Nothing here was weakened to get there.


Generated by Claude Code

@os-tesla
os-tesla marked this pull request as ready for review September 13, 2026 13:50
@os-tesla
os-tesla added this pull request to the merge queue Sep 13, 2026
Merged via the queue into main with commit da45e6b Sep 13, 2026
37 of 38 checks passed
@os-tesla
os-tesla deleted the claude/issue-9129-filter-token-proto-guard branch September 13, 2026 14:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants