Skip to content

feat(spec)!: retire the object's externalSharingModel — the external org-wide default no principal ever read (#23268) - #23441

Merged
objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-23268-retire-external-sharing-model
Oct 14, 2026
Merged

objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-23268-retire-external-sharing-model

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Fixes #23268
Clause-②: no

Executes ruling of record 6159300530 on #23268, letter A (the maintainer's 「23268 同意」): externalSharingModel is retired from packages/spec on 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 externalSharingModel as a second org-wide default for EXTERNAL (portal / partner) principals. Re-measured on origin/main e4ec6ec1a before 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 the plugin-security posture gate's env-tighten comparison), plus the metadata-core keep table and comments. Positive control on the same tree: git grep -c "sharingModel" origin/main -- packages/plugins/plugin-security/src is non-zero.
  • No runtime principal is external: packages/core/src/security/assemble-execution-context.ts assembles audience: undefined.
  • The authorization kernel's EXTERNAL rung (ADR-0095 / ADR-0099) is explicitly shared rows only, never OWD-derived.
  • Authored instances in this repository: two showcase objects (showcase_account, showcase_announcement).

The retirement kit (the spec-property-retirement playbook, surface by surface)

  • Tombstone. retiredKey() on ObjectSchema (a strictObject), 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 via publicSharing) and closes with the house os migrate meta --from 17 sentence. The route is the tombstone, not the strict deletion: tsc fails at every typed authoring site, and the refusal carries the prescription.
  • Neighbouring text. The tenancy.crossTenantAccess retirement hint no longer points at the key (ruling item 1). The publicSharing guidance entry that refuses a mis-nested publicSharing.externalSharingModel now says the key is removed, instead of steering the author "one level up" to a dead key. publicSharing itself is untouched (ruling item 3). The audience doc comment in kernel/execution-context.zod.ts changes one sentence.
  • D2 conversion object-external-sharing-model-removed. Protocol 18, retiredFromLoadPath, retiredAfter: '17.7.0', order 64. Inserted in MAJOR_18_CONVERSIONS by identifier (between metricFiltersRemoved and objectGridDefaultFiltersRemoved) and defined directly above the latter's definition. It strips the key from every objects[] entry, one notice per object. Fixture: one object carrying it, one canonical control.
  • Registration. RETIRED_KEYS_BY_MAJOR[18] gains data/Object:externalSharingModel (entries/retired-keys/18.data__Object__externalSharingModel.ts). D3 entry object-external-sharing-model-retired (entries/semantic/), with its STEP18_RATIONALE fragment (order 96).
  • Ledger. liveness/object.json: the row goes planned (it still pointed at ADR-0090: Permission Model v2 — concept convergence, final naming, AI-authoring safety (tracking) #2696, which is done) to dead, kept as a tombstone row with verifiedAt 2026-10-14; the README object row records it; state-counts/object.md regenerated (53 live, 1 dead, 54 classified).
  • Generated. 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 by check:generated --fix. The four ratchets other than authorable-surface do not move, as expected for a key-level tombstone on a def that still emits.
  • Lint (@objectstack/lint). security-external-wider-than-internal and its exported SECURITY_EXTERNAL_WIDER are removed; security-owd-alias no longer reads the external key; OWD_WIDTH (its only user) goes; comments in authoring-rules.ts, rule-explanations.ts and scripts/check-doc-security-posture.mjs follow.
  • Posture gate (@objectstack/plugin-security). R1 (env-tighten-only) compares sharingModel alone; its external half goes with the key.
  • Examples. showcase_account and showcase_announcement drop the key; security/index.ts, security/positions.ts, coverage.ts and docs/showcase_tour_security.md stop describing an external dial.
  • Docs. 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.
  • QA item. docs/qa/platform-checklist/areas/access-security.json access-security.owd-save-gate revision 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. surface moves mixed to api because no browser step remains.
  • Comments. metadata-protocol/src/protocol.ts (one comment line in the authoring-gate docblock), plugin-audit/src/objects/sys-comment.object.ts and its pin's prescription text.
  • Changeset. .changeset/23268-external-sharing-model-retired.md: @objectstack/spec minor with the BREAKING banner, the FROM to TO table and the one-line fix; @objectstack/lint minor (the removed export); @objectstack/plugin-security patch; one ADR-0087 disposition, registered object-external-sharing-model-removed, object-external-sharing-model-retired.

Census class: registered, with a mechanical strip

Stored metadata can carry the key: Studio's object settings offered the select, and PUT /api/v1/meta/object persisted it until this release (saveMetaItem validated it against the closed enum). So the class is registered, not not-required: applyConversionsToStoredItem replays the D2 strip over a stored object row 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 carrying externalSharingModel before the upgrade; the replayed strip heals them on read.

Pins that moved, and why each still bears load

  • New pin packages/spec/src/data/object-external-sharing-model-retirement.test.ts (repo project; listed in vitest.repo-tests.json), 18 tests: the tombstone at the schema, the object registry binding, ObjectSchema.create() and defineStack (STACK_SCHEMA_INVALID / 422), every old value refused, the control, the walked shape, the mis-nested publicSharing guidance, the tsc channel (@ts-expect-error, compiled by check: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#test already declares (the same five roots as the precedents; check:cross-package-test-inputs green). Exclusions are listed in the file with a reason each: the declaration file, the metadata-core keep table (ADR-0106 D1 classifies every key of the walked shape), the REST refusal pin, and four ledger-advisory witnesses (below).
  • REST door 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 refused 422 INVALID_METADATA with the prescription on the active, ?package= and ?mode=draft doors, and nothing is stored. The draft-promotion pin is kept on security-owd-unset; the package-author carve-out pin is kept on an OWD-less body; legal-pair rows become the three OWD values.
  • Posture gate object-posture-gate.test.ts: the R2-retirement and external R1 cases go; R1's internal cases and the wiring pin stay.
  • Lint. validate-security-posture.test.ts: the rule's two tests and reachability fixtures go; the obj receiver'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 a dead row, and a dead verdict 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 to page.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).
  • CLI. validate-lint-mapping-connector-source.test.ts (integration) and validate-json-warning-parity.e2e.test.ts (nightly tier) authored the key through defineStack, which now refuses it; their warned-row control moves to tool.outputSchema (experimental). i18n-flow-liveness-gate.test.ts: comment only.
  • Spec. object.test.ts (the crossTenantAccess hint asserts "explicit shares" and not the key), solution-blueprint.test.ts (the undeclared sibling becomes publicSharing), metadata-form-zod-reconciliation.test.ts (the planned omit row goes: tombstones are skipped by the detector), retired-key-removal-version.pin.test.ts (JOIN gains the conversion).
  • Dogfood 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 with expression-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 merging origin/main 04a314873 through scripts/pm/os-regen-merge.sh)

All exit 0 unless noted.

  • pnpm --filter @objectstack/spec build, then check:generated: all 14 artifacts up to date.
  • @objectstack/spec local project in three shards before the second merge: 225 / 7,366, 224 / 6,477 (1 todo), 224 / 6,619 plus one red fixed in retired-key-removal-version.pin.test.ts (JOIN), whose re-run is 1 / 20 green. After the merge: conversions, migrations, object.test.ts and that pin, 20 / 956.
  • @objectstack/spec repo 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-surface is green. After the merge: the new pin, retired-key-migrate-sentence.test.ts and conversions-major18-merge.test.ts, 3 / 45.
  • @objectstack/lint in three shards: 46 / 2,606, 46 / 1,862, and 46 files with 3 suites red on Cannot find package '@objectstack/spec/ui' while a concurrent gate rebuilt packages/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/rest meta-object-owd-gate.test.ts 11 / 11 (before and after the merge). @objectstack/metadata-core object-schema-fls-references.test.ts 52 / 52. @objectstack/plugin-audit sys-comment-retired-fields.test.ts 8 / 8. @objectstack/example-showcase 32 / 411. @objectstack/dogfood (guest grants + expression conformance) 2 / 28.
  • @objectstack/cli: unit i18n-flow-liveness-gate.test.ts 8 / 8; integration validate-lint-mapping-connector-source.test.ts 3 / 3; nightly tier (OS_TEST_TIERS=nightly) validate-json-warning-parity.e2e.test.ts 3 / 3.
  • typecheck: spec (including check:test-typecheck, which compiles the pin's @ts-expect-error), lint, plugin-security, plugin-audit, rest, cli, dogfood, example-showcase.
  • Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) derives 125 commands at 1060ed3e7; the run and its --ran reconciliation are in the report on the card. On the first full run (head a4633774e), 119 of 125 exited 0. check:platform-checklist exits 1 with 8 findings that are identical, line for line, on e4ec6ec1a (7 absent #symbol anchors in metadata-protocol / service-storage citations plus the attachments-storage.json anchor 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 head 1060ed3e7 the full set ran: 125 derived, 125 run, 124 exit 0 (check:dual-build-cjs-loads included); check:platform-checklist exit 1 with the 8 findings above; --ran: 125 accounted, 0 NOT-MEASURED. (Corrected by the seat from the dev's report 6164903067.) check-plugin-teardown-shape --self-test exited 3 on a shallow clone missing its pinned fixture commit; after fetching that commit it passes (48 cases).
  • ESLint, as a proven narrowing rather than the repo-wide pnpm lint. Population: eslint.config.mjs lints **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs} minus NEVER_LINTED. --no-inline-config --format json over the 36 changed lintable files at 1060ed3e7: 36 files, 0 errors, 0 warnings. Invariance: the config enables no type-aware linting (no parserOptions.project, no projectService) and the diff touches neither the config nor a baseline, so no verdict on an untouched file can move.
  • Changed lines: 1,334 (+884 / -450, 52 files), under the 3,000 human-merge line. No governed surface is touched.

Ablations (one-shot, through scripts/ablation-replace.mjs, each restored by blob equal to HEAD with an empty git diff HEAD; spec-source imports, no dist/ involved)

  • The tombstone replaced by the old 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 on showcase_announcement (anchor 1 to 0): exactly the tree-scoped walk went red, naming examples/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-sha pin 1fb1ced3, every read is typeof draft.externalSharingModel === 'string' or on an untyped record or an as any fixture.

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

  • Bound kept: no docs/adr/**, skills/**, .claude/**, .objectui-sha or content/docs/releases/**.
  • Not edited, now stale (governed): skills/objectstack-data/references/lint-rules.md:23 lists security-external-wider-than-internal as a live rule. skills/** is Tier H; a one-row deletion for whoever touches that file next.
  • Not edited, history: ADR-0086, ADR-0094 and ADR-0138 mention the key or the rule as part of their own records; the pending .changeset/pre/22161-lint-slice-2-one-line.md and .changeset/23224-guest-grants-channel.md describe the key as it was when they were written.
  • The out-of-repo population is NOT MEASURED, including cloud's stored rows (the stored preview above lists them).

Generated by Claude Code

…e REST door pins

Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 5 package(s): @objectstack/lint, @objectstack/metadata-protocol, @objectstack/plugin-audit, @objectstack/plugin-security, @objectstack/spec, touching 26 documentable anchor(s). ⚠️ 10 changed file(s) yielded no anchor (packages/lint/src/index.ts, packages/metadata-protocol/src/protocol.ts, packages/plugins/plugin-audit/src/objects/sys-comment.object.ts, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

20 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json a7a135dc267f0d154aca453f098c49ce8aac550c.

⛔ 11 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 10 changed file(s) yielded no anchor (packages/lint/src/index.ts, packages/metadata-protocol/src/protocol.ts, packages/plugins/plugin-audit/src/objects/sys-comment.object.ts, …) — pages documenting those are invisible to this run
  • 7 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 148 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 a7a135dc267f0d154aca453f098c49ce8aac550c → packageMentionDocs.

Which tree this was computed on

This run read content/docs from cc8e23feb1e10626f8bf96878cb8c58dcc38338c — the merge of head 1060ed3e74306c46a60e51745479a418806475bf into base a7a135dc267f0d154aca453f098c49ce8aac550c, 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 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

⚠️ 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 a7a135dc267f0d154aca453f098c49ce8aac550c → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 1060ed3e74306c46a60e51745479a418806475bf
Local-runs: none

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 main at the head, and the head's check-runs (read once). Not read: the dispatch order, the dispatching seat's conclusions. No worktree, build, test or gate re-run; record-recognisers.mjs --template was the only command run in the repo.

Check-runs on the head (read once, 2026-10-14): every completed run is success — Check Changeset, Governed Surface Queue Guard, Spec property liveness, Lint & Repo Gates and its 8 lint/typecheck jobs, TypeScript Type Check, Build Core, Build Docs, Dogfood Regression Gate, Check Documentation Links, Check PR Size, the four claim/card guards, Test Core 1/6, 2/6, 5/6, 6/6. In progress at read time: Test Core (3/6) and Test Core (4/6) — named here as in progress, not judged; the adopting seat reads their conclusions before the queue. Skipped (conditional, not failures): Console Pin Gate, Dogfood matrix shards, Dogfood Verify CLI, Packed-tarball smoke, Temporal Conformance. The check:platform-checklist exit 1 the dev reports locally is not a head check-run and is not judged here.

① Derived judgments

Ruling 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; crossTenantAccess hint stops pointing at it. Each accept-set / public-surface change the diff implies, judged:

  1. ObjectSchema.externalSharingModel → retiredKey(OBJECT_EXTERNAL_SHARING_MODEL_REMOVED) (packages/spec/src/data/object.zod.ts). Accept set narrows: every former enum value (and any other value) is refused at path externalSharingModel with the key-first prescription; tsc fails first via ServiceObject. The tombstone route (not strict deletion) is right for a strictObject whose key must keep its ledger row and carry a prescription (playbook route; rls.priority precedent the ledger note cites). The prescription names what still works (sharingModel, explicit shares, publicSharing) and closes with the house os migrate meta --from 17 sentence — right.
  2. Every door that parses an object now refuses it: ObjectSchema.create(), defineStack (STACK_SCHEMA_INVALID / 422), getMetadataTypeSchema('object') (the PUT /api/v1/meta/object/:name binding), and — because the schema parse runs before the lint discipline defers anything — the ?mode=draft door too. The REST pin (packages/rest/src/meta-object-owd-gate.test.ts) re-pins the three doors on a pair the retired ordering rule ADMITTED (private/private), so the refusal is the key, not the posture. Right, and the draft-door refusal is a deliberate, named widening of the 422 relative to the old external-wider rule (which drafts deferred); it follows from the tombstone and is recorded in the QA item.
  3. D2 conversion object-external-sharing-model-removed (protocol 18, retiredFromLoadPath: true, retiredAfter: '17.7.0', order 64, surface: 'object.externalSharingModel', lossless stripKeys over objects[], fixture with a control object). retiredAfter matches the neighbouring step-18 entries at the head (17.7.0). Census class registered with a mechanical strip — right: the PR body's reasoning (Studio offered the select; saveMetaItem persisted it against the closed enum) means stored object rows can carry it, and the pin replays applyConversionsToStoredItem('object', …) to show the strip heals them. not-required would have been wrong here.
  4. Registrations: RETIRED_KEYS_BY_MAJOR[18] gains data/Object:externalSharingModel; D3 entry object-external-sharing-model-retired (surface without backticks, replacement, reason naming the D2 id, acceptance criteria); STEP18_RATIONALE fragment order 96 in registry.ts.template; MAJOR_18_CONVERSIONS inserted by identifier. Changeset carries <!-- adr-0087: registered object-external-sharing-model-removed, object-external-sharing-model-retired -->. Right. The retired-key-removal-version.pin.test.ts JOIN gains the conversion — right (the pin would otherwise go red; the dev reports it did and fixed it).
  5. Liveness ledger object.json: planned → dead, authorWarn dropped, verifiedAt 2026-10-14, note cites the measurement tree and the tombstone anchor; README object row records the route; state-counts/object.md 53/0/0/1/0 = 54. Right — the tombstone keeps the key in the walked shape, so the row stays rather than leaving (the Retire tenancy.organizationField from the authorable surface — one platform table's fact, not customer configuration #19054 strict-deletion precedent does NOT apply). Spec property liveness is green on the head.
  6. Generated surface: authorable-surface/data.json → data/Object:externalSharingModel [RETIRED]; the three reference pages regenerate to never | optional | [REMOVED] …. Right (the check:generated family sits under Lint & Repo Gates, green).
  7. @objectstack/lint public surface narrows: SECURITY_EXTERNAL_WIDER and the rule security-external-wider-than-internal removed from validate-security-posture.ts and index.ts; security-owd-alias loses its externalSharingModel branch but SURVIVES for sharingModel — right: the ruling's "the alias rule … go with it" is read as the rule's external reader, not the rule, because the alias rule's internal reader is still live (ADR-0090 D4) and deleting it would widen the accept set on an unparsed intake. Module-private OWD_WIDTH goes with its only user. On an unparsed intake the key now reports as liveness-dead-property (the lint liveness pin flips the real-ledger case to the dead branch). Right. authoring-rules.ts D7 block count 14 → 13 is a comment-only follow of the removal.
  8. @objectstack/plugin-security objectPostureGate: R1 compares sharingModel alone; the external half of R1 goes; OWD_WIDTH kept for the internal comparison. Behaviour narrows only on bodies that can no longer reach the gate (the schema refuses them first). Right; R2 stays retired as before ([3 of #7891] Flip the registration: runtimeTypes gains object + permission/book — whole rule families cross the publish door #8310).
  9. Examples: showcase_account and showcase_announcement drop the key; security/index.ts, positions.ts, coverage.ts, showcase_tour_security.md stop describing an external dial. Right (ruling item 1 names the two objects). @objectstack/example-showcase is private: true — no changeset owed.
  10. Docs (non-governed): permissions/sharing-rules.mdx section rewritten to "External principals — no external org-wide default", permissions/authorization.mdx (3 passages), protocol/objectql/security.mdx, docs/design/permission-model.md (2 passages), plus the three regenerated reference pages. Each now states the ADR-0095 EXTERNAL-rung reading, matching ruling item 2's sentence. content/docs/releases/** untouched (the drift bot flags 11 release-owned pages as affected; they are read-only and correctly left). Right.
  11. QA item access-security.owd-save-gate revision 5: the Studio external-wider clause goes; the refusal/admit/shipped-declarations clauses are rewritten to the tombstone's 422 on every door, the admit without the key, and "no retired key shipped"; the seal clauses are unchanged; surface mixed → api; the new source anchor object.zod.ts#OBJECT_EXTERNAL_SHARING_MODEL_REMOVED names a constant the diff defines; history row 5 added, revision 4 row preserved. Right.
  12. Ruling item 3 (publicSharing untouched): the publicSharing schema and policy are not changed. The one edit near it is the strict-guidance string for a MIS-NESTED publicSharing.externalSharingModel, which previously steered the author "one level up" to the now-dead key — it now says the key is removed. Right; leaving the old text would have prescribed a refused key.
  13. Ruling item 5 (cloud seat): the PR body names os migrate meta --stored for the cloud seat's v18 stored preview. Right.
  14. Neighbouring text: crossTenantAccess retirement hint no longer names the key (ruling item 1); execution-context.zod.ts audience doc comment; protocol.ts authoring-gate docblock; sys-comment.object.ts prescription and its pin. Comment-only; right. execution-context.zod.ts and protocol.ts are in other seats' in-flight claims; the claim comment cleared them as comment-only ordinary concurrency and the single-writer guard is green.
  15. New pin object-external-sharing-model-retirement.test.ts (repo project, listed in vitest.repo-tests.json): schema, registry binding, create(), defineStack with control, every value, walked shape, mis-nested guidance, @ts-expect-error channel, stored-row seam, boot door with a lit control, per-object notices + idempotence, load-path retirement, the three registrations, and a tree-scoped absence walk with anti-vacuity floors and per-entry exclusion reasons (no allowlist file). The exclusion set is structural and each entry's job is to spell the key. Right.
  16. Tests re-pinned rather than deleted where the key was a control: CLI integration/nightly fixtures move their warned-row control to tool.outputSchema (experimental); lint-liveness-properties.test.ts moves the planned-with-opt-in control to page.print and narrows the opt-in anti-vacuity with its reason; solution-blueprint.test.ts swaps the undeclared sibling to publicSharing; the dogfood guest-grants file drops the one fixture object that declared the key (ruling C on [Decision] 访客(guest)的行可见范围:沿用对象的内部共享模型(A,现状,公开对象全部行可见)、按 externalSharingModel 判(B)、还是只经分享链接(C) #23266 already said the guest reads neither key, so the case loses nothing). Right. The REST door-order pin [ADR-0131 D6] … ahead of lint AND R1, hatch open is removed because its body was external-wider; the same door order is still pinned by the surviving [#7674] R1 widening case (an env overlay that widens … is refused by the package door before R1, hatch open) — acceptable, no coverage lost.

Nothing in the diff widens any accept set. No governed surface (docs/adr/**, skills/**, .claude/**, .objectui-sha, content/docs/releases/**) is touched; Governed Surface Queue Guard is green.

Residual mentions at the head, judged: packages/metadata-core/src/object-schema-fls-references.ts:712 keeps externalSharingModel: keep — right, the ADR-0106 D1 table classifies every key of the walked shape and the tombstone keeps the key in it (its test is pinned against the live shape); packages/plugins/plugin-sharing/src/sharing-service.ts:493 is a comment that already says the key "is not read here for anyone (#23268)" — right as it stands; skills/objectstack-data/references/lint-rules.md:23 still lists the removed rule — governed, see ③.

② Semver level

.changeset/23268-external-sharing-model-retired.md: @objectstack/spec: minor, @objectstack/lint: minor, @objectstack/plugin-security: patch, title feat(spec)!:, BREAKING banner, FROM → TO table (the key and the SECURITY_EXTERNAL_WIDER import), the one-line fix, the prescription verbatim, the retirement kit, the NOT MEASURED out-of-repo consumer line, and the ADR-0087 registered disposition naming both ids.

  • The breaking narrowing is published as minor on the 18.0.0-next.0 line, which is this repo's launch-window convention: the head's pending changesets for the same class (23082-view-data-api-provider-retired.md, 15204-s8-catalog-row-objects-retired.md) use feat(spec)!: + minor + a registered disposition, and the Check Changeset job's "Require an ADR-0087 disposition on a declared-breaking changeset" and "Guard against accidental major bumps (launch window)" steps both passed on the head. Matches.
  • @objectstack/lint: minor — a removed export and rule id: a published-surface narrowing; the FROM → TO row names it. Matches (same convention).
  • @objectstack/plugin-security: patch — one internal branch of a gate goes; no export changes. Matches.
  • Packages touched without a changeset: @objectstack/rest, @objectstack/plugin-audit, @objectstack/metadata-protocol, @objectstack/cli, @objectstack/dogfood (tests and comments only), @objectstack/example-showcase (private). None owes one. Right.
  • Clause-②: line: present in the PR body (Clause-②: no) and in the changeset (Clause-②: no (narrowing)), matching the claim (6161609217) and triage's grading (Clause-②: no, contract-tier review). Matches.

③ Boundary flags

Dev report 6164903067, open_questions: [] — nothing to answer there. deviations, each answered:

  1. File surface beyond the claim's list (stale tests across cli/lint/rest/plugin-security/plugin-audit/dogfood/spec, vitest.repo-tests.json, lint/src/index.ts, check-doc-security-posture.mjs, docs/design/permission-model.md, the publicSharing guidance string) — answered, accepted: each is a consequence of the retirement (a test authoring the key as a control, the export's barrel, a comment, a design doc restating D11, and a guidance string that prescribed the dead key); each is listed in the PR body; none is a governed or single-writer path; the single-writer guard is green.
  2. object-schema-fls-references.ts keep table NOT edited — answered, right (①, residual mentions): the tombstone keeps the key in the walked shape the table is pinned against; the claim listed the file on the assumption of a strict deletion.
  3. Lint witnesses kept authoring the key on unparsed intakes — answered, right: a dead ledger row warns on an unparsed intake, so those four files remain valid witnesses that the ledger loaded; the tree walk excludes them by name with the reason, not by allowlist.
  4. PR body stale on one line (check:dual-build-cjs-loads NOT MEASURED vs exit 0 on the final run) — answered: the body text is the dev's evidence prose; this record judges from the head's check-runs, not that line. The adopting seat applies the dev's correction text if it edits the body; no verdict weight.
  5. First pre-push of the ADR commit refused for a card trailer — process note on the companion branch; no effect on this diff.

out_of_scope_findings, each routed:

  • Permission Model v2 — named follow-up ADRs (post-ADR-0090 parking lot) #2776 item 6 pointer (ruling item 2's last clause) — belongs to the Tier H D11 PR / the PM, not this diff. Escalated to the adopting seat: confirm the pointer lands with the companion, since this PR cannot carry it.
  • objectui follow-up card (ruling item 4: filed by seat 3 to the objectui lane now, not after this PR lands) — not verifiable from this record's inputs; escalated to the adopting seat to confirm the card exists before the queue. Until it lands, a Studio author who picks a value gets the 422 prescription on save (pinned here), and stored rows heal on read; no runtime exposure.
  • skills/objectstack-data/references/lint-rules.md:23 lists the removed rule — Tier H (skills/**), correctly NOT touched by this PR. Escalated: the row deletion belongs with a governed PR (the companion docs(adr): ADR-0090 D11 — external visibility is the EXTERNAL rung; externalSharingModel retired (#23268) #23443 is the natural carrier).

Other flags from this review:

  • Test Core (3/6) and (4/6) in progress at read time (named above). The record's verdict is on the completed set; the adopting seat does not queue on it until both conclude success.
  • The PR is a draft at the head; the Fixes #23268 line and Clause-②: no are present; Part-of PR must not also close its card and The card this PR closes must claim this branch are green, consistent with the companion using Refs.
  • Docs Drift Check (advisory) flags 11 release-owned pages — read-only and untouched, as required.
  • Out-of-repo consumer population and cloud stored rows: NOT MEASURED, stated in the changeset and the body; the cloud seat pointer (ruling item 5) is present.

Implemented-by: claude/issue-23268-retire-external-sharing-model
Reviewed-by: session_01D6F9axfoPzR7t8G1J7Ytzz

VERDICT: PASS

Adopted by domain:spec seat 3 (#18883), baozhoutao, session session_01D6F9axfoPzR7t8G1J7Ytzz, at 2026-10-14T09:18Z: rendered by an isolated read-only reviewer at CONTRACT_REVIEW_TIER on the brief from record-recognisers.mjs --brief-template; spot-checked by the seat at the head (object.zod.ts:2760 externalSharingModel: retiredKey(OBJECT_EXTERNAL_SHARING_MODEL_REMOVED); object-schema-fls-references.ts:712 keep row unchanged). ③ routed: the objectui follow-up exists (objectstack-ai/objectui#12353, filed under ruling item 4); the skills/** row goes into the Tier H companion PR #23443; the #2776 item 6 pointer is the seat's, posted when the D11 amendment lands.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 14, 2026 09:24
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 14, 2026 09:24
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 14, 2026
Merged via the queue into main with commit 301eec6 Oct 14, 2026
49 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-23268-retire-external-sharing-model branch October 14, 2026 09:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants