Repository navigation
feat(spec)!: retire the object's externalSharingModel — the external org-wide default no principal ever read (#23268) - #23441
Conversation
…trations Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude <noreply@anthropic.com>
…e REST door pins Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude <noreply@anthropic.com>
… population Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude <noreply@anthropic.com>
…tire-external-sharing-model
Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude <noreply@anthropic.com>
…tire-external-sharing-model
📓 Docs Drift CheckThis PR changes 5 package(s): 20 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 11 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 148 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 cc8e23feb1e10626f8bf96878cb8c58dcc38338c && git checkout cc8e23feb1e10626f8bf96878cb8c58dcc38338c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a7a135dc267f0d154aca453f098c49ce8aac550c 1060ed3e74306c46a60e51745479a418806475bf && git checkout -B drift-repro a7a135dc267f0d154aca453f098c49ce8aac550c && git merge --no-ff 1060ed3e74306c46a60e51745479a418806475bf
node scripts/docs-audit/affected-docs.mjs --json a7a135dc267f0d154aca453f098c49ce8aac550c
|
Contract reviewServed-tier: Inputs read: card #23268 (body + all 8 comments, ruling of record 6159300530 letter A, dev report 6164903067), PR #23441 body, its 52-file list and the net diff against Check-runs on the head (read once, 2026-10-14): every completed run is ① Derived judgmentsRuling 6159300530 item 1 binds: retire the key on v18 with an ADR-0087 entry, refuse at the located spot with "delete the key", census-class the stored rows, and take the lint rules, the posture gate's external branch, the ledger row, the two example objects, the five doc pages and the QA item's external clause with it;
Nothing in the diff widens any accept set. No governed surface ( Residual mentions at the head, judged: ② Semver level
③ Boundary flagsDev report 6164903067,
Other flags from this review:
Implemented-by: VERDICT: PASS Adopted by Generated by Claude Code |
Fixes #23268
Clause-②: no
Executes ruling of record
6159300530on #23268, letter A (the maintainer's 「23268 同意」):externalSharingModelis retired frompackages/specon the v18 line, ADR-0049 enforce-or-remove. The ADR-0090 D11 sentence is a separate Tier H PR (see "Companion" below); this PR touches no governed surface.What the key was, and why it goes
ADR-0090 D11 declared
externalSharingModelas a second org-wide default for EXTERNAL (portal / partner) principals. Re-measured onorigin/maine4ec6ec1abefore this branch:git grep -n "externalSharingModel" origin/main -- 'packages/**.ts' ':!*.test.ts' ':!packages/spec/**': no row-pipeline reader. The only readers were authoring checks of the key against itself (the publish linter's alias and wider-than-internal branches, and theplugin-securityposture gate's env-tighten comparison), plus themetadata-corekeep table and comments. Positive control on the same tree:git grep -c "sharingModel" origin/main -- packages/plugins/plugin-security/srcis non-zero.packages/core/src/security/assemble-execution-context.tsassemblesaudience: undefined.EXTERNALrung (ADR-0095 / ADR-0099) is explicitly shared rows only, never OWD-derived.showcase_account,showcase_announcement).The retirement kit (the
spec-property-retirementplaybook, surface by surface)retiredKey()onObjectSchema(astrictObject),packages/spec/src/data/object.zod.ts. The prescription opens with`object.externalSharingModel`, says it never had an effect, prescribes "Delete the key", names what does work (sharingModel, explicit shares, share links viapublicSharing) and closes with the houseos migrate meta --from 17sentence. The route is the tombstone, not the strict deletion:tscfails at every typed authoring site, and the refusal carries the prescription.tenancy.crossTenantAccessretirement hint no longer points at the key (ruling item 1). ThepublicSharingguidance entry that refuses a mis-nestedpublicSharing.externalSharingModelnow says the key is removed, instead of steering the author "one level up" to a dead key.publicSharingitself is untouched (ruling item 3). Theaudiencedoc comment inkernel/execution-context.zod.tschanges one sentence.object-external-sharing-model-removed. Protocol 18,retiredFromLoadPath,retiredAfter: '17.7.0', order 64. Inserted inMAJOR_18_CONVERSIONSby identifier (betweenmetricFiltersRemovedandobjectGridDefaultFiltersRemoved) and defined directly above the latter's definition. It strips the key from everyobjects[]entry, one notice per object. Fixture: one object carrying it, one canonical control.RETIRED_KEYS_BY_MAJOR[18]gainsdata/Object:externalSharingModel(entries/retired-keys/18.data__Object__externalSharingModel.ts). D3 entryobject-external-sharing-model-retired(entries/semantic/), with itsSTEP18_RATIONALEfragment (order 96).liveness/object.json: the row goesplanned(it still pointed at ADR-0090: Permission Model v2 — concept convergence, final naming, AI-authoring safety (tracking) #2696, which is done) todead, kept as a tombstone row withverifiedAt2026-10-14; the READMEobjectrow records it;state-counts/object.mdregenerated (53 live, 1 dead, 54 classified).authorable-surface/data.json(data/Object:externalSharingModel [RETIRED]) and three reference pages (references/data/object.mdx,references/api/metadata.mdx,references/system/migration.mdx), regenerated bycheck:generated --fix. The four ratchets other thanauthorable-surfacedo not move, as expected for a key-level tombstone on a def that still emits.@objectstack/lint).security-external-wider-than-internaland its exportedSECURITY_EXTERNAL_WIDERare removed;security-owd-aliasno longer reads the external key;OWD_WIDTH(its only user) goes; comments inauthoring-rules.ts,rule-explanations.tsandscripts/check-doc-security-posture.mjsfollow.@objectstack/plugin-security). R1 (env-tighten-only) comparessharingModelalone; its external half goes with the key.showcase_accountandshowcase_announcementdrop the key;security/index.ts,security/positions.ts,coverage.tsanddocs/showcase_tour_security.mdstop describing an external dial.permissions/sharing-rules.mdx(the external-dial section becomes "External principals — no external org-wide default"),permissions/authorization.mdx(three passages),protocol/objectql/security.mdx,docs/design/permission-model.md(two passages), plus the regenerated reference pages.content/docs/releases/**is not touched.docs/qa/platform-checklist/areas/access-security.jsonaccess-security.owd-save-gaterevision 5: the external clauses go (the Studio external-wider clause; the external-wider refusal, admit and shipped-declarations clauses are rewritten to the tombstone's 422 on every door, draft included, the admit without the key, and "the showcase ships no retired key"). The packaged-object seal clauses are unchanged.surfacemovesmixedtoapibecause no browser step remains.metadata-protocol/src/protocol.ts(one comment line in the authoring-gate docblock),plugin-audit/src/objects/sys-comment.object.tsand its pin's prescription text..changeset/23268-external-sharing-model-retired.md:@objectstack/specminorwith the BREAKING banner, the FROM to TO table and the one-line fix;@objectstack/lintminor(the removed export);@objectstack/plugin-securitypatch; one ADR-0087 disposition,registered object-external-sharing-model-removed, object-external-sharing-model-retired.Census class:
registered, with a mechanical stripStored metadata can carry the key: Studio's object settings offered the select, and
PUT /api/v1/meta/objectpersisted it until this release (saveMetaItemvalidated it against the closed enum). So the class isregistered, notnot-required:applyConversionsToStoredItemreplays the D2 strip over a storedobjectrow as{ objects: [row] }, pinned in the new test.For the cloud seat (ruling item 5): cloud's v18 stored preview,
os migrate meta --stored, lists the tenant rows carryingexternalSharingModelbefore the upgrade; the replayed strip heals them on read.Pins that moved, and why each still bears load
packages/spec/src/data/object-external-sharing-model-retirement.test.ts(repo project; listed invitest.repo-tests.json), 18 tests: the tombstone at the schema, theobjectregistry binding,ObjectSchema.create()anddefineStack(STACK_SCHEMA_INVALID/ 422), every old value refused, the control, the walked shape, the mis-nestedpublicSharingguidance, thetscchannel (@ts-expect-error, compiled bycheck:test-typecheck); the D2 strip at the stored-row seam and the boot door (with a lit control), per-object notices, idempotence, retirement from the load path; the three registrations; and a tree-scoped absence walk over the radius@objectstack/spec#testalready declares (the same five roots as the precedents;check:cross-package-test-inputsgreen). Exclusions are listed in the file with a reason each: the declaration file, themetadata-corekeep table (ADR-0106 D1 classifies every key of the walked shape), the REST refusal pin, and four ledger-advisory witnesses (below).packages/rest/src/meta-object-owd-gate.test.ts: the external-wider 422 cases flip to the substance of the new semantics: a body carrying the key, with a pair the old rule ADMITTED, is refused422 INVALID_METADATAwith the prescription on the active,?package=and?mode=draftdoors, and nothing is stored. The draft-promotion pin is kept onsecurity-owd-unset; the package-author carve-out pin is kept on an OWD-less body; legal-pair rows become the three OWD values.object-posture-gate.test.ts: the R2-retirement and external R1 cases go; R1's internal cases and the wiring pin stay.validate-security-posture.test.ts: the rule's two tests and reachability fixtures go; theobjreceiver's read-key list drops the key, which is the structural pin that the linter reads nothing external; push-site count 18 to 16.lint-liveness-properties.test.ts: the key is now adeadrow, and adeadverdict warns on an unparsed intake without an opt-in, so the "the ledger loaded" witnesses keep it; the real-ledger case flips from the planned branch to the dead branch (rule id, default hint,row.status === 'dead'); the planned-with-opt-in control moves topage.print; the opt-in anti-vacuity is narrowed because no shipped opt-in lacks an authorHint any more (the class stays held by the synthetic case).validate-lint-mapping-connector-source.test.ts(integration) andvalidate-json-warning-parity.e2e.test.ts(nightly tier) authored the key throughdefineStack, which now refuses it; their warned-row control moves totool.outputSchema(experimental).i18n-flow-liveness-gate.test.ts: comment only.object.test.ts(thecrossTenantAccesshint asserts "explicit shares" and not the key),solution-blueprint.test.ts(the undeclared sibling becomespublicSharing),metadata-form-zod-reconciliation.test.ts(theplannedomit row goes: tombstones are skipped by the detector),retired-key-removal-version.pin.test.ts(JOIN gains the conversion).guest-grants-channel.dogfood.test.ts: the one fixture object that declared the key is removed with its grant, endpoint and seed (28 of 28 pass withexpression-conformance).Error message pin sweep:
git grep -n "security-external-wider-than-internal\|SECURITY_EXTERNAL_WIDER\|owd_external_wider"outside history files now hits only prose that records the retirement, the published skill reference below, ADRs, and pending changesets.Evidence (head
1060ed3e7, after mergingorigin/main04a314873throughscripts/pm/os-regen-merge.sh)All exit 0 unless noted.
pnpm --filter @objectstack/spec build, thencheck:generated: all 14 artifacts up to date.@objectstack/speclocal project in three shards before the second merge: 225 / 7,366, 224 / 6,477 (1 todo), 224 / 6,619 plus one red fixed inretired-key-removal-version.pin.test.ts(JOIN), whose re-run is 1 / 20 green. After the merge: conversions, migrations,object.test.tsand that pin, 20 / 956.@objectstack/specrepo project in two shards: 30 / 470 and 29 / 471. NOT MEASURED:scripts/build-schemas-check-mode.test.ts, excluded because it alone exceeds the foreground cap (the precedent's reading);check:authorable-surfaceis green. After the merge: the new pin,retired-key-migrate-sentence.test.tsandconversions-major18-merge.test.ts, 3 / 45.@objectstack/lintin three shards: 46 / 2,606, 46 / 1,862, and 46 files with 3 suites red onCannot find package '@objectstack/spec/ui'while a concurrent gate rebuiltpackages/spec/dist; those 3 re-run 3 / 33. After the merge: the two posture and liveness files, 2 / 251.@objectstack/plugin-security: 185 files, 3,772 passed, 45 skipped.@objectstack/restmeta-object-owd-gate.test.ts11 / 11 (before and after the merge).@objectstack/metadata-coreobject-schema-fls-references.test.ts52 / 52.@objectstack/plugin-auditsys-comment-retired-fields.test.ts8 / 8.@objectstack/example-showcase32 / 411.@objectstack/dogfood(guest grants + expression conformance) 2 / 28.@objectstack/cli: uniti18n-flow-liveness-gate.test.ts8 / 8; integrationvalidate-lint-mapping-connector-source.test.ts3 / 3; nightly tier (OS_TEST_TIERS=nightly)validate-json-warning-parity.e2e.test.ts3 / 3.typecheck: spec (includingcheck:test-typecheck, which compiles the pin's@ts-expect-error), lint, plugin-security, plugin-audit, rest, cli, dogfood, example-showcase.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths) derives 125 commands at1060ed3e7; the run and its--ranreconciliation are in the report on the card. On the first full run (heada4633774e), 119 of 125 exited 0.check:platform-checklistexits 1 with 8 findings that are identical, line for line, one4ec6ec1a(7 absent#symbolanchors inmetadata-protocol/service-storagecitations plus theattachments-storage.jsonanchor floor); none is in a file this diff touches, and the new anchor this diff cites (object.zod.ts#OBJECT_EXTERNAL_SHARING_MODEL_REMOVED) resolves. Four exited 3 (PREREQUISITE NOT MET) and three were re-run green after building their closures:check:i18n(10 packages in sync),check:i18n-coverage(14 configs, none new),check:skill-examples(263 examples). On the final head1060ed3e7the full set ran: 125 derived, 125 run, 124 exit 0 (check:dual-build-cjs-loadsincluded);check:platform-checklistexit 1 with the 8 findings above;--ran: 125 accounted, 0 NOT-MEASURED. (Corrected by the seat from the dev's report6164903067.)check-plugin-teardown-shape --self-testexited 3 on a shallow clone missing its pinned fixture commit; after fetching that commit it passes (48 cases).pnpm lint. Population:eslint.config.mjslints**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}minusNEVER_LINTED.--no-inline-config --format jsonover the 36 changed lintable files at1060ed3e7: 36 files, 0 errors, 0 warnings. Invariance: the config enables no type-aware linting (noparserOptions.project, noprojectService) and the diff touches neither the config nor a baseline, so no verdict on an untouched file can move.Ablations (one-shot, through
scripts/ablation-replace.mjs, each restored by blob equal to HEAD with an emptygit diff HEAD; spec-source imports, nodist/involved)z.enum([...]).optional()(anchor 1 to 0): 7 of the pin's 18 tests went red (schema refusal, every value, registry binding,create(),defineStack, the tsc channel's parse leg, the boot door), 11 stayed green.externalSharingModel: 'private'planted onshowcase_announcement(anchor 1 to 0): exactly the tree-scoped walk went red, namingexamples/app-showcase/src/data/objects/announcement.object.ts. A first attempt whose replacement still contained the anchor was refused by the tool as a no-op and re-run with a landing replacement.Studio
The objectui follow-up (the object settings select and the Ext badge) is the PM's card (ruling item 4), not in this diff. Until it lands, a Studio author who picks a value in that select gets the 422 with the prescription on save, the same refusal pinned above; a stored row carrying the key is healed on read by the replayed strip. The console reads the key from an untyped draft, so nothing in objectui stops compiling: measured at the
.objectui-shapin1fb1ced3, every read istypeof draft.externalSharingModel === 'string'or on an untyped record or anas anyfixture.Companion
The ADR-0090 D11 sentence (ruling item 2) is a separate Tier H draft PR touching only
docs/adr/0090-permission-model-v2-concept-convergence.md; that PR refers to the card and leaves its state to this one.Acceptance notes
docs/adr/**,skills/**,.claude/**,.objectui-shaorcontent/docs/releases/**.skills/objectstack-data/references/lint-rules.md:23listssecurity-external-wider-than-internalas a live rule.skills/**is Tier H; a one-row deletion for whoever touches that file next..changeset/pre/22161-lint-slice-2-one-line.mdand.changeset/23224-guest-grants-channel.mddescribe the key as it was when they were written.Generated by Claude Code