feat(runtime)!: the undeclared-action refusal has no opt-out — drop OS_ALLOW_UNDECLARED_ACTIONS (ADR-0110 D3 revised) - #3987
Merged
Conversation
…S_ALLOW_UNDECLARED_ACTIONS (ADR-0110 D3 revised) D3 as accepted refused an undeclared handler but shipped a migration valve that ran it anyway, slated for removal in 18. Removed before 17 ships. It contradicts the ruling it accompanies: a flag that executes an ungoverned, system-elevated handler IS the fail-open D3 closes, and ADR-0049's trichotomy has no 'enforced unless a flag says otherwise' state. And it had no observed users — a reconciliation sweep across platform packages, every example and every plugin found the only engine.registerAction call sites are app-todo's eight, all declared. What the valve bought is covered without it: the app still boots, declared actions still work, D5's inventory names each offender at startup, and the 404 names the defineAction to add. A regression test pins that the retired variable has no effect, so a stale deployment script fails loudly instead of silently reopening the gate. ADR-0110 carries a dated revision note rather than a rewritten history; D6 now says explicitly that its strongest form is no flag at all. runtime 888, metadata 276, objectql 1163 — each by exit code. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DTTKgYriDB6RVrkYjxxnG1
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 18 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-zhuang
pushed a commit
that referenced
this pull request
Jul 30, 2026
Integrates #3987 (ADR-0110 D3 revised): the undeclared-action refusal has no opt-out — OS_ALLOW_UNDECLARED_ACTIONS is retired. The conflict was this branch's #3962 single-wrap adjustment inside the old valve test, which #3987 rewrote into "refuses regardless of the retired flag"; main's side taken verbatim, as the valve semantics the adjustment belonged to no longer exist. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011AvZj6cLX7APd7roh2eK4F
os-zhuang
added a commit
that referenced
this pull request
Jul 30, 2026
… single wrap (#3962) (#3969) Fixes #3962 — the platform decision classifying the 200-on-failure wire as a bug, not a contract. The 200-with-inner-envelope shape was never designed: no ADR or doc specified it, it originated as the catch block reusing deps.success(), and /actions was the only route of 12 that double-wrapped. Five defects traced back to that one extra layer. Contract now, identical to /data: - ran, returned → 200 {success:true, data: <handler return value>} (single wrap) - ran, rejected → 400, semantic code on error.code (VALIDATION_FAILED with fields[] in details; FLOW_FAILED for a rejected flow) - never dispatched → 404 / 403 / 400 / 503 (unchanged, #3930/#3951) - crashed → 500 (unchanged, #3951; name-based discriminator now selects 400 vs 500) actions-validation-envelope.test.ts pinned the 200 "so flipping it later is a conscious, documented break"; #3962 is that decision and the flipped test cites it. Integrates ADR-0110 (#3958/#3987 — name identity, undeclared refusal with no opt-out) and #3971 (semantic error.code, details.code promotion) from main. client.actions.invoke/invokeGlobal still never throw: every failure status folds into {success:false, error}, success reads the single wrap, and a narrow legacy heuristic (boolean success, no foreign keys) keeps a current SDK correct against pre-#3962 servers. Migration (raw-HTTP callers): branch on the status; on 200, data is the handler's return value directly. SDK callers need no change.
os-zhuang
added a commit
that referenced
this pull request
Jul 30, 2026
…rity, bind tabPermissions + writeScope proofs (#3990) The three items #3980 flagged for the v17 window. 1. rls.priority removed — it promised "conflict resolution" that cannot exist: applicable policies OR-combine (most permissive wins, per the schema's own describe), so there is never a conflict to order and nothing ever read the key. Tombstoned via retiredKey (tsc never + parse-time prescription, both directions pinned), stripped mechanically by the permission-rls-priority-removed D2 conversion + D3 chain step, cleaned from factory helpers / the showcase example / the RLS docs, baselines updated. The docs table's enabled row now states the since-#3980-enforced contract. 2. permission.tabPermissions bound to the me-apps proof — after extending the proof to actually AUTHOR the property on the permission-set side (it had only mentioned tabPermissions in its header; binding then would have been the owner-anchor/allowTransfer anti-pattern). The new case pins hidden dropping the app from /me/apps, a more-visible grant winning it back (tabRank most-visible-wins), and unrelated principals untouched. 3. writeScope gets its runtime proof (showcase-scope-depth-write, 9/9, post-state assertions): 'unit' edits a BU co-member's row and does not descend; an ABSENT writeScope stays owner-only even under a 'unit' readScope (the axes gate independently); the hierarchy seam fails closed without the enterprise resolver. Bound as the scope-depth-write class. spec: 6879+ tests green incl. the chain-replay composability gate (union-merged over #3987's protocol-17 conversions); all gates green; dogfood fixture sweep corrected (the invoice-seed-isolation fixture no longer authors the retired key).
This was referenced Jul 30, 2026
os-zhuang
added a commit
that referenced
this pull request
Jul 30, 2026
…pPlugin never ran it under os dev (#4011) A positive-control dogfood (inject an undeclared handler, expect it named at boot) proved the inventory's kernel:ready hook never fired on the platform's own dev loop: AppPlugin is registered CONDITIONALLY, so the checklist that justifies D3's no-opt-out refusal (#3987) was never printed where an upgrade most needs it. The addressing vocabulary and reconcileActionRegistrations move into @objectstack/objectql — the engine owns the map they describe, and the dependency direction (runtime -> objectql) permits no other home. Runtime re-exports them unchanged, so dispatch, MCP and the inventory keep reading ONE implementation. ObjectQLPlugin audits in its existing kernel:ready handler after resyncAuthoredActions, re-runs on metadata:reloaded with fingerprint suppression, and stops the N-apps -> N-identical-reports multiplication. os dev / os serve still swallow ALL plugin boot logs (pre-existing sink defect, #4012); the 3 local analytics-slot test failures reproduce on clean main and are tracked in #4013.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
v17 release prep. D3 as accepted (#3958) refuses an undeclared handler but shipped
OS_ALLOW_UNDECLARED_ACTIONS=1as a migration valve that ran it anyway, documented as "removed in 18". This removes it before 17 ships, so the promise never needs tracking and the release carries the ruling without an asterisk.Why
It contradicts the ruling it accompanies. A flag that executes an ungoverned, system-elevated handler is the fail-open D3 exists to close. ADR-0049's trichotomy — enforced,
experimental, or absent — has no "enforced unless an env var says otherwise" state. Keeping the valve would have preserved the hole in configurable form, and the very existence of a "removed in 18" note was an admission that escape hatches without users become permanent.It had no observed users. A reconciliation sweep across the platform packages, every example and every plugin finds the only
engine.registerActioncall sites in the repo areapp-todo's eight — all declared since #3974. (The otherregisterActionhits aresettings.registerAction, a different service's registry that D3 does not govern, andresyncAuthoredActions, which registers from declarations.) The valve would have shipped a documented way to reopen the gate for a population nobody has ever seen.What replaces it
Nothing needs to, because the failure is bounded and already instrumented:
[action-governance]with its object and keydefineActionto add — its last clause now points at the boot log instead of at a flagMigration costs a code change rather than an env var. For reopening an authorization gate, that is the correct price.
Guard against silent resurrection
A regression test pins that setting the retired variable has no effect. Without it, a deployment script that still exports
OS_ALLOW_UNDECLARED_ACTIONS=1would look like it was doing something; the failure mode of a stale flag should be loud, not a silent re-open.ADR handling
ADR-0110 is
Accepted — implementedand merged, so its history is not rewritten. D3 gains a dated revision note recording what shipped, what changed before release, and why. D6 gains the corollary its own wording invited: the valve was correctly spelled under D6 (OS_ALLOW_*, opt out of enforcement) and still wrong, because what it opted out of was the gate itself rather than a strictness dial around it — so D6 governs naming and direction where a flag is justified, and is not a licence to add one.actions.mdxand thev17.mdxmigration entry drop the valve and state the no-opt-out rule plus the bounded blast radius.Verification
runtime888 ·metadata276 ·objectql1163 — each package run separately and checked by exit code. eslint clean.Pairs with #3982 (the console pin bump), which is the other v17 release-prep item; they are independent.
🤖 Generated with Claude Code
https://claude.ai/code/session_01DTTKgYriDB6RVrkYjxxnG1
Generated by Claude Code