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
feat: kind:'react' defaults ON, disable server-side via OS_DISABLE_REACT_PAGES
Per platform-author decision the react tier is no longer enterprise-gated/
default-off. It runs author JS, so it stays behind a host capability — but that
capability defaults ON (trusted, reviewed, draft-gated authors). A deployment
that does not trust its authors disables it with one env var:
- cli: console serving injects window.__OBJECTUI_CAPABILITIES_DISABLED__ =
['react-pages'] into the served HTML when OS_DISABLE_REACT_PAGES is set
(read per request, no rebuild).
- spec: kind describe reframed (default-on, OS_DISABLE_REACT_PAGES opt-out).
- ADR-0081 + showcase comment reframed away from enterprise-only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: docs/adr/0081-trusted-react-page-tier.md
+8-7Lines changed: 8 additions & 7 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -7,7 +7,7 @@
7
7
8
8
**Premise**: ADR-0080 shipped `kind:'jsx'` — a *constrained* JSX dialect parsed (never executed) into the SDUI tree. In practice that name oversold it: authors expected "JSX" to mean *any* HTML and *any* JavaScript (`useState`, `.map`, `onClick`), and the constrained, no-execution model categorically cannot do that. Two distinct things were conflated under one name. This ADR splits them: it makes the safe constrained tier **live up to a narrower, honest name (`html`)**, and adds a separate, explicitly-trusted **`react`** tier for the full-power case — available only where the operator accepts running author code.
9
9
10
-
> **Trigger**: testing the merged `kind:'jsx'` feature, the author found it "名不副实" (misnamed) — basic HTML tags didn't render and scripts were impossible — and recognized that the real-React capability belongs behind a switch, enabled only in a trusted private/enterprise deployment where "running unsandboxed author code in the main React app" is an acceptable, deliberate trust decision.
10
+
> **Trigger**: testing the merged `kind:'jsx'` feature, the author found it "名不副实" (misnamed) — basic HTML tags didn't render and scripts were impossible — and recognized that the real-React capability belongs behind a switch. The platform trusts its (reviewed, draft-gated) page authors, so that switch defaults ON; a deployment that does not trust its authors turns it off server-side. "Running author code in the main React app" is an accepted, default trust decision, reversible per deployment — not an enterprise-only feature.
11
11
12
12
---
13
13
@@ -16,7 +16,7 @@
16
16
1.**[rename]`kind:'jsx'` → `kind:'html'`.** The constrained, parse-never-execute tier (ADR-0080) is renamed to match what it is: author-written **HTML + Tailwind** (expressed as constrained JSX) compiled to the SDUI tree. `'jsx'` stays as a **deprecated alias** (already-saved pages keep loading); all authored examples/docs move to `'html'`.
17
17
2.**[capability] The `html` tier now resolves the full safe native HTML tag set.** Previously only `div/span/table/code/label` + semantic sectioning tags were registered, so `<h1>/<p>/<a>/<ul>/<li>/<img>/<blockquote>…` failed as `unknown-component`. They are now registered as passthrough renderers — the tier is finally honest about "HTML".
18
18
3.**[new tier]`kind:'react'` executes real React.** Its `source` is real JS/JSX (hooks, `.map`, event handlers, expressions), transpiled (Sucrase) and evaluated **in the main React tree — no sandbox** — by the new `@object-ui/react-runtime`. This is `parse = execute`: the categorical opposite of the `html` tier.
19
-
4.**[security = trust, not sandbox]`react` is gated by a host capability, default OFF.**`CAP_REACT_PAGES` is flipped only by the *host process* (`enableCapability('react-pages')`), never by authored metadata. Open-source / multi-tenant builds ship it **off**; enterprise / private deployments — where authors are trusted and changes pass human review (draft-gating, ADR-0033) — opt in. A sandbox (iframe/worker) is the tool for *untrusted* execution; we deliberately choose **trust + review** instead, which keeps the author's code a first-class citizen of the app (shared React tree, real components, real data) at the cost of requiring the operator's explicit consent.
19
+
4.**[security = trust, not sandbox]`react` is gated by a host capability that defaults ON.**`CAP_REACT_PAGES` is controlled only by the *host*, never by authored metadata. It defaults ON because ObjectStack pages are authored by trusted authors and pass human review (draft-gating, ADR-0033). A deployment that does NOT trust its authors disables it **server-side** — the ObjectStack runtime injects the disable global when `OS_DISABLE_REACT_PAGES` is set (one env var, no rebuild). A sandbox (iframe/worker) is the tool for *untrusted* execution; we deliberately choose **trust + review** instead, which keeps the author's code a first-class citizen of the app (shared React tree, real components, real data). This is NOT an enterprise-only feature.
20
20
5.**[runtime] Vendored, not depended.**`@object-ui/react-runtime` inlines the ~150-LOC react-runner core (MIT) so we fully own the injected **scope/imports surface** (the actual capability ceiling) and can **lazy-load** it — the transpiler ships in a separate chunk fetched only when a `react` page renders *with the capability on*.
21
21
6.**[scope] Inject data blocks + React; leave layout to HTML.** The runtime scope exposes `React`, the curated **public data blocks** (`<ObjectTable>`, `<ObjectForm>`, charts, metrics — each a prop-driven wrapper over `SchemaRenderer`), a `<Block type=…/>` escape hatch, and the page `data`. Layout/structure is plain HTML + Tailwind (React's strength) — we do **not** bridge React children into schema-children renderers.
22
22
7.**[lint]`react` is not linted by the constrained parser.**`validate-jsx-pages` (ADR-0080's build gate) validates `html`/`jsx` only; running the constrained JSX parser over real React source would false-error on hooks and expressions. The `react` tier's safety boundary is the **capability + human review**, not a static gate.
@@ -31,7 +31,7 @@
31
31
| Processing |**parsed**, never executed → SDUI tree | transpiled + **executed** in the main React tree |
32
32
| JavaScript | none (static literals) | full |
33
33
| Safety model | safe by construction (no execution) |**trust** (capability + review); no sandbox |
34
-
| Where it runs | OSS default ON |enterprise/private only, capability default **OFF**|
34
+
| Where it runs | OSS default ON |default ON; disable server-side via `OS_DISABLE_REACT_PAGES`|
@@ -47,9 +47,10 @@ The dividing line is exactly ADR-0080 §2's "parse ≠ execute". `html` keeps th
47
47
### 2. Make `html` honest
48
48
Register the safe native HTML flow/inline set (`h1–h6, p, a, ul/ol/li, dl/dt/dd, blockquote, pre, strong/em/b/i/u/small/mark/sub/sup, figure/figcaption, img, hr, br, time, address, …`) as passthrough renderers that forward standard attributes and recurse children. Excluded: anything already registered, and anything that can execute or escape (`script/style/iframe/object/embed/link/meta/form/input`, plus `button` which is the component). Defense-in-depth even though this tier never executes JS: strip `on*` handlers and `dangerouslySetInnerHTML`, neutralize `javascript:`/`data:` URLs on `<a href>`. Unknown attributes are warnings (the page still renders), per the ADR-0080 validator.
49
49
50
-
### 3. Add `react` as a capability, not a default
51
-
-**Gate**: `@object-ui/core` gains `enableCapability`/`disableCapability`/`isCapabilityEnabled` + `CAP_REACT_PAGES`. Default-closed. Host-only (a global pre-seed `globalThis.__OBJECTUI_CAPABILITIES__` is also honored for HTML-bootstrap hosts). It is structurally impossible to enable from authored metadata.
52
-
-**Renderer**: PageRenderer routes `kind:'react'` to a renderer that — only when the capability is on — lazy-imports `@object-ui/react-runtime`, builds the scope, and renders via its error-boundaried `ReactRunner`. Capability off → a clear "this page requires the React-pages capability" notice, not a crash.
50
+
### 3. Add `react` behind a host capability that defaults ON
51
+
-**Gate**: `@object-ui/core` gains `enableCapability`/`disableCapability`/`isCapabilityEnabled` + `CAP_REACT_PAGES`. `react-pages` defaults ON. Host-only — it reads `globalThis.__OBJECTUI_CAPABILITIES_DISABLED__` (force off) and `__OBJECTUI_CAPABILITIES__` (force on); it is structurally impossible to flip from authored metadata.
52
+
-**Server opt-out**: the ObjectStack console serving (framework `cli/utils/console.ts`) injects the disable global into the served HTML when `OS_DISABLE_REACT_PAGES` is set — one env var, read per request, no rebuild. This is the single server-side switch a deployment uses to turn the tier off.
53
+
-**Renderer**: PageRenderer routes `kind:'react'` to a renderer that — when the capability is on — lazy-imports `@object-ui/react-runtime`, builds the scope, and renders via its error-boundaried `ReactRunner`. Capability off → a clear "React pages are disabled on this deployment" notice, not a crash.
53
54
54
55
### 4. Vendor the runtime; own the scope
55
56
`@object-ui/react-runtime` inlines react-runner: `transform` (Sucrase `jsx`+`typescript`+`imports`, production), `generateElement` (eval with injected scope; `createRequire` resolves `require()` only against an explicit `imports` map — a control point), and `ReactRunner` (a component with `getDerivedStateFromError`). The capability ceiling is precisely the injected scope/imports — owning that file is why we vendor rather than depend.
@@ -62,4 +63,4 @@ Register the safe native HTML flow/inline set (`h1–h6, p, a, ul/ol/li, dl/dt/d
62
63
-**Two safety stories, each clean.**`html` is safe by construction and stays the OSS/multi-tenant default. `react` is safe by *trust*, opt-in, and obviously so (you typed `enableCapability`).
63
64
-**No build/deploy for either.**`html` compiles at save; `react` transpiles at render (lazy chunk). Neither needs a bundler in the author loop.
64
65
-**AI fit.** AI writes both fluently. For `react`, correctness is bounded by the injected scope + human review (ADR-0033), not by hoping the model never writes a bad effect.
65
-
-**Cost.**`react` runs author code in the main tree: a thrown render is caught by the error boundary, but an author can still write a slow/incorrect page. That is the accepted price of the trusted tier and the reason it is off by default. Revisit if a future need arises for *untrusted* rich pages — that would be a *different* ADR (sandboxed execution), not this one.
66
+
-**Cost.**`react` runs author code in the main tree: a thrown render is caught by the error boundary, but an author can still write a slow/incorrect page. That is the accepted price of the trusted tier; a deployment that does not trust its authors flips `OS_DISABLE_REACT_PAGES`. Revisit if a future need arises for *untrusted* rich pages — that would be a *different* ADR (sandboxed execution), not this one.
0 commit comments