Skip to content

feat(runtime)!: the undeclared-action refusal has no opt-out — drop OS_ALLOW_UNDECLARED_ACTIONS (ADR-0110 D3 revised) - #3987

Merged
os-zhuang merged 1 commit into
mainfrom
claude/drop-undeclared-actions-valve
Jul 30, 2026
Merged

feat(runtime)!: the undeclared-action refusal has no opt-out — drop OS_ALLOW_UNDECLARED_ACTIONS (ADR-0110 D3 revised)#3987
os-zhuang merged 1 commit into
mainfrom
claude/drop-undeclared-actions-valve

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

v17 release prep. D3 as accepted (#3958) refuses an undeclared handler but shipped OS_ALLOW_UNDECLARED_ACTIONS=1 as 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.registerAction call sites in the repo are app-todo's eight — all declared since #3974. (The other registerAction hits are settings.registerAction, a different service's registry that D3 does not govern, and resyncAuthoredActions, 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:

  • the app still boots; only an undeclared action's own endpoint 404s
  • every declared action still works, unchanged
  • D5's boot inventory already lists each offender under [action-governance] with its object and key
  • the 404 already names the defineAction to add — its last clause now points at the boot log instead of at a flag

Migration 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=1 would 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 — implemented and 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.mdx and the v17.mdx migration entry drop the valve and state the no-opt-out rule plus the bounded blast radius.

Verification

runtime 888 · metadata 276 · objectql 1163 — 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

…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
@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 30, 2026 1:03am

Request Review

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Jul 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/runtime.

18 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via packages/runtime)
  • content/docs/api/index.mdx (via @objectstack/runtime)
  • content/docs/api/wire-format.mdx (via @objectstack/runtime)
  • content/docs/automation/hook-bodies.mdx (via @objectstack/runtime)
  • content/docs/concepts/north-star.mdx (via packages/runtime)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/runtime)
  • content/docs/deployment/index.mdx (via @objectstack/runtime)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/runtime)
  • content/docs/deployment/single-project-mode.mdx (via @objectstack/runtime)
  • content/docs/deployment/vercel.mdx (via @objectstack/runtime)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/runtime)
  • content/docs/permissions/authentication.mdx (via @objectstack/runtime)
  • content/docs/permissions/authorization.mdx (via packages/runtime)
  • content/docs/plugins/packages.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/runtime)
  • content/docs/releases/implementation-status.mdx (via @objectstack/runtime)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang
os-zhuang merged commit 347f460 into main Jul 30, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/drop-undeclared-actions-valve branch July 30, 2026 01:22
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).
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants