Skip to content

objectstack build lowers an inline hook handler that references Intl into the QuickJS body — detect-free-identifiers allowlists host globals the sandbox does not provide, so the handler passes validate/typecheck/test/build and throws ReferenceError in production #14301

Description

@huangyiirene

Filed by the triage seat (session session_019kDRpB7D2XzVzkaLp57T5D, R+89) as the defect half split out of #14168. #14168 is a feature question (a declarative value-domain slot on a field) and sits in the decision inbox; this half does not depend on that decision and is graded directly.

What was measured

By #14168's reporter on @objectstack/cli / @objectstack/runtime 17.2.0, Node v22.22.2, and re-verified here on origin/main a39b02a6 by reading the allowlist:

  • The QuickJS sandbox the runtime evaluates lowered hook bodies in (quickjs-emscripten 0.32.0, the variant AppPlugin wires through QuickJSScriptRunner) has no Intl — typeof Intl is undefined there, while Date and JSON exist. HookBodyCapability grants nothing that would add it.
  • packages/cli/src/utils/detect-free-identifiers.ts, the GLOBALS set starting at line 39, allowlists Intl next to Math, JSON, Date, Reflect, Proxy, under the comment "Web-ish that the sandbox / Node commonly provide". A handler body that calls Intl.DateTimeFormat therefore has no free identifier, extractHookBody lowers it, and the hook-body-lowering lint rule (Hook-body lowering fails OPEN: tryExtractBody swallows extractHookBody's refusal into a warnings array and bundles the closure — make it loud (ask 1; the constant-surface half is split out) #13651) has nothing to report — that rule fires only on a refused lowering.
  • Consequence, measured in a real artifact: pnpm validate, typecheck, test and build are all green (in-process tests run the raw function in Node, where Intl exists); in production the lowered body throws ReferenceError: Intl is not defined, and with the onError: 'abort' a validation-shaped hook must declare, every write to that object is refused.

Why this is a defect, not a documentation gap

The allowlist is the platform's own statement of what a lowered body may reference. Where it names a global the sandbox does not provide, the build lowers code that cannot run and every local gate agrees it is fine — the silent-until-production shape. The deprecation text on Hook.handler ("prefer body") steers authors into exactly this path.

Ruled direction (triage)

Split GLOBALS into the set the sandbox actually provides and the set only Node provides. A free reference to a Node-only global becomes a lowering refusal with a named reason (for example: Intl is not available in the hook sandbox — keep the check in a string handler ref, or move it to a validation rule), so the existing #13651 lint rule reports it and os build's existing warn-and-bundle behaviour handles the rest. The membership of the sandbox set is measured against the shipped QuickJS build (a typeof X probe run inside the same sandbox the runtime uses), never written from memory, and pinned by a test that reads that probe. ⛔ Not in scope: changing whether os build fails on the lowering class (#13838), or giving the sandbox Intl (a capability expansion).

Grade: pm:queue · priority:p2 · domain:cli (landing file is in packages/cli) · type Bug. Not clause ②: the lowering already refuses handlers with free identifiers; this corrects which identifiers count as free, against the runtime's measured truth.

Re-check

git grep -n "'Intl'" origin/main -- packages/cli/src/utils/detect-free-identifiers.ts

Positive control: 'JSON' in the same list.

Dedup

search_issues (two queries, positive controls returned #13651 / #13838): #13651 (closed — lowering failed open, made loud), #13838 (pm:on-hold — whether os build should fail by default on the lowering class), #14168 (the feature half), #1867 (closed — sandbox crashes on nested writes). None names the allowlist/sandbox mismatch.

Refs: #14168 · #13651 · #13838 · objectstack-ai/duly#24 (the application that hit it).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions