Skip to content

Commit 5e7cf63

Browse files
feat(cli)!: kind:'react' pages are off unless the deployment turns them on (#25789)
<!-- claude-projects-attribution: {"github_login":"os-tesla"} --> _Requested by **Tesla** · [project thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6JaoBY54KcuzDWcTYXW18gG3bJ)_ Clause-②: no Metadata-protocol: none ## What changes The `kind:'react'` page tier, whose page source runs in the Console with no sandbox, is now **off unless the deployment turns it on**. ADR-0081 is amended to match. - **The switch.** `packages/cli/src/utils/console.ts` reads `readReactPagesTier()` once per Console document. It is on for `OS_PAGE_REACT_ENABLED=true` (also `1`, `on`, `yes`, `enabled`) and off when the variable is unset or `false`, `0`, `off`, `no`, `disabled`. Any other value leaves it off and logs one warning naming the variable and the accepted values. The earlier name, `OS_PAGE_REACT`, is still read through `readEnvWithDeprecation` while the new one is unset, with its one-time deprecation warning. The new name follows the `OS_{DOMAIN}_{FEATURE}_ENABLED` rule for an opt-in flag (AGENTS.md Prime Directive 9). - **What the document carries.** The server now writes the Console's capability global in both directions: the enable list (`__OBJECTUI_CAPABILITIES__`) when on, the disable list when off. So the served Console follows the server's setting whichever default its build carries. That makes this PR safe to land before the objectui half, which flips the renderer's own default. - **The content-security-policy.** `'unsafe-eval'` stays tied to the same per-document read (unchanged logic from the nonce-CSP change), so by default the Console document refuses `eval`. - **app-showcase opts in.** `examples/app-showcase/.env` sets `OS_PAGE_REACT_ENABLED=true`. `os dev`, `os serve` and `os start` load that file from the project root (dotenv-flow), so `pnpm dev`, the showcase smoke and `os start` in that example keep running its three react pages. - **Docs and contract text.** `content/docs/ui/react-pages.mdx`, `content/docs/ui/pages.mdx`, `content/docs/protocol/objectui/layout-dsl.mdx`, the `PageSchema.kind` description in `packages/spec` (and the regenerated `content/docs/references/ui/page.mdx`), the published skill rule `skills/objectstack-ui/rules/pages.md`, and the platform-checklist item `studio-authoring` for the react tier (revision 4) all state the new default and switch. - **ADR-0081 amendment.** Status line and an amendment block: decision 3 / TL;DR 4 are superseded on the default only; the tier, the gate and its host-only control stand. ## Landing order 1. This PR (server side). With it merged, a deployment's setting decides, against the Console build objectstack currently pins and against the next one. 2. objectui: the `react-pages` default flips to off in `@object-ui/core` and the disabled notice names `OS_PAGE_REACT_ENABLED=true` (separate draft PR, linked from the thread). ## Known side effect until a follow-up lands With the tier off, the Console's schema library probes for `eval` once at boot and falls back without it. Under this policy that probe is refused, so a browser that reports content-security-policy violations logs one `script-src` eval refusal per Console load. Nothing on the page is affected. Before this PR that happened only on deployments that set `OS_PAGE_REACT=off`; after it, on every deployment that leaves the tier off. objectui#13511 removes the probe (one line in the Console boot). The changeset says this to operators. ## Tests Run on head `e6df92d2`: - `pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2 src/utils/console` → `Test Files 7 passed (7)`, `Tests 142 passed (142)`. - `console.content-security-policy.test.ts` (real plugin on a real Hono app): default document refuses `'unsafe-eval'` and carries the disable global under the nonce; `OS_PAGE_REACT_ENABLED=true` allows it and carries the enable global; legacy `OS_PAGE_REACT=on` / `off` in both directions; the new name wins over the legacy one; an unrecognized value stays off and warns exactly once over two documents. - `console.branded-head.test.ts`: the served-unchanged shell now includes the capability script (off by default). - `pnpm --filter @objectstack/spec check:generated`: `check:docs` regenerated (`content/docs/references/ui/page.mdx`). `check:liveness` is red on the base commit too, for `translation/flows.screens` (a citation this PR does not touch). Derived gates (`node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, 130 families, `--ran` reconciles all 130): - 126 exit 0, five of them after building the prerequisite they named (cli and client-react dists, the pinned teardown control commit): `check-plugin-teardown-shape --self-test`, `check:skill-examples`, `check:i18n`, `check:i18n-walk-parity`, `check:type-check-debt`. - `check:liveness` (and `check:generated`, which reports only that row) exit 1 on `translation/flows.screens`. The same command on the base commit `f0cf33833` exits 1 with the same row. - `check:i18n-coverage` and `check:dual-build-cjs-loads` NOT MEASURED locally: both need a full workspace build. CI runs them. Ablation: with `readReactPagesTier` returning on for an unset value, four cases in `console.content-security-policy.test.ts` go red; restored, `git diff HEAD` is empty. Full dogfood (title carries `!`): in progress on `e6df92d2`; the result goes in a comment. ## Governed surfaces This diff touches `docs/adr/0081-trusted-react-page-tier.md` and `skills/objectstack-ui/rules/pages.md`, so it is Tier H and stays in draft for an authorized approval. ADR-0081's decision named the old default, so the default does not move without the amendment (Prime Directive 13). `skills/**` readings: `skills/objectstack-ui/rules/pages.md` 428 → 428 lines; all `skills/**/SKILL.md` together 4383 → 4383 lines (unchanged; the edit rewords three lines in place). ## 维护者速读(草稿) - **改了什么**:`kind:'react'` 页面(页面源码在 Console 里直接执行、无沙箱)从「默认开」改为「默认关」。部署方设 `OS_PAGE_REACT_ENABLED=true` 才开;旧变量 `OS_PAGE_REACT=on/off` 仍可用(带一次弃用提示)。服务端把开/关两个方向都写进 Console 文档,CSP 的 `'unsafe-eval'` 跟随同一开关。app-showcase 用自己的 `.env` 显式开启。ADR-0081 加修订说明。 - **为什么改**:作者是否可信是每个部署自己的判断,平台默认不应替所有部署做这个判断;默认关、显式开,把这个决定交给运维方。 - **风险与代价(含回滚)**:已在用 react 页面的部署升级后,这些页面显示「已禁用」提示,直到设置 `OS_PAGE_REACT_ENABLED=true`(changeset 写明迁移)。默认关时,每次加载 Console 会多一条 zod 的 eval 探测 CSP 违规上报,不影响页面,objectui#13511 消除。回滚:revert 本 PR,或部署侧设该变量。 - **席位意见**: - **你要做的**:审阅并批准本 PR(受管面:ADR 与 skills),然后看 objectui 那半的 draft PR。 --- _Generated by [Claude Code](https://claude.ai/code/session_01X4d6XVfcfxL17bMpptytcc)_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01X4d6XVfcfxL17bMpptytcc --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent f2a870e commit 5e7cf63

13 files changed

Lines changed: 231 additions & 74 deletions

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@objectstack/cli': patch
3+
'@objectstack/spec': patch
4+
---
5+
6+
feat: `kind:'react'` pages are off unless the deployment turns them on
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
**BREAKING**: the `kind:'react'` page tier, which runs page source in the Console, now defaults to **off** (ADR-0081, amended). A deployment that serves react pages turns the tier on with `OS_PAGE_REACT_ENABLED=true`, in the environment or the project's `.env` file. With it off, each react page shows a notice naming that switch instead of running, and the Console document's content-security-policy no longer allows `'unsafe-eval'`. `kind:'html'` pages are unaffected.
12+
13+
To keep react pages running after the upgrade, set `OS_PAGE_REACT_ENABLED=true` on the deployment. Stored pages need no change.
14+
15+
<!-- adr-0087: not-required (no-migration-prescription) an operator environment default moved; no spec key, export or stored row changed, so there is nothing for objectstack migrate meta to rewrite -->
16+
17+
The earlier name is still read while the new one is unset: `OS_PAGE_REACT=on` turns the tier on and `OS_PAGE_REACT=off` keeps it off, each with a one-time deprecation warning. `true`, `1`, `on`, `yes` and `enabled` turn it on; unset, `false`, `0`, `off`, `no` and `disabled` leave it off; any other value leaves it off and is reported once in the server log.
18+
19+
The server now writes the Console's capability global in both directions, so a Console build with either default follows the server's setting. `examples/app-showcase` opts in through its `.env`, so its three react pages keep running. The `PageSchema.kind` description in `@objectstack/spec` now states the new default.
20+
21+
Operators who collect content-security-policy reports may see one `script-src` eval refusal per Console load while the tier is off: the Console's schema library probes for `eval` once at boot and falls back without it. Nothing on the page is affected, and the probe is removed on the objectui side.

‎content/docs/protocol/objectui/layout-dsl.mdx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@ write the page body as a string. Pick by what the page needs:
3939
For `html` / `react`, the page body is the `source` string (it is the
4040
source-of-truth; `regions` is ignored). `html` is **safe by construction** (it
4141
is parsed into the same SDUI tree this document describes, never executed).
42-
`react` executes author code, so it is gated by a host capability that is **ON
43-
by default** and disabled per-deployment with the `OS_PAGE_REACT=off` env
44-
variable. Both tiers can embed the platform's real data components
42+
`react` executes author code, so it is gated by a host capability that is **OFF
43+
by default** and turned on per deployment with the `OS_PAGE_REACT_ENABLED=true`
44+
env variable. Both tiers can embed the platform's real data components
4545
(`<ObjectTable>`, `<ObjectForm>`, charts, metrics).
4646

4747
> **Learn more:** [ADR-0080](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0080-ai-authored-ui-jsx-source.md)

‎content/docs/references/ui/page.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ View filter rule
215215
| **interfaceConfig** | `{ source?: string; columns?: string[] \| object[]; sort?: object[]; filterBy?: object[]; … }` | optional | Interface-level page configuration (for Airtable-style interface pages) |
216216
| **aria** | `{ ariaLabel?: string \| Record<string, string>; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes |
217217
| **print** | `{ paperSize?: Enum<'A4' \| 'A5' \| 'Letter' \| 'Legal'>; orientation?: Enum<'portrait' \| 'landscape'>; margins?: object; repeatHeader?: boolean; … }` | optional | Print declaration — makes this page a print page, a document authored in the page's own blocks: paper size and orientation, margins in millimetres, the running header and footer (the page's own `header` / `footer` regions), page numbers and page-break hints, each mapped to print CSS. A print page is a `kind: 'full'` page of `type: 'record'`, `'home'` or `'app'` with its blocks in `regions`, and every block in it must come from the printable block subset: containers that draw every child (`page:section`, `page:card`, `page:footer`), field blocks (`record:details`, `record:highlights`), the child-record table `record:line_items`, `element:text`, `element:image`, `element:divider`, `element:definition-list`, `element:repeater`, `element:number` and `object-metric`; `os validate` / `os build` / `os lint` and the metadata save door refuse any other block inside it. Applied by the console's browser print rendering, which prints the page through the browser's own print dialog (where it can be saved as PDF); the server-side PDF renderer is to follow. A list view's print button is `allowPrinting`, not this. |
218-
| **kind** | `Enum<'full' \| 'slotted' \| 'html' \| 'react'>` | optional (default: `"full"`) | Page override mode. full \| slotted = structured authoring; html = author-written constrained JSX compiled (parsed, never executed) to the tree (ADR-0080; ADR-0081 named it html), styled by the registered components' structured props plus a JSON `style` object with hsl(var(--token)) theme colors; react = real-React source executed at render by the runtime (ADR-0081), styled by inline `style` with the same token colors; it runs author JS, so it is gated by a host capability that defaults ON and is disabled server-side via the OS_PAGE_REACT=off env toggle. Do not author Tailwind classes in page source in either tier: `source` is runtime metadata the build-time Tailwind never scans, so utility classNames silently produce no CSS (ADR-0065; ADR-0080). |
218+
| **kind** | `Enum<'full' \| 'slotted' \| 'html' \| 'react'>` | optional (default: `"full"`) | Page override mode. full \| slotted = structured authoring; html = author-written constrained JSX compiled (parsed, never executed) to the tree (ADR-0080; ADR-0081 named it html), styled by the registered components' structured props plus a JSON `style` object with hsl(var(--token)) theme colors; react = real-React source executed at render by the runtime (ADR-0081), styled by inline `style` with the same token colors; it runs author JS, so it is gated by a host capability that defaults OFF and is turned on server-side via the OS_PAGE_REACT_ENABLED=true env toggle. Do not author Tailwind classes in page source in either tier: `source` is runtime metadata the build-time Tailwind never scans, so utility classNames silently produce no CSS (ADR-0065; ADR-0080). |
219219
| **slots** | `{ header?: object \| object[]; actions?: object \| object[]; alerts?: object \| object[]; highlights?: object \| object[]; … }` | optional | Slot override map for slotted pages |
220220
| **source** | `string` | optional | Page source text. For kind==='html' it is constrained JSX compiled to the tree by @objectstack/sdui-parser at save time (parse, never execute), styled by the registered components' structured props plus a JSON `style` object with hsl(var(--token)) theme colors. For kind==='react' it is real React/JSX executed at render by @object-ui/react-runtime (trusted tier), styled by inline `style` with the same token colors. Do not author Tailwind classes in page source in either tier: `source` is runtime metadata the build-time Tailwind never scans, so utility classNames silently produce no CSS (ADR-0065; ADR-0080). Authoritative over `regions` in both. |
221221
| **requires** | `string[]` | optional | Plugin namespaces the page's source uses, derived from the source at save — omit it. The key exists only on a kind==='html' page, the kind whose source is compiled at save; on a 'react', 'full' or 'slotted' page — and a page that omits kind, which is 'full' — it is refused at parse. On a server that has the deployment's SDUI component manifest, saving an html page compiles its source and stores the namespaces it uses here; a written list that disagrees with the source is refused (422 INVALID_METADATA, page-requires-disagrees-with-source) — on a draft save it is kept until the draft's publish, which refuses it. At load, a stored page whose list names a plugin no component in that manifest carries is reported, page and plugin named, and is still served. A server with no manifest checks neither and says so once at boot. |

‎content/docs/ui/pages.mdx‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,9 @@ is rejected by the schema rather than rendering empty.
108108
`'jsx'` keep loading as `html` pages.
109109

110110
Because `kind: 'react'` executes author JavaScript, it is gated by a host capability that
111-
defaults **ON** and is disabled server-side per deployment with `OS_PAGE_REACT=off`. The
112-
`html` tier is unaffected by that switch — it is never executed.
111+
defaults **OFF** and is turned on server-side per deployment with
112+
`OS_PAGE_REACT_ENABLED=true`. The `html` tier is unaffected by that switch — it is never
113+
executed.
113114

114115
<Callout type="warn">
115116
Neither source tier is styled with Tailwind utility classes. A page's `source` is

‎content/docs/ui/react-pages.mdx‎

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -58,20 +58,27 @@ The two tiers also differ in what they give you for layout, in opposite directio
5858

5959
## The security gate
6060

61-
Because the react tier runs author JavaScript, it is gated by a host capability named
62-
`react-pages`, which defaults **ON** — the platform's assumption is that page authors
63-
are reviewed and draft-gated.
61+
Because the react tier runs author JavaScript in every viewer's session, with no sandbox,
62+
it is gated by a host capability named `react-pages`, which defaults **OFF**. The tier is
63+
for deployments whose page authors are trusted and reviewed.
6464

65-
A deployment that does not trust its page authors turns the tier off **server-side**:
65+
A deployment that trusts its page authors turns the tier on **server-side**:
6666

6767
```bash
68-
OS_PAGE_REACT=off objectstack start
68+
OS_PAGE_REACT_ENABLED=true objectstack start
6969
```
7070

71-
The server then injects the console's capability-disable flag, and every `kind:'react'`
72-
page renders an explanatory notice instead of executing. `off`, `0`, `false`, `no` and
73-
`disabled` are all accepted spellings. `kind:'html'` pages are unaffected — they were
74-
never executed in the first place, which is the whole point of the split.
71+
or with the same line in the project's `.env` file. `true`, `1`, `on`, `yes` and `enabled`
72+
turn it on; unset, `false`, `0`, `off`, `no` and `disabled` leave it off. Any other value
73+
leaves it off and is reported once in the server log. The earlier name `OS_PAGE_REACT`
74+
(`on` / `off`) is still read when `OS_PAGE_REACT_ENABLED` is unset, with a deprecation
75+
warning.
76+
77+
With the tier off, every `kind:'react'` page renders a notice that names the switch
78+
instead of executing, and the Console's content-security-policy refuses `eval`. With it
79+
on, the server tells the Console to run react pages and the policy allows the `Function`
80+
call the runtime compiles them with. `kind:'html'` pages are unaffected either way — they
81+
are never executed, which is the whole point of the split.
7582

7683
## What is in scope
7784

‎docs/adr/0081-trusted-react-page-tier.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ADR-0081: A trusted `kind:'react'` page tier — real React executed in the main tree, gated by a host capability; and renaming `kind:'jsx'` → `kind:'html'`
22

3-
**Status**: Accepted (2026-06-30) — framework scope implemented (`html`/`react` page kinds + `jsx` alias, lint split `validate-jsx-pages`/`validate-react-pages`, `OS_PAGE_REACT=off` server toggle); the `@object-ui/react-runtime` executor + `CAP_REACT_PAGES` gate live in objectui.
3+
**Status**: Accepted (2026-06-30) — framework scope implemented (`html`/`react` page kinds + `jsx` alias, lint split `validate-jsx-pages`/`validate-react-pages`, `OS_PAGE_REACT=off` server toggle); the `@object-ui/react-runtime` executor + `CAP_REACT_PAGES` gate live in objectui. **Amended** (2026-10-25 — the `react` tier defaults **OFF**; a deployment opts in with `OS_PAGE_REACT_ENABLED=true`. See "Amendment (2026-10-25): default off, opt in per deployment".)
44
**Deciders**: ObjectStack Protocol Architects
55
**Builds on**: [ADR-0080](./0080-ai-authored-ui-jsx-source.md) (AI authors a *constrained* JSX text, parsed-never-executed, into the SDUI tree; the component registry is the contract; capability ≠ contract), [ADR-0033](./0033-ai-assisted-metadata-authoring.md) (AI writes metadata via draft-gated `saveMetaItem` — the human-review boundary), [ADR-0077](./0077-authoring-surface-boundary-hook-flow-validation.md) (route by intent; verifiability tier = friction tier; **code is the flagged escape hatch**), [ADR-0078](./0078-no-silently-inert-metadata.md) (no silently-inert metadata).
66
**Consumers**: `@objectstack/spec` (`packages/spec/src/ui/page.zod.ts` — `kind` gains `'html'` and `'react'`; `'jsx'` kept as a deprecated alias), `@objectstack/lint` (`validate-jsx-pages` — lints `html`/`jsx`, **skips** `react`), `../objectui` `@object-ui/react-runtime` (**new** — the trusted runtime-React executor) + `@object-ui/core` (the host capability gate) + `@object-ui/components` (PageRenderer routing + the full HTML tag set).
@@ -13,6 +13,8 @@
1313

1414
> **Amendment (2026-06-30 — ADR-0065 styling correction).** The "HTML + **Tailwind**" framing for page `source` is **superseded on styling**. A page's `source` is *runtime metadata*, so the console's build-time Tailwind never scans it — authored utility `className`s silently produce no CSS (the exact failure ADR-0065 was written to prevent; the Task Desk modal's `bg-black/50` backdrop rendered transparent). The tiers themselves stand; only the styling **primitive** changes: `kind:'html'` styles via the registered components' structured props + a JSON `style` object; `kind:'react'` styles via inline `style={{}}` with `hsl(var(--token))` theme colors and renders drawer/modal overlays through `<ObjectForm formType="drawer"|"modal">` (never a hand-rolled `fixed inset-0`). **Do not author Tailwind classes in page source.** See [ADR-0065](./0065-sdui-styling-model.md).
1515
16+
> **Amendment (2026-10-25): default off, opt in per deployment.** Decision 3 and TL;DR 4 are **superseded on the default**; the tier, its gate and its host-only control stand. `CAP_REACT_PAGES` now defaults **OFF** in `@object-ui/core`, and the ObjectStack console serving (`cli/utils/console.ts`, `readReactPagesTier`) turns it on only when the operator sets `OS_PAGE_REACT_ENABLED=true` (`OS_PAGE_REACT=on`, the earlier name, is still read through `readEnvWithDeprecation`). The server writes the capability global in **both** directions — the enable list when on, the disable list when off — so its answer holds against a Console build with either default. The Console document's content-security-policy keeps `'unsafe-eval'` tied to the same per-request read. Off, a react page renders the named "React pages are disabled on this deployment" notice. **Why the default moved:** "trust + review" names a property of one deployment's page authors, which only that deployment's operator can vouch for, so the platform default no longer vouches for it on every deployment; the operator makes the trust decision explicitly. Reference deployments that ship react pages opt in in their own configuration (`examples/app-showcase/.env`).
17+
1618
## TL;DR
1719

1820
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'`.

0 commit comments

Comments
 (0)