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/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 Intltypeof 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).

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
     blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
    }
    } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
    })();
    (function(){
    try {
    var __m = "github.com";
    var __re = new RegExp('^' + "github\\.com" + '
    
    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/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 Intltypeof 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).

    Metadata

    Metadata

    Assignees

    Labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
      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/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 Intltypeof 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).

      Metadata

      Metadata

      Assignees

      Labels

      Type

      Projects

      No projects

        Milestone

        No milestone

        Relationships

        None yet

        Development

        No branches or pull requests

        Issue actions

        , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
        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/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 Intltypeof 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).

        Metadata

        Metadata

        Assignees

        Labels

        Type

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
          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/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 Intltypeof 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).

          Metadata

          Metadata

          Assignees

          Labels

          Type

          Projects

          No projects

            Milestone

            No milestone

            Relationships

            None yet

            Development

            No branches or pull requests

            Issue actions

            , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
            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/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 Intltypeof 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).

            Metadata

            Metadata

            Assignees

            Labels

            Type

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              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/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 Intltypeof 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).

              Metadata

              Metadata

              Assignees

              Labels

              Type

              Projects

              No projects

                Milestone

                No milestone

                Relationships

                None yet

                Development

                No branches or pull requests

                Issue actions

                , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                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/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 Intltypeof 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).

                Metadata

                Metadata

                Assignees

                Labels

                Type

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions