Skip to content

The metadata write refusal answers a different code per TOPOLOGY: saveMetaItem's scoped-kernel branch refuses before the repository's package door ever runs #8184

Description

@huangyiirene

What this is

Fallout found while implementing #7682 (package-writability discrimination in SysMetadataRepository.assertAllowed). Filed rather than fixed: #7682's dispatch declared sys-metadata-repository.ts as its file surface and explicitly forbade editing protocol.ts, which two other cards (#8136, #8003) are editing concurrently.

The two refusal points

An artifact-backed write of a type without allowOrgOverride is refused twice over, and which one answers depends on the kernel's environmentId — a row-scoping key, not a topology declaration:

  1. packages/metadata-protocol/src/protocol.ts, inside saveMetaItem, guarded by if (this.environmentId !== undefined) (search for the not_overridable message beginning "is provided by a code package and the type has not opted into per-org overlay writes"). Emits 403 NOT_OVERRIDABLE, reads no package.
  2. packages/metadata-protocol/src/sys-metadata-repository.tsassertAllowed, the topology-INDEPENDENT gate. After readonly-package-locks-studio: the read-only metadata refusal keys on the metadata type's overlay policy, not package writability (ITEM_LOCKED / WRITABLE_PACKAGE_REQUIRED never emitted) #7682 this one reads the base the caller named and answers 403 ITEM_LOCKED (read-only base) / 422 WRITABLE_PACKAGE_REQUIRED (runtime-only into a read-only base) / 403 NOT_OVERRIDABLE (writable or unnamed base).

Point 1 shadows point 2 whenever it runs. So after #7682, the SAME request answers differently per deployment shape:

kernelPUT /api/v1/meta/object/showcase_task?package=READONLY_PKG
host-config / CLI lightweight assembler (environmentId undefined — the flagship showcase, self-hosted servers)403 ITEM_LOCKED, lockSource: package
project/cloud per-env kernel (environmentId set)403 NOT_OVERRIDABLE — package never read

The QA run behind #7682 (#7637) measured the first row, which is why #7682's fix lands there.

Why it is worth closing

This is the same class of finding #5086 and #6710 already closed twice in this method: "keying authorization off a row-scoping key is what made a type-level declaration depend on deployment topology" (protocol.ts, the #5086 block comment). The refusal VOCABULARY now depends on it too — a cloud tenant and a self-hosted tenant get different machine-readable codes for the same refused write, and a client that learns to handle ITEM_LOCKED on one will not see it on the other.

Note it is not a regression: point 1 answered NOT_OVERRIDABLE before #7682 as well. #7682 makes the divergence visible rather than creating it.

Suggested shape (not a ruling)

Have point 1 consult the same shared isWritablePackage predicate (packages/metadata-protocol/src/package-writability.ts) that the ADR-0070 D1 gate a few hundred lines below it and the repository door both use, so one condition keeps one vocabulary on every kernel. The alternative — deleting point 1 and letting the repository answer on all topologies — is the larger change and would need its own reading of why the scoped branch exists.

packages/metadata-protocol/src/sys-metadata-repository.package-writability.test.ts (added by #7682) states this boundary in its final describe block, so whoever picks this up has the reachability argument and a harness already written.

Related

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions