Repository navigation
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
Conversation
…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>
📓 Docs Drift CheckThis PR changes 1 package(s): 12 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 11 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 39e95aecf330bc6e938afee0827f38b2f3c50265 && git checkout 39e95aecf330bc6e938afee0827f38b2f3c50265
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin abd254508b861da8ed1e96c2a813541b3274fb40 49f00d1b396d5f507432e19a6bcf983b194296d3 && git checkout -B drift-repro abd254508b861da8ed1e96c2a813541b3274fb40 && git merge --no-ff 49f00d1b396d5f507432e19a6bcf983b194296d3
node scripts/docs-audit/affected-docs.mjs --json abd254508b861da8ed1e96c2a813541b3274fb40
|
…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>
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 spellingOBJECTSTACK_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 answers403 NOT_OVERRIDABLE, and the refusal's first sentence names "a managed package". These stay as they were:Declared test change outside the card surface:
packages/plugins/plugin-security/src/packaged-permission-set-lock-gate.test.ts(declared todomain:serviceson #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 isregistryAllowsOverlay(type), which isisOverlayAllowedwithout the environment variable. These all read this one predicate:refusePackagedBaseOverride);refusePackagedBaseRemoval);packagedBaseRefusal, which setseditable/deletable).The
/automationdefinition doors ask the same protocol doors.The repository (
SysMetadataRepository.assertAllowed) applies the same rule one layer down. With the hatch open, onlyintent: 'runtime-only'(a new item no package ships) passes. Anoverride-artifactintent 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 indeleteMetaItem.The sentence comes from one builder,
managedItemSealedSentenceinpackaged-base-regime.ts. It is internal and not reachable from the package entry.flow,action,permission) keeps naming its sanctioned route: the clone or the switch.SysMetadataRepository.readOnlyBaseOverrideErrordrops its third parameter,hatchOpen, because nothing it chose survives the seal.Built entry declarations (
dist/index.d.ts), measured. Nothing widens:private static registryAllowsOverlay;andprivate isSealedManagedItem;;static readOnlyBaseOverrideError(type: string, packageId: string): Errorloses its optional third parameter, which narrows it; no caller outside the package exists (git grep);managedItemSealedSentenceis not exported.Door table (M1, M2, M4, M5), measured on a real boot
Setup:
bootStack(crmStack, { automation: true }).8311650228. Head is this branch.RestServer/api/v1/metaroutes; DISP is the runtimeHttpDispatcher.OS_METADATA_WRITABLE=flow,object,field,permission,position.OBJECTSTACK_METADATA_WRITABLE.OS_METADATA_WRITABLE=job.The two transports read the same unless a row notes otherwise. The dispatcher serves no
DELETEon/metaitems: it answers405 METHOD_NOT_ALLOWEDin every mode, at base and at head. The DELETE rows are therefore REST./meta/flow/crm_convert_lead_wizard(shipped)/meta/object/crm_lead(field relabel)/meta/permission/crm_sales_user/meta/position/sales_rep(M5)/automation/crm_convert_lead_wizard/meta/flowover a legacy overlay row/meta/flowwith no row/meta/object/crm_leadover a legacy row/meta/object/crm_lead?dropStorage=true/data/crm_lead404 OBJECT_NOT_FOUND, GET 500 DATABASE_ERROR)/meta/permission,/meta/positionover a legacy roweditable/deletable/meta/typesentry forflow,allowOrgOverride/overrideSourceControls, measured in every mode, on both transports, with base equal to head:
crm_opportunity.all): 200. Its REST DELETE: 200./meta/flow/NEW: 200./automation(create): 200./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.
envWritableTypes(), reads both spellings throughreadEnvWithDeprecation.envWritableMetadataTypes(), readsOS_METADATA_WRITABLEonly.At base, the legacy spelling advertised managed items as writable, while every write onto them answered 403:
allowOrgOverride: true;editable: true;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.tsandaction-activation-posture-gate.test.ts: 2 files, 46 passed. In thegroupandisolatedpostures a tenant admin is refused and the operator is allowed; enable is gated as well as disable; the clone door is not gated. In thesingleposture 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/automationanswer 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 realHttpDispatcher, protocol and repository:editable/deletablefalse;packages/qa/dogfood/test/managed-content-sealed.dogfood.test.ts, 10 cases, on CRM under the OS hatch:/automationrefused;dropStoragerefused with the data plane up;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 restoreTwo mutations reopen the hatch for managed items:
registryAllowsOverlayalso readsenvWritableTypes().if (hatchOpen && intent === 'runtime-only') return;becomesif (hatchOpen) return;.Both landed on disk (
ablation-replace: anchor 1 → 0, blob changed). Both reacheddist/: the preflight found the marker in 2 built files.The restore was proved three ways:
6df9a994bc36and690b710cc415);git diff HEADis empty and the working tree is clean;--absentpassed for both markers.The direction was an ordinary red.
Changeset and ADR-0087
The changeset is
.changeset/15206-managed-content-sealed.md:@objectstack/metadata-protocolminor, 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-registrationreads it as[BREAKING+bang+clause-②-narrowing] not-required.The
OS_METADATA_WRITABLErow incontent/docs/deployment/environment-variables.mdxnow 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/main11d119ab18merged, plus the plugin-security test change.49f00d1b39then changed one dogfood test file only, and shard 2/3 was re-run there.@objectstack/metadata-protocol@objectstack/rest@objectstack/runtime@objectstack/objectql@objectstack/plugin-securityDogfood shards:
63ea4b2a32: 78 files, 574 passed;49f00d1b39: 77 files, 551 passed + 1 skipped;63ea4b2a32: 76 files + 1 skipped file, 679 passed + 8 skipped.typecheckexits 0 for plugin-security (its test layer included) and dogfood at49f00d1b39, and for metadata-protocol, rest, runtime and objectql in the first round. No source inmetadata-protocolhas changed sincec2d18e52f5.Gate families come from
node scripts/pm/dispatch-gates.mjs --commands, run with no paths at49f00d1b39. All 109 commands ran, and every one exits 0.--ranreconciles: 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 jsonover the diff's 29.tsfiles, at49f00d1b39, gives 29 files, 0 errors and 0 warnings. The proof that narrowing excludes nothing:eslint.config.mjs:971matches**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}.eslint.config.mjs:327-328), so this diff cannot move the verdict on any file it does not touch.Serial constraints
3c4873ce32).git merge-treeagainst its head81cd9291bcexits 0, and its hunks (protocol.ts around 22697-22780; the repository around 81, 195, 1318, 1337) do not overlap.origin/main11d119ab18is merged (merged68163d1fc).origin/mainhas moved since, toabd254508b(9 commits);git merge-treeagainst it exits 0.domain:services) also editssecurity-catalog-cold-boot-environment-holder.dogfood.test.ts, at itsmkdtempSyncline and its cleanup. This PR leaves those lines alone. test(dogfood): the cold-boot catalog file takes its database roots under tmpdir() and removes them itself #22416 has not landed;git merge-treeagainst its headd8eee2191aexits 0.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./metaroute removes such a row.dropStorageof a managed object stays refused.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/typeslisting still reportsallowOrgOverride: 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.
OS_METADATA_WRITABLE=job, PUT/api/v1/meta/job/s2_job_job_restanswers 200 "Saved job 's2_job_job_rest' (env-wide, state=active)". This holds at base and at head, on both transports.OBJECTSTACK_METADATA_WRITABLE=job, PUT/meta/job/NEWanswers 403 NOT_CREATABLE and prescribesOS_METADATA_WRITABLE, while the protocol's own reader honours the legacy spelling.Carriers, noted, not filed. These are dead or stale after the seal:
OBJECT_OVERLAY_PACKAGE_MISMATCH;object-posture-gate;DELETE_RESTRICTEDat the/automationdoor;/meta;runtime/src/domains/automation.ts:1488;domain:servicesas theirs to carry: thepackaged-permission-set-lock-gate.tsheader,permission-set-projection.ts(around 1203, 1301, 1393),object-posture-gate.ts:14and:93, andsecurity-plugin.ts:4841;content/docs/permissions/authorization.mdx:480-486;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:
packages/rest/src/meta-object-owd-gate.test.tsrest-meta-packaged-action-permission-refusal.test.tsrest-meta-packaged-flow-refusal.test.tsrest-meta-managed-seal-hatch.test.ts(new)packages/runtime/src/domains/automation-packaged-base-lock.test.tsmeta-overlay-read-your-writes.test.tsmeta-managed-content-seal.test.ts(new)packages/objectql/src/protocol-commit-history.test.tsprotocol-destructive.test.tsprotocol-meta.test.tsprotocol-object-overlay-layer.test.tsprotocol-registry-shadow.test.tspackages/qa/dogfood/test/showcase-object-extension-scalar-divergence.dogfood.test.tsmanaged-content-sealed.dogfood.test.ts(new)security-catalog-cold-boot-environment-holder.dogfood.test.ts(from feat(objectql)!: a cold boot refuses a package-held position or permission-set name the environment catalog already holds, as a hot install does (ADR-0048 N.3) #22365, re-premised in Patch round 1;domain:cli, declared by the seat; test(dogfood): the cold-boot catalog file takes its database roots under tmpdir() and removes them itself #22416 edits other lines of it)domain:serviceson [PM seat] domain:services — 🟢 os-bill #6021):packages/plugins/plugin-security/src/packaged-permission-set-lock-gate.test.tsscripts/engine-double-contract.pinned.json(three rows for the new runtime double)Patch round 1
packages/plugins/plugin-security/src/packaged-permission-set-lock-gate.test.tswas declared todomain:serviceson [PM seat] domain:services — 🟢 os-bill #6021. Its two hatch-OPEN cases ("a package-less save targeting a package-declared set", and "a DRAFT save of the packaged name") are retitled "refused by the protocol package door". Each now assertsnot.toBeInstanceOf(PackagedPermissionSetLockedError), keepstoMatchObject({ code: 'NOT_OVERRIDABLE', status: 403 })and the no-row assertion, and no longer asserts that the message contains the package id. The header says which layer answers. No plugin-security source file changes, and no other plugin-security file. plugin-security is now green: 187 files, 3915 passed + 45 skipped.origin/mainmerged (11d119ab18, merged68163d1fc, no conflict). The merge brought feat(objectql)!: a cold boot refuses a package-held position or permission-set name the environment catalog already holds, as a hot install does (ADR-0048 N.3) #22365'ssecurity-catalog-cold-boot-environment-holder.dogfood.test.ts, which went red in shard 2/3:org_adminandeveryonethroughOS_METADATA_WRITABLE=position, and expected 200.403 NOT_OVERRIDABLE. This is the same M5 refusal as above.49f00d1b39.mkdtempSyncline and the cleanup are untouched; test(dogfood): the cold-boot catalog file takes its database roots under tmpdir() and removes them itself #22416 owns them. test(dogfood): the cold-boot catalog file takes its database roots under tmpdir() and removes them itself #22416 has not landed, and the two merge cleanly.Generated by Claude Code