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
fix(plugin-dev,types): the production escape hatch stops being silent (#3900) (#4308)
* fix(plugin-dev,types): the production escape hatch stops being silent (#3900)
`DevPlugin.init()` refuses to run under `NODE_ENV=production` (ADR-0115 D6),
and `OS_ALLOW_DEV_PLUGIN` overrides that refusal. As shipped, the override
returned early with no output at all: the process ran the development assembly
while every log line and the ready banner read like an ordinary production
start.
That reproduces, one level up, the defect the guard exists to close. The
guard's own precedent says so — `OS_ALLOW_DEGRADED_TENANCY` boots degraded and
brands it everywhere an operator looks, and `OS_ALLOW_DRIVER_CONNECT_FAILURE`'s
contract is "logged loudly at startup". An escape hatch that says nothing
leaves the operator's only evidence of a degraded state in an env var they may
not have set themselves.
The override now brands itself twice: a warning at `init()`, emitted before any
assembly work so it survives an assembly step that later throws, and a repeat
on the ready banner. Only hazards live for that configuration are named — the
auth secret published inside the npm package (suppressed when the operator
passed their own `authSecret`) and the in-memory driver with persistence off
(suppressed when the `driver` toggle is off). The dev-admin seed is
deliberately absent: `plugin-auth`'s `maybeSeedDevAdmin` is hard-gated to
`NODE_ENV === 'development'` and cannot fire on this path, so warning about it
would spend the attention the real hazards need.
The flag also moves off a bare `process.env[…] === '1'` onto
`resolveAllowDevPlugin()` in `@objectstack/types`, joining the `OS_ALLOW_*`
family's shared truthy vocabulary next to `resolveAllowDegradedTenancy` /
`resolveAllowDriverConnectFailure`. The strict comparison failed CLOSED on
`OS_ALLOW_DEV_PLUGIN=true` — safe, but it reads to an operator as a broken
flag.
No change to the refusal path, which this issue re-verified end to end:
`kernel.use()` only registers, `initPluginWithTimeout` does not catch,
`bootstrap()` rethrows, and `os serve`'s outer handler prints the message and
exits 1. The throw is genuinely fatal here, so it needs none of the
`process.exit(1)` the tenancy guard required for sitting inside a broad catch.
Verified against a real `LiteKernel` boot in all three states: refuses without
the hatch, boots branded (init + banner) with it, and stays silent on an
ordinary dev boot.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TjTEKP96798WKamk2VCkAp
* docs(plugins): the packages entry names the branded override, not just the hatch (#3900)
The docs-drift check flagged `content/docs/plugins/packages.mdx` as
referencing the changed code. Its plugin-dev line described the guard's
refusal and the escape hatch's existence, which is still true, but read as
"the hatch turns the guard off" — the half this change replaced. It now says
the override brands itself.
`@objectstack/types`'s entry needed nothing: `resolveAllowDevPlugin` falls
under the "degraded-boot helpers" it already lists. `releases/v17.mdx`'s only
plugin-dev mention is the retired `DevPluginConfigSchema` cluster, unrelated.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TjTEKP96798WKamk2VCkAp
---------
Co-authored-by: Claude <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: content/docs/plugins/packages.mdx
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -347,7 +347,7 @@ All services implement contracts from `@objectstack/spec/contracts` and are kern
347
347
348
348
**Development Assembly Plugin** — one plugin that wires the real platform stack for zero-config local development.
349
349
350
-
-**Features**: Auto-assembles ObjectQL + in-memory driver + auth + security + Hono server + REST + dispatcher + app metadata, plus optional real services when installed (storage, realtime, i18n); registers no stubs — a slot no plugin fills stays empty, as in production (ADR-0115); refuses to boot with `NODE_ENV=production` (`OS_ALLOW_DEV_PLUGIN` escape hatch)
350
+
-**Features**: Auto-assembles ObjectQL + in-memory driver + auth + security + Hono server + REST + dispatcher + app metadata, plus optional real services when installed (storage, realtime, i18n); registers no stubs — a slot no plugin fills stays empty, as in production (ADR-0115); refuses to boot with `NODE_ENV=production` (`OS_ALLOW_DEV_PLUGIN` escape hatch, which brands the override in the boot log and on the ready banner instead of overriding silently)
351
351
-**When to use**: Zero-config local development and playgrounds
Copy file name to clipboardExpand all lines: docs/adr/0115-plugin-dev-assembly-not-stub-table.md
+2Lines changed: 2 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -90,6 +90,8 @@ Precision, for the record: the `auth` stub is **dead code on the real identity p
90
90
91
91
**D6 — plugin-dev grows a production guard, in the same change as Tier A.**`DevPlugin.init` throws when `NODE_ENV === 'production'`, escape hatch `OS_ALLOW_DEV_PLUGIN=1` (Prime Directive #9`OS_ALLOW_*` shape — deliberately scary; the shipped name, landed by #4126's subset). Deleting the fakes removes the security-semantics hazard; the guard covers what deletion cannot: the plugin still assembles a stack around a well-known default auth secret and a seeded dev admin, which no production deployment should get by accident. It does not ship as a separate earlier fix because Tier A itself waits for nothing (resolves open question 3 — the "maybe it shouldn't wait" concern was about the security stubs, and D2 removes them rather than gating them).
92
92
93
+
*Amendment (2026-07-31, [#3900](https://github.com/objectstack-ai/objectstack/issues/3900)) — **the hatch brands, and shares the family's vocabulary.*** As first shipped by #4126 the guard was complete in its refusal and silent in its permission: `OS_ALLOW_DEV_PLUGIN` returned early with no output, so a process that took the hatch ran the development assembly while every log line and banner read like an ordinary production start. That reproduces one level up the exact defect the guard exists to close — the operator's only evidence of a degraded state is the env var they may not have set themselves — and it is the one thing the flag's own precedent forbids: `OS_ALLOW_DEGRADED_TENANCY` boots degraded *and brands it everywhere an operator looks* (`serve.ts`), and `resolveAllowDriverConnectFailure`'s contract is "logged loudly at startup". The override now warns at `init()` (before any assembly work, so it survives a later throw) and again on the ready banner, naming only the hazards live for that configuration: the auth secret published inside the npm package (suppressed when the operator passed their own), and the in-memory driver with persistence off. The dev-admin seed is deliberately excluded — `plugin-auth`'s `maybeSeedDevAdmin` is hard-gated to `NODE_ENV === 'development'` and cannot fire on this path, and a warning about a non-event spends the attention the real ones need. The flag also moves from a bare `process.env[…] === '1'` to `resolveAllowDevPlugin()` in `@objectstack/types`, joining the `OS_ALLOW_*` family's truthy vocabulary (`1`/`true`/`on`/`yes`): the strict comparison failed CLOSED on `OS_ALLOW_DEV_PLUGIN=true`, which is safe but reads to an operator as a broken flag. #3900 also re-verified the refusal path itself — `kernel.use()` only registers, `initPluginWithTimeout` does not catch, `bootstrap()` rethrows, and `os serve`'s outer handler exits `1` — so the `throw` is genuinely fatal here and needs none of the `process.exit(1)` that the tenancy guard required for sitting inside serve's broad `catch`.
94
+
93
95
**D7 — The descriptions converge on what the plugin is.**`content/docs/plugins/packages.mdx` and the plugin README/class doc describe an assembly plugin ("auto-wires ObjectQL + driver-memory + auth + security + HTTP server + REST + dispatcher + app metadata for local development"); the "Stub services" section and the "simulating all 17+ kernel services" claim are removed. Docs describing the retired design are ADR-0078's silent lie in prose form.
Copy file name to clipboardExpand all lines: packages/plugins/plugin-dev/README.md
+17-1Lines changed: 17 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -81,7 +81,23 @@ To use a capability locally, install its real service — e.g. `@objectstack/ser
81
81
82
82
## Production guard
83
83
84
-
`init()` throws when `NODE_ENV === 'production'`: the assembly is built around a well-known default auth secret and a seeded dev admin. If you really mean it, set `OS_ALLOW_DEV_PLUGIN=1`.
84
+
`init()` throws when `NODE_ENV === 'production'`: the assembly is built around a well-known default auth secret and a seeded dev admin. Nothing swallows the throw on a real boot path — `os serve` prints the message and exits `1`.
85
+
86
+
If you really mean it (a staging box that pins `NODE_ENV=production`, a smoke test), set `OS_ALLOW_DEV_PLUGIN` to a truthy value (`1` / `true` / `on` / `yes`).
87
+
88
+
Taking that hatch is never silent. The boot log names the hazards that are actually live for your configuration, and the ready banner repeats the brand, so a process running the dev assembly cannot look like an ordinary production start:
89
+
90
+
```
91
+
⚠ DEV ASSEMBLY UNDER NODE_ENV=production (OS_ALLOW_DEV_PLUGIN is set) — the boot guard was
92
+
explicitly overridden. This process is running the DEVELOPMENT assembly, which is not
93
+
hardened for production traffic (ADR-0115 D6).
94
+
• Auth secret is the default published inside @objectstack/plugin-dev. It is public, so
95
+
anyone can mint a session this stack accepts. Pass `authSecret` explicitly.
96
+
• Data goes to the in-memory driver with persistence disabled — every record is lost
97
+
when this process exits.
98
+
```
99
+
100
+
The dev-admin seed is deliberately *not* on that list: `plugin-auth`'s seeding is hard-gated to `NODE_ENV === 'development'`, so it cannot fire on this path.
0 commit comments