Skip to content

feat(metadata-protocol)!: managed content is sealed — OS_METADATA_WRITABLE no longer opens an item a managed package ships (ADR-0131 D6, #15206 S2) - #22401

Merged
objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/issue-15206-s2-managed-seal
Oct 9, 2026
Merged

objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/issue-15206-s2-managed-seal

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Refs #15206 (S2)
Clause-②: no

Stage S2 of #15206: managed content is sealed (ADR-0131 D6, regime C). The operator hatch OS_METADATA_WRITABLE, and its legacy spelling OBJECTSTACK_METADATA_WRITABLE, no longer opens an overlay write onto an item a managed package ships, and no longer opens a removal of one. Such a request now answers 403 NOT_OVERRIDABLE, and the refusal's first sentence names "a managed package". These stay as they were:

  • disabling a managed flow or action (operator-gated, under its wall);
  • cloning a flow under a new name, which records no linkage;
  • creating a new flow in Studio (the positive control).

Declared test change outside the card surface: packages/plugins/plugin-security/src/packaged-permission-set-lock-gate.test.ts (declared to domain:services on #6021). Its two hatch-OPEN cases now pin that the protocol's package door answers, not the lock; see Patch round 1.

The mechanism (M3: one predicate)

isSealedManagedItem(type, name) holds when the item is artifact-backed and its type's registry entry opens no overlay channel. The registry half is registryAllowsOverlay(type), which is isOverlayAllowed without the environment variable. These all read this one predicate:

  • the save door (refusePackagedBaseOverride);
  • the removal door (refusePackagedBaseRemoval);
  • the read envelope (packagedBaseRefusal, which sets editable / deletable).

The /automation definition doors ask the same protocol doors.

The repository (SysMetadataRepository.assertAllowed) applies the same rule one layer down. With the hatch open, only intent: 'runtime-only' (a new item no package ships) passes. An override-artifact intent gets the sealed sentence; that covers draft promotion, restore and revert.

For a shipped item, two other places no longer consult the hatch: the code-only check in saveMetaItem, and the repository route in deleteMetaItem.

The sentence comes from one builder, managedItemSealedSentence in packaged-base-regime.ts. It is internal and not reachable from the package entry.

  • A type with an ADR-0126 regime row (flow, action, permission) keeps naming its sanctioned route: the clone or the switch.
  • Every other type says it is sealed, says that the hatch does not open a managed item, and keeps the source remedy.

SysMetadataRepository.readOnlyBaseOverrideError drops its third parameter, hatchOpen, because nothing it chose survives the seal.

Built entry declarations (dist/index.d.ts), measured. Nothing widens:

  • two new private members: private static registryAllowsOverlay; and private isSealedManagedItem;;
  • static readOnlyBaseOverrideError(type: string, packageId: string): Error loses its optional third parameter, which narrows it; no caller outside the package exists (git grep);
  • managedItemSealedSentence is not exported.

Door table (M1, M2, M4, M5), measured on a real boot

Setup:

  • The CRM example, booted through bootStack(crmStack, { automation: true }).
  • Base is 8311650228. Head is this branch.
  • Two transports: REST is the RestServer /api/v1/meta routes; DISP is the runtime HttpDispatcher.
  • Hatch modes:
    • OS: OS_METADATA_WRITABLE=flow,object,field,permission,position.
    • LEGACY: the same value under OBJECTSTACK_METADATA_WRITABLE.
    • NONE: no hatch.
    • JOB: OS_METADATA_WRITABLE=job.

The two transports read the same unless a row notes otherwise. The dispatcher serves no DELETE on /meta items: it answers 405 METHOD_NOT_ALLOWED in every mode, at base and at head. The DELETE rows are therefore REST.

Probe OS base OS head LEGACY base LEGACY head NONE and JOB (base = head)
PUT /meta/flow/crm_convert_lead_wizard (shipped) 200 403 NOT_OVERRIDABLE 403 403 403
PUT /meta/object/crm_lead (field relabel) 200 403 403 403 403
PUT /meta/permission/crm_sales_user 403 (plugin-security lock) 403 (protocol door) 403 403 403
PUT /meta/position/sales_rep (M5) 200 403 403 403 403
PUT /automation/crm_convert_lead_wizard 200 403 403 403 403
DELETE /meta/flow over a legacy overlay row 200 403 403 403 403
DELETE /meta/flow with no row 200 403 403 403 403
DELETE /meta/object/crm_lead over a legacy row 200 403 403 403 403
DELETE /meta/object/crm_lead?dropStorage=true 200, and the object leaves the data plane (POST /data/crm_lead 404 OBJECT_NOT_FOUND, GET 500 DATABASE_ERROR) 403, and the data plane stays up (201 / 200) not run not run not run
DELETE /meta/permission, /meta/position over a legacy row 200 200 (the #6960 repair, kept) 200 200 200
DISP envelope of the shipped flow, editable / deletable true / true false / false true / true false / false false / false
/meta/types entry for flow, allowOrgOverride / overrideSource true / env true / env (Q2) true / env true / env false / registry

Controls, measured in every mode, on both transports, with base equal to head:

  • PUT of a view overlay (crm_opportunity.all): 200. Its REST DELETE: 200.
  • PUT /meta/flow/NEW: 200.
  • POST /automation (create): 200.
  • Toggle off, then on: 200 / 200.
  • Clone: 200, and the clone carries no linkage keys.
  • PUT /meta/job/NEW: 403 NOT_CREATABLE under NONE, OS and LEGACY; 200 under JOB, unchanged (M6, out of scope, below).

M2: the two hatch readers diverge on the legacy spelling. This is measured.

  • The protocol's reader, envWritableTypes(), reads both spellings through readEnvWithDeprecation.
  • The repository's reader, envWritableMetadataTypes(), reads OS_METADATA_WRITABLE only.

At base, the legacy spelling advertised managed items as writable, while every write onto them answered 403:

  • the listing read allowOrgOverride: true;
  • the DISP envelope read editable: true;
  • every write answered 403, because the repository's hatch never opened.

At head the seal makes both readers irrelevant for managed items, and the envelope reads false under both spellings. For creating an item no package ships, the divergence remains, and it is reported below.

M4. This PR does not touch the toggle path or its gate. The gate is measured by automation-activation-posture-gate.test.ts and action-activation-posture-gate.test.ts: 2 files, 46 passed. In the group and isolated postures a tenant admin is refused and the operator is allowed; enable is gated as well as disable; the clone door is not gated. In the single posture the gate is inert, which is why the CRM boot reads 200 on the toggle. As measured above, the clone carries no linkage, and a new flow and POST /automation answer 200. M5. A position overlay through the hatch is refused at save time (the table above).

Pins

New:

  • packages/rest/src/rest-meta-managed-seal-hatch.test.ts, 6 cases. The REST door relays the seal for flow, object, field, permission and position, under both spellings and both kernel shapes. Each answer is byte-equal to the hatch-shut answer and names "managed package". It also covers DELETE of a flow and an object.
  • packages/runtime/src/meta-managed-content-seal.test.ts, 12 cases. It drives the real HttpDispatcher, protocol and repository:
    • a PUT is refused and writes no row;
    • the GET envelope reads editable / deletable false;
    • controls: a view overlay and a new flow answer 200.
  • packages/qa/dogfood/test/managed-content-sealed.dogfood.test.ts, 10 cases, on CRM under the OS hatch:

Re-premised: the existing pins that carried "the hatch opens a managed item" now pin the seal. They are in metadata-protocol (10 files), objectql (5), rest (3), runtime (2), and the dogfood showcase scalar-divergence file. That file now seeds its pre-seal rename as a legacy row and cold-boots, so its read assertions keep a premise.

Patch round 1 adds two more: the plugin-security lock-gate cases, and #22365's cold-boot catalog control (below).

Reverse verification, at c2d18e52f5, under a trap restore

Two mutations reopen the hatch for managed items:

  • In the protocol, registryAllowsOverlay also reads envWritableTypes().
  • In the repository, if (hatchOpen && intent === 'runtime-only') return; becomes if (hatchOpen) return;.

Both landed on disk (ablation-replace: anchor 1 → 0, blob changed). Both reached dist/: the preflight found the marker in 2 built files.

Suite Mutated Restored
metadata-protocol seal pins 13 failed / 72 passed 85 / 85
rest 6 failed / 6 6 / 6
runtime 8 failed / 4 passed (the 4 passing are the controls) 12 / 12
dogfood 4 failed / 6 passed (the 6 passing are the premise, #6960, view, new flow, toggle and clone) 10 / 10

The restore was proved three ways:

  • each blob equals HEAD (6df9a994bc36 and 690b710cc415);
  • git diff HEAD is empty and the working tree is clean;
  • after a rebuild, --absent passed for both markers.

The direction was an ordinary red.

Changeset and ADR-0087

The changeset is .changeset/15206-managed-content-sealed.md: @objectstack/metadata-protocol minor, with the BREAKING paragraph for the v18 prerelease line. Changesets is in pre mode.

The ADR-0087 marker is not-required (no-migration-prescription): no spec key, stored shape or export changes, and every stored row loads and serves unchanged. check:adr-0087-registration reads it as [BREAKING+bang+clause-②-narrowing] not-required.

The OS_METADATA_WRITABLE row in content/docs/deployment/environment-variables.mdx now says what the hatch opens and what it never opens, and lists the sanctioned route for each type.

Tests and gates

Package suites, at 63ea4b2a32: origin/main 11d119ab18 merged, plus the plugin-security test change. 49f00d1b39 then changed one dogfood test file only, and shard 2/3 was re-run there.

Suite Files Tests
@objectstack/metadata-protocol 223 passed + 3 skipped 28323 passed + 19 skipped
@objectstack/rest 270 passed 5151 passed + 327 skipped
@objectstack/runtime 345 passed 5551 passed + 19 skipped
@objectstack/objectql 390 passed 7666 passed
@objectstack/plugin-security 187 passed 3915 passed + 45 skipped

Dogfood shards:

  • shard 1/3, at 63ea4b2a32: 78 files, 574 passed;
  • shard 2/3, at 49f00d1b39: 77 files, 551 passed + 1 skipped;
  • shard 3/3, at 63ea4b2a32: 76 files + 1 skipped file, 679 passed + 8 skipped.

typecheck exits 0 for plugin-security (its test layer included) and dogfood at 49f00d1b39, and for metadata-protocol, rest, runtime and objectql in the first round. No source in metadata-protocol has changed since c2d18e52f5.

Gate families come from node scripts/pm/dispatch-gates.mjs --commands, run with no paths at 49f00d1b39. All 109 commands ran, and every one exits 0. --ran reconciles: 109 derived, 109 run, 0 NOT-MEASURED, 0 UNRUN.

Lint. CI owns the repo-wide lint. Here, a narrowed run of eslint --no-inline-config --format json over the diff's 29 .ts files, at 49f00d1b39, gives 29 files, 0 errors and 0 warnings. The proof that narrowing excludes nothing:

  • The population comes from eslint's own config: eslint.config.mjs:971 matches **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}.
  • The file count comes from the JSON output.
  • Type-aware linting is never enabled (eslint.config.mjs:327-328), so this diff cannot move the verdict on any file it does not touch.

Serial constraints

Open questions

Q1: legacy overlay rows of types that do not merge at read are stranded. These are flow, action, hook, object and similar types. A row the hatch wrote earlier keeps serving, and can no longer be edited or removed through /meta, with the hatch set or not.

This touches ADR-0029 D9.6 ("the hatch … the one door for the life of the customization") and the ADR-0086 D1 env-overlay tighten path. Both texts now describe a door that is shut; that is a Tier H edit.

Triage answered Q1 with C: S2 stays as built, and S5 reports hatch-written environment overlay rows on sealed items at boot and stops serving them. Nothing changes for Q1 in this PR.

Q2: the /meta/types listing still reports allowOrgOverride: true, overrideSource: 'env' for a type named in the hatch. Studio's per-item lock reads the envelope, which is now correct. The listing flag is about the type, not the item. The carrier is the rename in #22340.

Q2 was answered A: the flag stays, because the per-item envelope is the truthful signal and the key belongs to #22340.

Acceptance notes

Out of scope. These are reported for the seat to file; nothing was filed here.

  • The hatch opens runtime creation of code-only types (M6), class b. Reach: on CRM, OS_METADATA_WRITABLE=job, PUT /api/v1/meta/job/s2_job_job_rest answers 200 "Saved job 's2_job_job_rest' (env-wide, state=active)". This holds at base and at head, on both transports.
  • The legacy spelling diverges between the two readers, class a. Reach: under OBJECTSTACK_METADATA_WRITABLE=job, PUT /meta/job/NEW answers 403 NOT_CREATABLE and prescribes OS_METADATA_WRITABLE, while the protocol's own reader honours the legacy spelling.

Carriers, noted, not filed. These are dead or stale after the seal:

  • dead or unreachable code:
    • the write-side OBJECT_OVERLAY_PACKAGE_MISMATCH;
    • the packaged-baseline R1 branch in plugin-security object-posture-gate;
    • DELETE_RESTRICTED at the /automation door;
    • the plugin-security lock gate's metadata-door registration;
    • the D9.7 subtraction through /meta;
  • stale comments:
    • runtime/src/domains/automation.ts:1488;
    • in plugin-security, declared to domain:services as theirs to carry: the packaged-permission-set-lock-gate.ts header, permission-set-projection.ts (around 1203, 1301, 1393), object-posture-gate.ts:14 and :93, and security-plugin.ts:4841;
  • stale docs:
    • permission-sets.mdx:363;
    • plugins/adding-a-metadata-type.mdx:59-63;
    • docs/qa/platform-checklist/areas/studio-authoring.json (it says the hatch clears the read-only badge).

Cross-lane paths

These are outside the card's declared surface, and each is a test re-premised by the seal or a new pin:

Patch round 1

Patch round 2

The contract review found one published sentence this PR made false: the "Runtime OWD posture gate" bullet in content/docs/permissions/authorization.mdx (about lines 480-486). At this head, an environment overlay of a packaged object is refused 403 NOT_OVERRIDABLE in both directions, hatch open or shut; packages/rest/src/meta-object-owd-gate.test.ts pins exactly that. The page was declared to domain:devx on #6023. Only that sentence changed (commit 09eff73814); nothing else on the page, and no code or test.

Before:

An environment overlay of a
  **packaged** object may only *tighten* `sharingModel` /
  `externalSharingModel`, never widen them beyond the packaged declaration
  (`403 owd_widening_forbidden` — widen it in the package source and
  publish instead; this closes the `OS_METADATA_WRITABLE=object` escape
  hatch as an unvalidated widening path, ADR-0086 D1).

After:

An environment overlay of a
  **packaged** object is refused outright, whether it tightens or widens
  `sharingModel` / `externalSharingModel`, and with `OS_METADATA_WRITABLE`
  set or not (`403 NOT_OVERRIDABLE` — managed content is sealed, ADR-0131
  D6): change the posture in the package source and publish instead. The
  package door answers ahead of this gate, so its packaged-baseline rule
  (`403 owd_widening_forbidden`, ADR-0086 D1) is not reached through the
  metadata API.

The route named is the one the bullet already used, and the one the page names for packaged permission sets: change the package source and publish. An object has no linkage-free clone. The bullet's other sentences are unchanged and still true: the gate is registered on the seam, the write path only is gated, and R2 is retired.

Gates at 09eff73814. dispatch-gates --commands, run with no paths, derives 110 commands: round 1's 109 plus check:merge-driver. All 110 exit 0, and --ran reconciles 110 derived, 110 run, 0 NOT-MEASURED, 0 UNRUN. That set includes every docs family:

  • check-doc-frontmatter and its self-test;
  • check:doc-authoring and check:doc-anchors;
  • check:docs-redirects, check:docs-single-h1, check:docs-audit-scope, check:docs-transcript-drift and check:docs-spec-enumerations;
  • check-affected-docs and check:published-readme-links.

Outside the derived set, check:docs-locale-catch-all, check:docs-image-tag, check:docs-image-tag-sync and check:adr-links also exit 0. #22416 has since landed on main. git merge-tree of this head against main 27a8b33dec, which contains it, exits 0.


Generated by Claude Code

claude added 5 commits October 9, 2026 00:27
…TABLE no longer opens an item a managed package ships (ADR-0131 D6)

WIP: the two package doors and the repository type door read one predicate
(isSealedManagedItem: artifact-backed and the registry opens no overlay
channel); the hatch keeps its type-level unlock for items no managed package
ships. Refusals name the managed package. Tests follow.

Claude-Session: https://claude.ai/code/session_01EUBvqtauTDmHi2ZgY759p2
Co-authored-by: Claude <noreply@anthropic.com>
…d-content seal; re-premise the cases the hatch used to carry

The hatch-open pins flip to the seal (403 NOT_OVERRIDABLE, nothing written,
nothing removed) on the protocol, the repository, both HTTP transports and a
booted CRM; the controls (regime-O overlay, a new flow, the switch, the
linkage-free clone, the #6960 repair) stay green beside them. Cases that used
the hatch only to reach a downstream rule move onto a tenant-authored object or
a pre-seal row read by a cold boot.

Claude-Session: https://claude.ai/code/session_01EUBvqtauTDmHi2ZgY759p2
Co-authored-by: Claude <noreply@anthropic.com>
…sured sentence lengths

Claude-Session: https://claude.ai/code/session_01EUBvqtauTDmHi2ZgY759p2
Co-authored-by: Claude <noreply@anthropic.com>
…er's bound; its engine double joins the pinned ledger

Claude-Session: https://claude.ai/code/session_01EUBvqtauTDmHi2ZgY759p2
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Oct 9, 2026
@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-protocol, touching 17 documentable anchor(s).

12 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/declarative-endpoints.mdx (via NOT_CREATABLE (literal, a string literal in assertAllowed))
  • content/docs/api/metadata-api.mdx (via NOT_CREATABLE (literal, a string literal in assertAllowed))
  • content/docs/concepts/metadata-lifecycle.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class), SysMetadataRepository (symbol, a top-level class), saveMetaItem (symbol, a method of class ObjectStackProtocolImplementation), NOT_OVERRIDABLE (literal, a string literal in assertAllowed))
  • content/docs/deployment/environment-variables.mdx (via NOT_OVERRIDABLE (literal, a string literal in assertAllowed))
  • content/docs/deployment/validating-metadata.mdx (via saveMetaItem (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/kernel/cluster.mdx (via saveMetaItem (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/kernel/services-checklist.mdx (via deleteMetaItem (symbol, a method of class ObjectStackProtocolImplementation), saveMetaItem (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/permissions/administrator-guide.mdx (via NOT_OVERRIDABLE (literal, a string literal in assertAllowed))
  • content/docs/permissions/authorization.mdx (via saveMetaItem (symbol, a method of class ObjectStackProtocolImplementation), NOT_OVERRIDABLE (literal, a string literal in assertAllowed))
  • content/docs/permissions/capabilities.mdx (via NOT_CREATABLE (literal, a string literal in assertAllowed))
  • content/docs/permissions/permission-sets.mdx (via NOT_OVERRIDABLE (literal, a string literal in assertAllowed))
  • content/docs/ui/public-data-collection.mdx (via NOT_OVERRIDABLE (literal, a string literal in assertAllowed))

⛔ 4 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/releases/v17/17-0.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class), NOT_CREATABLE (literal, a string literal in assertAllowed))
  • content/docs/releases/v17/17-6.mdx (via NOT_OVERRIDABLE (literal, a string literal in assertAllowed))
  • content/docs/releases/v17/17-7.mdx (via NOT_OVERRIDABLE (literal, a string literal in assertAllowed))

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.

What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 11 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 27a8b33dec2eb98b73b3e5146e740067cdbd7806 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from c6394f1198c4640659d23357d8a964c16b6770c6 — the merge of head 09eff738144880b71352856df824fadb99af46ee into base 27a8b33dec2eb98b73b3e5146e740067cdbd7806, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin c6394f1198c4640659d23357d8a964c16b6770c6 && git checkout c6394f1198c4640659d23357d8a964c16b6770c6
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 27a8b33dec2eb98b73b3e5146e740067cdbd7806 09eff738144880b71352856df824fadb99af46ee && git checkout -B drift-repro 27a8b33dec2eb98b73b3e5146e740067cdbd7806 && git merge --no-ff 09eff738144880b71352856df824fadb99af46ee

node scripts/docs-audit/affected-docs.mjs --json 27a8b33dec2eb98b73b3e5146e740067cdbd7806

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 27a8b33dec2eb98b73b3e5146e740067cdbd7806 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

claude added 2 commits October 9, 2026 03:09
…protocol package door, which now seals a managed item with the hatch open (ADR-0131 D6)

With the hatch open, a save of a package-declared permission set is refused by
the protocol's package door ahead of the authoring-gate seam, as the file's own
hatch-CLOSED case already pins, so the lock is not reached. Both cases now
assert the class is NOT the lock's, keep the NOT_OVERRIDABLE / 403 envelope and
the no-row assertion, and drop the package-id message assertion; the header
says which layer answers.

Claude-Session: https://claude.ai/code/session_01EUBvqtauTDmHi2ZgY759p2
Co-authored-by: Claude <noreply@anthropic.com>
…tion rows the way an older release left them; the hatch no longer opens that save (ADR-0131 D6)

#22365's control saved stored definitions under two built-in position names
through OS_METADATA_WRITABLE=position. The positions are shipped by the
platform's own package, so the seal now refuses that save with the hatch set
too. The control pins the 403 NOT_OVERRIDABLE refusal, writes the rows at the
driver as the file's legacy-row case already does, and keeps its assertions:
the restart boots and the stored definition answers.

Claude-Session: https://claude.ai/code/session_01EUBvqtauTDmHi2ZgY759p2
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 49f00d1b396d5f507432e19a6bcf983b194296d3
Local-runs: none

Inputs read: card #15206 body and all 20 comments (the stage plan 6067844889, triage 6068032120, the S2 note 6071504870, the retriage 6073429093 and its answer 6073941543, both os-dev-reports 6073375433 and 6074422275); ADR-0131 D2/D6, ADR-0126 §3, ADR-0029 D9.6/D9.7, ADR-0086 D1 and #6960 on main; the cross-lane declarations #6021 6073437063, #6024 6074389847, #6023 6071511369 and the #22400 note 6074377711; PR #22401's body, its 32-file list (identical to git diff --name-only origin/main...head) and the net diff; the head's check-runs. The merge-base with main (abd254508b) is 11d119ab18; git merge-tree of the head against origin/main and against #22416's head d8eee2191a both exit 0 (a read of git objects, no worktree). PR #22416 is still open, not merged.

① Derived judgments

Accept-set changes the diff implies, each judged on the code at the head:

  • Save door (refusePackagedBaseOverride, protocol.ts:17239): predicate is isSealedManagedItem = isArtifactBacked && !registryAllowsOverlay; registryAllowsOverlay reads OVERLAY_ALLOWED_TYPES only, never envWritableTypes(). A hatch-open overlay of an artifact-backed item of a non-overlay type moves admitted → 403 NOT_OVERRIDABLE. Right (ADR-0131 D6 regime C). The named-base limb keeps ITEM_LOCKED. Right.
  • Removal door (refusePackagedBaseRemoval, :17342): same predicate, && !mergesOverlayAtRead(type). Hatch-open removal of a managed flow/object/action/hook row moves admitted → 403. Right. The A legacy env overlay on an artifact-backed item of a rolled-back type can no longer be REMOVED through the ordinary delete path (403) — only via OS_METADATA_WRITABLE #6960 carve-out (OVERLAY_CAPABLE_TYPES = supportsOverlay) is untouched, so a stored row of permission, position, page, app, dataset, book, tool, skill still removes and restores the package definition. Right, and pinned (dogfood control (#6960), packaged-base-refusal page delete → null).
  • Read envelope (servedLockState, :16828) asks both doors through packagedBaseRefusal; editable/deletable read false for a managed item with the hatch open under both spellings. Right, pinned in the runtime file ×4.
  • Repository (assertAllowed, sys-metadata-repository.ts:1784): the hatch limb is now if (hatchOpen && intent === 'runtime-only') return;, and override-artifact falls through to managedItemSealedSentence. put (:730), promoteDraft (:1146) and restoreVersion (:1275) all reach it with opts.intent; the protocol computes that intent from isArtifactBacked at protocol.ts:21025 (save), :22519 (publish), :25661 (revertCommit) and :26205 (rollback). So draft promotion, restore and revert are sealed one layer down. Right. assertDeleteAllowed (:2046) keeps the supportsOverlay and origin-gated carve-outs and seals the rest. Right.
  • saveMetaItem code-only check (:20450): for a shipped item it reads registryAllowsOverlay alone; for an item no package ships it still reads isOverlayAllowed (hatch included), so runtime creation of a hatch-named type stays open (M6; triage 6073941543: by design). Right.
  • deleteMetaItem route (:26800): for an artifact-backed item the repository-vs-raw route ignores the hatch. On an environment kernel the removal door (:26744) answers first, so ?dropStorage=true never reaches shouldDropStorage (:26916 / :27086). Right, pinned (dogfood: 403, data plane up).
  • /automation doors (runtime/src/domains/automation.ts:1500-1510): PUT /:name and DELETE /:name ask packagedBaseRefusal; POST / and the clone write go through tenantAuthoredWriteRefusal, which asks the same door. The toggle writes the activation ledger, not sys_metadata. Right, pinned (automation-packaged-base-lock hatch-open case now asserts PUT and DELETE 403 and toggle 200).
  • Sentences: codeOnlyOverrideError, readOnlyBaseOverrideError and the repository's type door no longer prescribe the hatch; codes and statuses unchanged. Right. The NOT_CREATABLE sentence for a runtime-only write still names the hatch, which is the one door that opens that write. Right.
  • Public surface (Clause-②: no): readOnlyBaseOverrideError(type, packageId) loses its optional third parameter — a narrowing; git grep at the head finds callers only in protocol.ts:17301, sys-metadata-repository.ts:1826 and in-package tests. Right. registryAllowsOverlay and isSealedManagedItem are private. managedItemSealedSentence is exported from its module but src/index.ts exports nothing from packaged-base-regime and package.json exposes only .. Nothing widens. Right.

Paths that write sys_metadata for an artifact-backed name, each read at the head: save (both kernels), draft save, publish/promote, restore, revert/rollback, delete (both routes), /automation PUT/POST/DELETE/clone, migrateStoredMetadata (:21716, writes through the sealed path and now records failed on a stranded row rather than rewriting it), plugin-security permission-set-overlay-discard.ts:258 (deletes a permission-set overlay row directly: the merge-at-read removal #6960 keeps, not a seal miss). No door edits or removes a managed item of a non-overlay type under either spelling. Two notes, neither a miss introduced here: (a) the host-config kernel's raw-engine repair (:27081) still deletes a stored residue row of a code-only type (job, field, capability) — pre-existing, hatch-independent, removes residue and leaves the code definition standing; (b) the repository trusts the caller's intent, so a direct getOverlayRepo().put(..., intent: 'runtime-only') on a managed name lands a row (the dogfood file seeds its legacy rows exactly so) — no production caller outside protocol.ts, pre-existing.

What must stay open — each kept and pinned, none sealed by accident: regime O (view overlay PUT/DELETE 200: dogfood, runtime ×4, packaged-base-refusal); the #6960 removal for permission sets and positions (dogfood 200, rows gone); toggle (dogfood 200 under single; the operator wall's 46 pins untouched); linkage-free clone (dogfood); a new flow through /meta and POST /automation (dogfood, runtime ×4); a new item of a hatch-named type (package-writability control: job runtime-only lands a row).

The removal door and the stranded rows: at base, hatch open, DELETE over a legacy overlay row of a managed flow or object reset it; at head it answers 403 and the row stays, as pinned. Triage's Q1 → C (6073941543) sends those rows to S5 (reported at boot, not served) and their migration fate to C7. The PR body, the changeset ("keeps serving, and can no longer be edited or removed through the metadata API, with the hatch set or not") and environment-variables.mdx ("Overlay rows this hatch wrote before are still served") are truthful at this head; no sentence promises a remedy that does not exist.

Re-premised pins, spot-checked (10 files + the showcase file): protocol.packaged-base-refusal (hatch case → a 2×2 matrix of spelling × kernel comparing hatch-open to hatch-shut byte for byte, plus page delete and view save still null), sys-metadata-repository.package-writability (NARROW's three preservation pins flipped to refused on both kernels, the legacy spelling added, a job runtime-only control added), protocol.legacy-overlay-delete, protocol.tenant-authored-write, protocol.delete-receipt-wording; objectql protocol-object-overlay-layer (the D9.7 subtraction under the hatch → refused, layer and data plane kept; the plural spelling folded into the same case), protocol-destructive (specimen moved to a tenant-authored object, the detector's remaining population), protocol-meta (specimen moved to a regime-O view); rest meta-object-owd-gate (tighten and both widen cases → 403 NOT_OVERRIDABLE, declaredCode undefined), rest-meta-packaged-flow-refusal; runtime automation-packaged-base-lock, meta-overlay-read-your-writes; dogfood showcase-object-extension-scalar-divergence (the rename seeded as a pre-seal row + cold boot, so both read cases keep a premise; the round-trip pinned refused). No subject weakened, skipped or deleted; the two owd-gate R1 cases are kept as refused-before-R1 pins rather than deleted. The plugin-security lock-gate file: its two hatch-OPEN cases now assert exactly what its hatch-CLOSED case asserts (not the lock's class, 403 NOT_OVERRIDABLE, no row) — the same subject, the layer identity pinned as moved; the lock itself stays positively pinned in packaged-permission-set-lock.test.ts:1049/1070/1105. #22365's cold-boot control still tests its subject (rows under built-in position names, restart boots, stored definition answers) with the seal's 403 pinned first; SYS is defined at line 55; its hunks (34-42, 115-125, 204-229) are adjacent to, not the same lines as, #22416's (38-57, 104-131), and merge-tree is clean.

Reverse verification: the two mutations (protocol registryAllowsOverlay also reading envWritableTypes(); repository if (hatchOpen) return;) reopen exactly the cases that assert the hatch-open 403s. The reported partitions match the code: rest 6/6 fail (4 PUT + 2 DELETE); runtime 8 fail / 4 pass (the 4 controls); dogfood 4 fail / 6 pass (premise, #6960, view, new flow, toggle, clone). New pins are not vacuous: every refusal case asserts status, code and no row, and the controls assert a row landed.

Findings, note only: the legacy-spelling reader divergence is its own card (triage); the hatch's runtime creation of allowRuntimeCreate: false types is by design (triage).

② Semver level

.changeset/15206-managed-content-sealed.md: @objectstack/metadata-protocol minor, feat(...)!:, Clause-②: no (narrowing), adr-0087: not-required (no-migration-prescription). The level matches what the diff publishes: source changes are confined to three metadata-protocol files; objectql (5), rest (3 + 1 new), runtime (2 + 1 new), plugin-security (1) and dogfood (3) change tests only, and scripts/engine-double-contract.pinned.json is tooling, so no other package owes a changeset. (narrowing) is BREAKING per AGENTS.md, and BREAKING on the v18 line is graded minor because .changeset/pre.json is {mode: pre, tag: next} and 22080-v18-line-opens.md already majors the fixed group that holds both @objectstack/spec and @objectstack/metadata-protocol (.changeset/config.json fixed[0]) — the same shape S1's changeset took. Every sentence about pre mode is true. The PR's Clause-②: no line agrees with the changeset's arm, which is where the gate reads it. The ADR-0087 disposition is honest: no spec key, stored shape or export moves, every stored row loads unchanged, and the stranded rows are S5's reported population and C7's migration fate by triage's ruling, so this change prescribes no migration. The merge-at-read list in the changeset (permission, position, page, app, dataset, book, tool, skill) is exactly the registry's supportsOverlay: true, allowOrgOverride: false set. The OS_METADATA_WRITABLE row in environment-variables.mdx is true at this head: the hatch opens runtime creation and the org-scoped write (orgScopedWriteRefusal still reads isOverlayAllowed) for items no package ships, and nothing on a managed item.

③ Boundary flags

Implemented-by: claude/issue-15206-s2-managed-seal
Reviewed-by: session_01EUBvqtauTDmHi2ZgY759p2

VERDICT: FAIL

…used in both directions, hatch set or not (ADR-0131 D6)

The runtime OWD posture gate bullet said such an overlay may only tighten the
OWD pair and that a widening one answers 403 owd_widening_forbidden, closing
the OS_METADATA_WRITABLE=object hatch as a widening path. Since the managed
content seal, the package door refuses every environment overlay of a packaged
object with 403 NOT_OVERRIDABLE, tightening or widening, with the hatch open or
shut, ahead of the gate. The bullet now says so and names the route: change
the posture in the package source and publish.

Claude-Session: https://claude.ai/code/session_01EUBvqtauTDmHi2ZgY759p2
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 09eff738144880b71352856df824fadb99af46ee
Local-runs: none

Re-review after the FAIL on 49f00d1b39 (6074635359). Inputs read on top of that review's: the patch-round-2 os-dev-report 6074930749, the amended #6023 declaration 6074649945, the rewritten PR body, the net diff 49f00d1b39..09eff73814, and this head's check-runs. The card has no objection since the last review (only a claim release, a new claim and the dev report). main is at 27a8b33dec, which is #22416's squash-merge commit; git merge-tree of this head against it exits 0 (a read of git objects, no worktree). The merge-base is unchanged, 11d119ab18.

① Derived judgments

git diff --stat 49f00d1b39..09eff73814: one file, content/docs/permissions/authorization.mdx, +7 −5, all inside the "Runtime OWD posture gate" bullet; one commit, docs(permissions): …. protocol.ts, the repository, the regime module, every test and the changeset are byte-identical to the reviewed head, so every ① judgment of 6074635359 stands as written: the one predicate at the save door, the removal door and the read envelope; the repository's runtime-only-only hatch limb reached by put, promoteDraft and restoreVersion with the protocol's isArtifactBacked intent; the hatch-independent code-only check and delete route for shipped items; the /automation doors; the kept regime-O overlay, #6960 removal, toggle, linkage-free clone, new flow and new hatch-named item; the narrowed readOnlyBaseOverrideError with no out-of-package caller; nothing widening.

The rewritten bullet, judged sentence by sentence against this head:

  • "An environment overlay of a packaged object is refused outright, whether it tightens or widens sharingModel / externalSharingModel, and with OS_METADATA_WRITABLE set or not (403 NOT_OVERRIDABLE — managed content is sealed, ADR-0131 D6)". True. saveMetaItem calls refusePackagedBaseOverride at protocol.ts:20389, whose predicate isSealedManagedItem reads the registry alone, and object declares allowOrgOverride: false; packages/rest/src/meta-object-owd-gate.test.ts pins the tightening overlay (private/private over public_read/private), both widening overlays and the lint-plus-R1 body, each 403 NOT_OVERRIDABLE with declaredCode undefined and no row, with envWritableObject: true. With the hatch shut the same door already answered at base.
  • "change the posture in the package source and publish instead". True as a route: the package author's change, the one the page already names for packaged permission sets; the sentence claims no runtime route, and an object has none (regime E extends, it does not re-posture).
  • "The package door answers ahead of this gate, so its packaged-baseline rule (403 owd_widening_forbidden, ADR-0086 D1) is not reached through the metadata API." True. runAuthoringGate has one call site, protocol.ts:20960, inside saveMetaItem, after the package door at :20389 and after the lint table (INVALID_METADATA at :20593 / :20796); the package door is asked on both kernel shapes (pinned on the host-config topology in sys-metadata-repository.package-writability.test.ts and the plugin-security lock-gate file, and on the environment kernel in the REST file). A body with a packaged baseline is by definition artifact-backed, so it meets the door first; owd_widening_forbidden (object-posture-gate.ts:132) is unreachable through /meta. The test file's "door ORDER" case pins exactly this.
  • The retained sentences — the gate is registered on the registerAuthoringGate seam and runs on runtime-authored object bodies; write-path only, stored metadata loads unchanged; R2 retired, the 422 lint door answers first for external-wider and unauthored-OWD bodies, and "the 403 gate remains for packaged-baseline widening only" — describe the gate's registration and scope, which the new sentence then says is not reached; for a non-packaged body the lint-before-gate order still holds. Not false.

Nothing else on the page moved (the diff is the bullet alone), and the page's only other mentions of the hatch, owd_widening_forbidden or NOT_OVERRIDABLE are the bullet's own (lines 481–487). The pages judged stale in 6074635359 are unchanged and still stale-not-false; environment-variables.mdx is unchanged and still true.

② Semver level

Unchanged and right: .changeset/15206-managed-content-sealed.md, @objectstack/metadata-protocol minor with the BREAKING paragraph, Clause-②: no (narrowing), adr-0087: not-required (no-migration-prescription), in pre mode (pre.json {mode: pre, tag: next}) on the fixed group 22080-v18-line-opens.md already majors. The round-2 commit is a docs page only, which publishes nothing from any package, so it owes no changeset and changes no level; the PR's Clause-②: no line still agrees with the changeset's arm. The body's Patch round 2 section quotes the before and after text exactly as the diff reads, adds the page to the cross-lane list under domain:devx / #6023, removes it from the stale-docs carrier list, and brings the Serial section current (#22416 landed; main 27a8b33dec; merge-tree clean) — each of those sentences checks out.

③ Boundary flags

Implemented-by: claude/issue-15206-s2-managed-seal
Reviewed-by: session_01EUBvqtauTDmHi2ZgY759p2

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 9, 2026 06:02
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 9, 2026
Merged via the queue into main with commit e02833c Oct 9, 2026
44 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-15206-s2-managed-seal branch October 9, 2026 06:32
This was referenced Oct 9, 2026
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/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants