Skip to content

fix(metadata-protocol): OS_METADATA_WRITABLE does not unlock a write into a read-only package - #8320

Merged
os-zhuang merged 3 commits into
mainfrom
claude/issue-8146-narrow-package-door-hatch
Aug 13, 2026
Merged

fix(metadata-protocol): OS_METADATA_WRITABLE does not unlock a write into a read-only package#8320
os-zhuang merged 3 commits into
mainfrom
claude/issue-8146-narrow-package-door-hatch

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#8146

OS_METADATA_WRITABLE no longer unlocks a write into a read-only package. The operator hatch stays exactly as documented — a metadata-type-level unlock — and stops reaching the package dimension it was never described as touching.

The defect

With the documented hatch set, the QA run measured:

OS_METADATA_WRITABLE=permission
PUT /api/v1/meta/permission/showcase_contributor?package=com.example.showcase
→ 200, row lands { package_id: 'com.example.showcase', organization_id: null }

com.example.showcase is a read-only package, and Studio renders that same permission matrix fully disabled behind a "Read-only" badge. Two surfaces answered the same question differently — and the row landed bound into the read-only package, which is not the per-org overlay the variable's own documentation describes.

Ruling chain (not re-litigated here)

  • Maintainer, 2026-08-12 — option B: the badge is telling the truth and the server should refuse. The hatch is type-level by its own shipped docs (content/docs/deployment/environment-variables.mdx: named types are treated "as allowOrgOverride: true … overridden per-org") and this package's CHANGELOG ("deliberately does not unlock the org dimension"). A type-level unlock says nothing about the package dimension, so the 200 is a bug.
  • PM, 2026-08-12 — NARROW (veto window unopposed): refuse only when the caller names a read-only base. A package-less hatch write still lands the overlay the docs promise. The BROADER reading — the hatch never unlocks a write against an item a read-only package provides, named or not — would retire the hatch's only documented use and is not implemented here; it needs a maintainer decision plus a docs/ADR change.

Premise verification — the measurement this card hangs on

NARROW is only justified if it preserves something: that a package-less hatch write currently lands a genuine env-wide / per-org overlay. That was an assumption, so it was measured against unpatched main rather than argued.

Both preservation cases pass on origin/main with the source change reverted — i.e. this behaviour exists today and this PR does not create it:

package-less hatch write, env kernel → { package_id: null, organization_id: null }
package-less hatch write, org kernel → { package_id: null, organization_id: 'org_acme' }

So the hatch's documented behaviour is real, and NARROW genuinely preserves it. The card's reproduction also still reproduces on current main (the refusal cases go red when reverted, meaning the write still answers 200) — the ruling's "close as moot" branch is not available.

The fix

The whole change is limb ordering in SysMetadataRepository.assertAllowed: the package door moves above the env-hatch limb and below every registry limb. isWritablePackage is reused from package-writability.ts, never re-spelled.

A hatch write that names a read-only base is refused with the codes the error-code ledger already registers for this condition — both code and status pinned:

  • override-artifact403 ITEM_LOCKED, carrying lockSource: 'package' (ADR-0010's reserved value for a package-layer lock) and the package id — the server-side counterpart of Studio's badge.
  • runtime-only422 WRITABLE_PACKAGE_REQUIRED, the same code and prescription saveMetaItem already emits for ADR-0070 D1.

What deliberately keeps working

The hatch is narrowed, not retired. No documentation changes and no capability is withdrawn:

  • a package-less hatch write still lands the env-wide overlay;
  • under an org kernel it still lands the per-org override the docs promise;
  • a hatch write naming a writable base still lands;
  • an ADR-0005 org overlay of a code-shipped item is untouched — it names the read-only package it customizes by construction and returns at the registry limb, above the door. This is why the door had to go below the registry limbs: one limb higher and the overlay model itself closes.

The refusal no longer prescribes the step the caller already took

With the hatch open, the old message would have said "set OS_METADATA_WRITABLE" while that variable is set — a false prescription of the kind #8185's patch round rejected, and the shape that makes an automated client retry forever. The message is now chosen by whether the hatch is open: with it open it explains that the hatch unlocks the type and not package writability and points at the remedy that works (retry without ?package=); with it closed the original prescription is still offered, because then it is true. Same code and status either way — one condition, only the remedy differs.

Reverse verification (direction predicted before running)

Predicted: the discrimination cases go red, the preservation cases stay green. Measured exactly that — source reverted to origin/main, suite kept:

5 failed | 15 passed (20)

The 5 red are precisely the #8146 refusal cases (including the end-to-end saveMetaItem reproduction). The 15 green include every preservation case — and their staying green on both sides is the point: they do not depend on this change, which is what makes them honest pins rather than artifacts of it.

origin/main has not touched sys-metadata-repository.ts since this branch's merge base, so the reverted file is genuinely the pre-change version.

Tests

packages/metadata-protocol/src/sys-metadata-repository.package-writability.test.ts — 20 cases (9 inherited from #8185, 11 added here). Every refusal case pins bothcode and status; the preservation cases assert where the row binds, not merely that the call succeeded.

metadata-protocol 77 files / 1128 tests passed
objectql 196 files / 3451 tests passed
rest 107 files / 1781 tests passed
runtime 150 files / 2306 tests passed

Consumer sweep direction, stated so it can be reviewed: those three are downstream consumers, hand-picked as the packages that actually reference OS_METADATA_WRITABLE / envWritableMetadataTypes (the full ...@objectstack/metadata-protocol prefix set is effectively the whole repo). The ^... suffix form was used only to build dependency closures. This change moves a real allow decision — unlike #8185, which moved only code selection — so the sweep was the point, not a formality.

Gates

Named by the dispatch: check:cross-package-test-inputs, check:durability-log-level, check:filter-alias-parity.

Re-derived against the actual diff, which surfaced five families the dispatch list missed — all run, all green: check:changeset-gate-self-tests, check:objectui-changeset, check:objectui-pin-fresh, check-changeset-no-major.mjs, check-objectui-pin-fresh.mjs. Plus check:nul-bytes, check:error-code-casing, and the two convention-triggered families a new test file moves: check:query-options-erasure and check:type-check-coverage (the type-check ledger was not raised — no new debt). ESLint clean on both files; the one complaint at line 863 is pre-existing and identical on the merge base.

No new error codes are introduced — both were already registered to this package by #8185 — so nothing in packages/spec moves.

Known boundary — #8184 stays open, out of scope here

On a scoped kernel (environmentId !== undefined) saveMetaItem refuses earlier, in protocol.ts, with the undiscriminated NOT_OVERRIDABLE — so this refusal is not reachable on that topology. That file is forbidden to this card (#8136 and #7654 are editing it concurrently, and the scoped-kernel refusal there belongs to #8184). Not a regression: that branch answered NOT_OVERRIDABLE before this change too. The suite's final describe states the boundary so it does not read as coverage.

Provenance note for the reviewer

This card was re-dispatched after an earlier agent's container died. That agent left no remote branch, but it had left a local commit and worktree, which this session recovered rather than rewrote. Everything above — the premise measurement, the reverse verification, the consumer sweep and every gate — was re-run in this session against the recovered tree; none of it is inherited on trust. One stale comment from #8185 ("the hatch half is deliberately uncovered") was corrected, since this change is what closes that gap.


Generated by Claude Code

…fusal and its preservation cases (#8146)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WMpuAfA2KSdDjGF6tm1bH
…d' comment
#8185 wrote it while the OS_METADATA_WRITABLE gap was still open; this
change closes that gap, so the comment now contradicts the suite docblock.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WMpuAfA2KSdDjGF6tm1bH
@vercel

vercelBot commented Aug 13, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 13, 2026 5:19am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-protocol.

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

  • content/docs/concepts/metadata-lifecycle.mdx(via @objectstack/metadata-protocol)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/metadata-protocol)
  • content/docs/protocol/kernel/http-protocol.mdx(via @objectstack/metadata-protocol)

1 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/v9.mdx(via @objectstack/metadata-protocol)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

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.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Decision] OS_METADATA_WRITABLE unlocks a write on a read-only package while Studio renders a "Read-only" badge — which one is telling the truth?

2 participants

@os-zhuang@claude