You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
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/maina39b02a6 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.
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.
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.
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/runtime17.2.0, Node v22.22.2, and re-verified here onorigin/maina39b02a6by reading the allowlist:quickjs-emscripten0.32.0, the variantAppPluginwires throughQuickJSScriptRunner) has noIntl—typeof Intlisundefinedthere, whileDateandJSONexist.HookBodyCapabilitygrants nothing that would add it.packages/cli/src/utils/detect-free-identifiers.ts, theGLOBALSset starting at line 39, allowlistsIntlnext toMath,JSON,Date,Reflect,Proxy, under the comment "Web-ish that the sandbox / Node commonly provide". A handler body that callsIntl.DateTimeFormattherefore has no free identifier,extractHookBodylowers it, and thehook-body-loweringlint rule (Hook-body lowering fails OPEN:tryExtractBodyswallowsextractHookBody'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.pnpm validate,typecheck,testandbuildare all green (in-process tests run the raw function in Node, whereIntlexists); in production the lowered body throwsReferenceError: Intl is not defined, and with theonError: '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("preferbody") steers authors into exactly this path.Ruled direction (triage)
Split
GLOBALSinto 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:Intlis 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 andos build's existing warn-and-bundle behaviour handles the rest. The membership of the sandbox set is measured against the shipped QuickJS build (atypeof Xprobe 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 whetheros buildfails on the lowering class (#13838), or giving the sandboxIntl(a capability expansion).Grade:
pm:queue·priority:p2·domain:cli(landing file is inpackages/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
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— whetheros buildshould 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).