Skip to content

fix(metadata): the object read's sortability follows the caller's field permission for every caller class (#22250) - #22308

Merged
objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/issue-22250-meta-fls-mask-admin
Oct 8, 2026
Merged

objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/issue-22250-meta-fls-mask-admin

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Fixes #22250
Clause-②: yes (widening)

What this does

For one caller, the object metadata read and the data route disagreed about a field that caller's field-level permission marks unreadable. The sortability projection served beside the schema on GET /meta/object/NAME marked that field sortable: true for a platform-admin or organization-admin caller, while the data route refuses a sort or filter on it with 403 PERMISSION_DENIED.

After this PR, the sortability projection is derived from the caller's own field permission for every caller class. The object definition itself (item.fields) is unchanged: whole for the exempt classes, because Studio and Setup authoring need it, and masked for everyone else. The data route and the permission read are unchanged. The spec's description of the projection is amended to match.

Measured first: the exemption and its stated reason

  • Where it is. isObjectSchemaMaskExempt in packages/metadata-core/src/object-schema-fls.ts (ADR-0106 D4). It exempts isSystem and any caller holding manage_metadata, studio.access or setup.access. The platform-admin set holds all three. An organization admin holds setup.access. resolveObjectSchemaMaskPosture answers such a caller passthrough / exempt before it asks the security service.
  • Its stated reason. ADR-0106 D4: "Studio/Setup authoring requires the full schema." The code adds the write-gate derivation: a projected read sent back by a PUT would delete the fields it withheld. So this is the triage ruling's first branch: keep the definition read for authoring, and make the runtime projections agree with the caller's field permission.
  • Dispatch assumption M2 was partly wrong. The sortability projection (translateMetaEnvelope in packages/rest/src/meta-item-read-gate.ts, which both transports call) already read the post-mask document. For an exempt caller, though, that document is the unmasked one, so the projection inherited the exemption.
  • Picker-facing field lists on the server. sortability is the only runtime projection the object read serves beside the definition. The list read and the layered view serve definitions only, with no projection. item.fields is itself the definition the ruling keeps for authoring. objectui handles its pickers against the permission read on its side (objectui#11925, objectui#11943).

Door table: examples/app-showcase, real boot

Setup: showcase_client_liaison (which marks showcase_project.budget, spent and budget_remaining as readable: false) is bound to the everyone position. Callers: the seeded platform admin; phone.demo promoted to organization admin; auditor.demo as a member.

The table was measured at base c8bb3c8d9c and at head 8b412a83e2, then re-measured on the final head a337d0cc53, which carries 805f1ba09a's change to the exemption predicate's source and the spec text. Every head cell below reads the same on 8b412a83e2 and on a337d0cc53.

Door Platform admin, base → head Organization admin, base → head Member (control), base → head
GET /auth/me/permissions, budget readable: false → same readable: false → same readable: false → same
GET /data/showcase_project field absent from rows → same field absent → same field absent → same
GET /data/showcase_project?sort=budget (and ?budget=100) 403 PERMISSION_DENIED → same 403 → same 403 → same
GET /meta/object/showcase_project, item.fields.budget present (22 fields) → present (22) present (22) → present (22) absent (19) → absent (19)
same read, sortability.fields.budget { sortable: true } (23 entries) → absent (20 entries) { sortable: true } (23) → absent (20) absent (20) → absent (20)
same read, ETag "39ce56dc" → "39ce56dc~3c4b8092" "39ce56dc" → "39ce56dc~3c4b8092" "39ce56dc~32c59ec2" → same
GET /meta/object/showcase_project/layers (authoring) effective 22 fields with budget → same same → same 19 without budget → same

At head, all three classes are served byte-equal sortability. Conditional reads, the same on both heads:

  • The platform admin's own ETag answers 304, and so does the organization admin's own ETag (on a337d0cc53).
  • The pre-change ETag "39ce56dc" answers 200.
  • The member sending the admin's ETag gets 200.

After unbinding the set, the unrestricted admin's ETag is "39ce56dc" again (byte-identical, ADR-0106 D3), and budget is { sortable: true } with 23 entries.

How

  • @objectstack/metadata-core (object-schema-fls.ts). The exempt posture of a caller exempt by capability, in a deployment with a security service, carries a lazy runtime question. That question asks the same getMetadataReadableFields the mask asks a non-exempt caller, at most once. It never rejects: a throw or an undefined answer settles undetermined, with one warn, so D4's "a sick security service cannot fault an exempt caller" holds. resolveObjectSchemaRuntimeView(document, posture) returns the view the runtime projections are derived from: the served document for every other caller, and for an exempt caller the fields their permission reads, plus a fingerprint of what it withholds. Each withheld name in that fingerprint carries a qualifier (@ occurs in no machine name), so an exempt caller and a projected caller denied the same field never share a validator. isObjectSchemaMaskExempt now delegates its capability half to a private helper, which the resolver asks alone. That adds no second elevation read (check:system-context-census).
  • @objectstack/rest.
    • translateMetaEnvelope takes an optional runtimeDocument (default: the served document) and derives sortability from it.
    • createMetaItemAnswer step 5 computes the runtime view and passes it through the translateEnvelope port, which gains an optional third parameter.
    • In rest-server.ts, the cached arm of GET /meta/:type/:name treats a posture carrying runtime like a projecting posture: the protocol is not handed If-None-Match, the runtime view's fingerprint is folded into the ETag, and the 304 is judged against that folded value. The private translateMetaEnvelope and metaItemAnswerSources forward the view.
  • @objectstack/runtime (domains/meta.ts). Only answerMetaItem's translateEnvelope port, which forwards runtimeDocument. Every dispatcher item read already goes through createMetaItemAnswer.

Spec text (cross-lane, domain:spec)

Declared by the seat on domain:spec's seat post. The change is to text only: no shape, key, enum or accept-set change.

  • packages/spec/src/api/sortability.zod.ts.
    • The ObjectSortabilitySchema.fields .describe() now says: "The domain is the served field map plus the always-provisioned id, less any field the caller's own field permission withholds: a caller exempt from the object-schema mask (ADR-0106 D4) is served such fields in item and gets no entry for them; for every other caller the served map already excludes them." The list of absent names gains "a field the caller may not read".
    • The module docblock's category list gains case 5, "Withheld by the caller's field permission". Case 1's domain sentence carries the same exception, which removes nothing for every caller but a D4-exempt one.
    • resolveObjectSortability's docblock says that, for a D4-exempt caller, it runs on the document narrowed to the fields the caller's own permission reads.
  • Generated. content/docs/references/api/sortability.mdx and content/docs/references/api/protocol.mdx were regenerated by pnpm --filter @objectstack/spec check:generated --fix. That forced a spec build, found only check:docs stale (14 of 15 green), and regenerated it. A second check:generated run then reported all 15 artifacts current. Nothing generated was hand-edited.
  • Changeset. No @objectstack/spec entry was added, because no changeset gate requires one for a description-only change:
    • Check Changeset (pr-automation.yml) counts the changesets a PR adds;
    • check-changeset-no-major.mjs, driven offline with this body as its event, reads Clause-②: yes (widening) and is satisfied by metadata-core and rest graded minor, and it lists no ungraded spec package;
    • check-empty-changeset.mjs is green.

Tests

  • packages/metadata-core/src/object-schema-fls.test.ts (new describe block):

    • per class (platform admin, organization admin, member): the runtime view holds only the readable fields, the exempt definition is served whole, and the member's view is the served document by reference;
    • one permission gives one view whatever the class; no validator is shared with the definition mask;
    • an unrestricted exempt caller is byte-identical; the question is lazy and asked at most once, and never for a document without fields;
    • undefined and a throw both settle undetermined with a warn; isSystem and a deployment with no service carry no question.

    One existing assertion went from toEqual to toMatchObject plus a typeof runtime === 'function' check, because the exempt posture now carries the function. Its "asks nothing" pin (asked === 0) is kept.

  • packages/rest/src/meta-object-sortability-caller-fls.test.ts (new), per class, on the cached (default) and uncached branches:

    • the definition is unchanged, sortability never names the unreadable field, and it is equal across classes;
    • the member control still agrees with resolveObjectSortability(item);
    • the cached arm's ETag is fingerprinted, the protocol is not handed the conditional, the admin's own ETag answers 304, the member's ETag does not cross over, and an unrestricted admin keeps "v1" and its 304;
    • a throwing service answers 200 with the definition and the unprojected sortability.
  • packages/runtime/src/domains/meta-object-sortability-caller-fls.test.ts (new): the same per-class pins on the dispatcher's protocol-backed and registry-backed item reads.

Reverse verification (from committed state, on-disk proof by scripts/ablation-replace.mjs):

  • A. translateMetaEnvelope was mutated to derive sortability from the served document (the behaviour before this PR). The REST suite went 5 failed / 8 passed. After pnpm --filter @objectstack/rest build, ablation-dist-preflight found the marker in dist, and the runtime suite went 4 failed / 4 passed. Restore: blob equal to HEAD, git diff HEAD empty; rebuild; preflight --absent with a clean tree; 13/13 and 8/8 green.
  • B. The exempt posture was mutated to never carry runtime. The metadata-core file went 7 failed / 29 passed, all assertion failures. Restored, blob equal to HEAD.

The first B attempt used an undefined identifier and went red on a ReferenceError, so that reading is void. The second was refused by the tool because the replacement contained the anchor, so nothing was mutated.

Local verification (final head a337d0cc53)

All suites ran through the shared verify lock. Exit codes were read from its VERDICT line.

  • This round (spec text and regenerated docs).
    • @objectstack/spec test: 625 files, 18711 passed and 1 todo. typecheck exit 0, which includes check:scripts-typecheck and check:test-typecheck.
    • dispatch-gates --commands derives 111 commands for the full touched path set (11 paths). All 111 ran and exited 0.
    • --ran reconciliation: 111 derived, 111 run, 0 NOT-MEASURED, 0 UNRUN (a derived zero: every entry records its exit code).
    • The derivation warns that one file it reads, scripts/platform-object-tenancy-census.json, changed on main since this branch's merge base, so that family read this branch's copy.
    • Narrowed lint on sortability.zod.ts:
      • eslint --no-inline-config --format json reports 1 file, 0 errors and 0 warnings.
      • The effective config (--print-config) enables 5 syntax rules and parserOptions.project is null.
      • With no type-aware linting, the change cannot move a verdict on a file it does not touch.
  • Earlier round (unchanged since).
    • @objectstack/metadata-core: 18 files, 425 passed; typecheck exit 0.
    • @objectstack/rest at 3768ffad20: 268 files, 5142 passed and 327 skipped; typecheck exit 0.
    • @objectstack/runtime: 340 files, 5508 passed and 19 skipped at 805f1ba09a; typecheck exit 0 at 3768ffad20.
    • Narrowed lint over those 7 files: 0 errors and 0 warnings.
    • Gates: 64 of 64 exited 0 at 3768ffad20.
  • Merge. main was merged once, at 9865012548 (fix(rest,runtime): ?package=all names no package on every /meta door of both hosts (#22188) #22265 touched rest-server.ts and runtime/src/domains/meta.ts in a different region). Against the current origin/main, git merge-tree --write-tree exits 0 and no file overlaps, so no second merge was made.

Answered by the seat

  • Clause-②. The measurement decides: yes (widening), with minor for metadata-core and rest and patch for runtime. The claim's line was revised to match. Two transport-local copies of the posture logic were refused.
  • The spec text. It rides this PR, not a separate card, with packages/spec lifted for exactly this text. It is declared cross-lane on domain:spec's seat post.

Acceptance notes

These are observations only, read from source and not measured. No card is filed for them.

  • objectui's docblock. objectui's packages/core/src/utils/column-sortability.ts repeats the sentence this PR amends ("the served field map plus the always-provisioned id"). That is another repo and is not edited here. Carrier: objectui domain:ui seat 1 (objectui#11925's filer).
  • Partially masked fields. For any caller, sortability keys on readability, while the security service's query guard also refuses a predicate or sort on a field served partially masked (maskingRule). Per getQueryableFields's docblock, a masked field is readable but not queryable. No in-repo app declares a maskingRule today (showcase permission-sets.ts). Whoever touches resolveObjectSortability next should weigh it.
  • External objects. On an ADR-0015 external object, an author-declared platform-injected column that the caller cannot read is re-added by unprovisionedInjectedColumns as sortable: true plus a caveat once the mask removes the declaration. This is the same for every class.

Generated by Claude Code

claude added 7 commits October 8, 2026 12:00
…runtime projections

An exempt caller's posture now carries a lazy `runtime` question (that
caller's own field permission), and `resolveObjectSchemaRuntimeView`
derives the view the runtime projections beside the schema read from.

Claude-Session: https://claude.ai/code/session_01EUBvqtauTDmHi2ZgY759p2
Co-authored-by: Claude <noreply@anthropic.com>
… on every item exit

The cached arm folds the runtime view's fingerprint into the ETag and
judges the conditional request itself for a posture carrying runtime.

Claude-Session: https://claude.ai/code/session_01EUBvqtauTDmHi2ZgY759p2
Co-authored-by: Claude <noreply@anthropic.com>
…red runtime view

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

Claude-Session: https://claude.ai/code/session_01EUBvqtauTDmHi2ZgY759p2
Co-authored-by: Claude <noreply@anthropic.com>
…of a second isSystem read

Claude-Session: https://claude.ai/code/session_01EUBvqtauTDmHi2ZgY759p2
Co-authored-by: Claude <noreply@anthropic.com>
…he test layer typechecks

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

github-actions Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/metadata-core, @objectstack/rest, @objectstack/runtime, @objectstack/spec, touching 17 documentable anchor(s).

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

  • content/docs/permissions/system-context.mdx (via isObjectSchemaMaskExempt (symbol, a top-level function))
What this run could not see
  • 5 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 — 145 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 dc4a5c630844f04587e790c21ca807f306239518 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from c04c5691ed821c9e0965e32cad5fb34be863f480 — the merge of head a337d0cc537ea09b8a2bee82f9b80270df735b1e into base dc4a5c630844f04587e790c21ca807f306239518, 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 c04c5691ed821c9e0965e32cad5fb34be863f480 && git checkout c04c5691ed821c9e0965e32cad5fb34be863f480
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin dc4a5c630844f04587e790c21ca807f306239518 a337d0cc537ea09b8a2bee82f9b80270df735b1e && git checkout -B drift-repro dc4a5c630844f04587e790c21ca807f306239518 && git merge --no-ff a337d0cc537ea09b8a2bee82f9b80270df735b1e

node scripts/docs-audit/affected-docs.mjs --json dc4a5c630844f04587e790c21ca807f306239518

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

claude added 2 commits October 8, 2026 14:29
…ssion as an absence case

Text only: the module docblock's category list gains case 5, and the
ObjectSortabilitySchema.fields description no longer claims the domain is the
served field map for a caller exempt from the object-schema mask.

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: a337d0cc537ea09b8a2bee82f9b80270df735b1e
Local-runs: none

Inputs: card #22250 (body; triage 6057391955; claim 6058980283 as edited; os-dev-reports 6061977564 and 6063483631), PR #22308 (body, 11-file list, the net diff from merge-base 58707166f to the head), ADR-0106 D1/D3/D4/D6/D8 on main, packages/spec/src/api/sortability.zod.ts on main and on the head, the domain:spec cross-lane declaration on #6017 (6062064959), and the check-runs on the head. Read-only: no worktree, no build, no test, no gate run.

① Derived judgments

Direction (triage: tighten, never loosen) — right.

  • item.fields is unchanged for every class. On the head isObjectSchemaMaskExempt reads isSystem, then holdsObjectSchemaMaskExemptCapability over the same OBJECT_SCHEMA_MASK_EXEMPT_CAPABILITIES the main predicate read inline: the same exempt classes, the definition still served whole; a member is still projected by step 4 of createMetaItemAnswer and by maskObjectDocument on the cached arm. Pinned per class in all three new suites.
  • sortability is narrowed only for a D4-exempt caller whose own field permission withholds a field. translateMetaEnvelope derives it from runtimeDocument (default: document), and resolveObjectSchemaRuntimeView returns the served document by reference unless the posture is passthrough carrying runtime AND the answer is project AND something is withheld. For a project, undetermined, disabled, no-service or not-applicable posture the view is the served document, so every non-exempt class and every deployment posture is byte-unchanged. Right.
  • Permission read (/auth/me/permissions) and the data route: no file of theirs is in the diff, and the door table on a real boot reads both unchanged for all three classes. Right.
  • Other runtime or picker-facing projections served beside the definition on main: none. resolveObjectSortability is the only resolveObject* export in packages/spec/src/api; in the non-test sources of rest and runtime, sortability is built only inside translateMetaEnvelope (item read, metaType === 'object'); the list read and the layered answer call no projection. The dev's "sortability is the only one" holds.

D4 sick-service invariant — right. resolveObjectSchemaMaskPosture still decides exemption before any service call (resolving the posture asks nothing; the asked === 0 pin is kept). The runtime asker is attached only for a capability-exempt caller when an ask exists; it is lazy (invoked only by resolveObjectSchemaRuntimeView, and only for a document with a fields record), memoised (settled ??= settle(), so at most one call per posture), and it never rejects: await ask() sits inside try, and a throw and a non-array answer both return undetermined with one warn. The exempt caller's definition read cannot become a 5xx, so ADR-0106 D4's exempt caller "cannot be turned into an error by a sick security service" holds; D6 tier 3 (throw answers 5xx) stays the non-exempt caller's posture, untouched. Cost note: an exempt item read that serves sortability now pays one security call it did not pay on main; D4's text constrains faults, not latency.

Caching / D3 — right.

  • Unrestricted exempt caller: withheld is empty, so fingerprint is '', and foldVisibilityFingerprintIntoEtag(etag, '') returns etag unchanged: the ETag is byte-identical to main. Pinned ("v1" and its 304) and measured (39ce56dc after unbinding the set).
  • An exempt caller and a projected caller denied the same field: the definition mask hashes the bare name, the runtime view hashes @runtime: plus the name, and @ occurs in no machine name, so the two validators cannot collide. Pinned (not.toBe) and measured (39ce56dc~3c4b8092 against 39ce56dc~32c59ec2).
  • No stale 304: maskApplies is true for a posture carrying runtime, so the protocol is not handed If-None-Match and the 304 is judged below against the triple-folded value (visibility, runtime view, intake). A sick-service transition in either direction moves the validator (v versus v~fp), so a client re-fetches rather than revalidating a body of the other state. An undetermined definition posture still answers private, no-store with no ETag. The uncached arm emits no ETag, and the runtime dispatcher serves no conditional arm. The cached document is never mutated (the view is a shallow copy with a new fields).

check:system-context-census — right. The head's only isSystem read in object-schema-fls.ts stays inside isObjectSchemaMaskExempt, the page's anchor; holdsObjectSchemaMaskExemptCapability reads systemPermissions only, and resolveObjectSchemaMaskPosture reads no isSystem. No new elevation read; the predicate exempts the same classes as main for the definition read. Lint & Repo Gates, the gate's job, is green on the head.

Public surface (judged on the exports maps: both packages expose . to dist/index.d.ts; metadata-core's index re-exports object-schema-fls.js whole; rest's index names translateMetaEnvelope and MetaItemAnswerSources).

  • @objectstack/metadata-core: resolveObjectSchemaRuntimeView (new function) — right. ObjectSchemaRuntimeView (new interface) — right. ObjectSchemaRuntimeFieldPosture (new type) — right. ObjectSchemaMaskPosture's passthrough member gains an optional runtime (a function answering a promise of the field posture) — additive: a consumer constructing a posture is unaffected; a consumer comparing an exempt posture with toEqual now sees a function (the one in-repo pin moved to toMatchObject). Right. RUNTIME_VIEW_FINGERPRINT_QUALIFIER, holdsObjectSchemaMaskExemptCapability and runtimeFieldPostureAsker are not exported; isObjectSchemaMaskExempt and OBJECT_SCHEMA_MASK_EXEMPT_CAPABILITIES are unchanged. Nothing narrowed.
  • @objectstack/rest: translateMetaEnvelope gains an optional fifth parameter with a default — right. MetaItemAnswerSources.translateEnvelope gains an optional third parameter — right (an implementer with two parameters stays assignable). RestServer.translateMetaEnvelope and metaItemAnswerSources are private, so their signature changes are not in the published declaration. Nothing narrowed.
  • @objectstack/runtime: no declaration change (answerMetaItem is module-internal). Right.
  • Wire: GET /meta/object/:name and the dispatcher's /metadata/object/:name keep their shape; for a restricted D4-exempt caller the sortability.fields map loses the withheld entries (absence already means "no platform sort behind this name") and the cached ETag gains a fold. No query parameter, route, or request accept-set change. @objectstack/spec: ObjectSortabilitySchema's shape is unchanged; .describe() text only.

② Semver level

  • .changeset/22250-meta-fls-runtime-sortability.md: @objectstack/metadata-core minor (new exported symbols plus an optional member) — right. @objectstack/rest minor (optional parameters on an exported function and an exported interface) — right under the WHICH LEVEL rule (a fix( that widens an index is minor). @objectstack/runtime patch (behaviour only; no declaration moved) — right. Pre mode is on (.changeset/pre.json, tag next), which does not change the grades; the fixed group applies the highest bump to every member.
  • Clause-②: yes (widening) stands in the PR body and in the changeset body and matches the claim's edited line. Both arms of the entry gate hit (a packages/spec/src/** path and a yes declaration), which is what this record answers. The check-changeset-no-major level axis is satisfied by two minor grades; nothing breaking, so no ADR-0087 disposition is owed.
  • No @objectstack/spec entry. Three published spec artifacts do change content: src/api/sortability.zod.ts ships in the tarball (files), dist carries the new .describe() string, and json-schema/ (gitignored, built at publish) will carry the new description. But no shape, key, enum or accept set moves, so no minor is owed and no Clause-② arm arises from spec; the fixed group republishes @objectstack/spec at the same bump regardless; and no gate requires an entry (Check Changeset, check-empty-changeset and the level axis are all green). Judged right for the level. Completeness note for the dispatch seat's ACCEPT, since changeset prose is outside this record's face: the changeset body names no spec file, so the compiled release notes will not say the projection's published description was amended; a "@objectstack/spec": patch line, or one sentence in the existing body, closes that. Not a FAIL condition.

③ Boundary flags

  • Round-1 Q1 (claim no versus measured yes) — answered by the seat as option A; right. The shared normaliser belongs in metadata-core so the two transports cannot drift; two transport-local copies would be the two-implementations shape the skill refuses.
  • Round-1 Q2 (amend the spec text?) — answered as option A, riding this PR with packages/spec lifted for text only and declared cross-lane on [PM seat] domain:spec — ⏳ vacant #6017 (6062064959, which names the file and "text only"); no objection posted on metadata read: for a platform-admin caller the object read and the sortability projection keep a field that caller's field-level permission marks unreadable, while the data route refuses it #22250. Right. The resolveObjectSortability docblock amendment the declaration did not name by symbol is inside the declared file and is text only — covered.
  • Spec text, per caller class (security service healthy, mask enabled): member — the served map already excludes the withheld field and the projection equals resolveObjectSortability(item): true. Platform admin and organization admin (capability-exempt) — served the field in item, no entry: true, pinned on both transports and both arms. isSystem — resolveProjectionFieldMask answers all fields for a system caller, so nothing is withheld and no entry is missing: true. Nothing else the text says changes: cases 1 to 4 keep their verdicts, the absent-name list only gains a member, and resolveObjectSortability still judges by the spec's own predicates — it is handed a narrower document, it does not read a caller. Two deployment postures, not caller classes, fall outside the sentence exactly as they fell outside the previous sentence: under the D8 escape hatch the whole metadata plane is unmasked by choice, projection included; under a sick service (D6 tier 2, or the D4 asker settling undetermined) the projection is served unprojected with one warn. The spec text describes the enforced posture; the degraded ones are stated in metadata-core's docblocks and in the changeset's "Failure posture". Not escalated.
  • Generated docs: protocol.mdx's table row is the .describe() string joined verbatim; sortability.mdx's case 5 and amended case 1 are the docblock's lines verbatim. check:docs runs inside the green TypeScript Type Check job, which is the mechanical proof neither was hand-edited.
  • Acceptance note 1 (objectui column-sortability.ts docblock): another repository; the sentence is present at the pinned sha a58626c8; the consumer contract (absence means no sort) is unchanged, so the objectui code stays correct and only its comment goes stale; a carrier is named. Correctly not this PR's; not made worse.
  • Acceptance note 2 (maskingRule fields listed sortable): pre-existing for every class on main (getQueryableFields holds a partially masked field readable but not queryable, while sortability keys on readability). This PR keys the runtime view on the same getMetadataReadableFields the mask uses, so the exempt class now behaves exactly as the member already did. Read from source, and no in-repo app declares a maskingRule, so there is no repro to file; Prime Directive chore: version packages #10 puts it in the notes. Correctly not this PR's; not made worse.
  • Acceptance note 3 (ADR-0015 external injected columns re-added by unprovisionedInjectedColumns): pre-existing for every class; for an exempt caller the narrowed view now meets the same re-add path the member's masked document already met. Correctly not this PR's; not made worse. The carrier being "none" is a weakness of the note, not a defect of the diff.
  • Dev deviations: the door table measured at 8b412a83e2 was re-measured on this head with no differing cell — closed. Ablation B's void and refused attempts are disclosed and only the third reading is cited — accepted. The pnpm test -- --maxWorkers=2 misspelling ran the whole package — harmless. No labels written. main was not re-merged after 9865012548, on a clean merge-tree with zero file overlap — the queue rebuilds on current main anyway. Case 5 cites the card number inside a code docblock that renders into the reference page — the repo's bracketed-number code-comment convention; check:doc-authoring and check-issue-citations are green.
  • Predicate nuance, noted, not escalated: the runtime docblock says "never isSystem", but the asker is attached to any context isObjectSchemaMaskExempt admits that also holds an exempt capability, isSystem included. Every system context in core, runtime, rest and plugin-auth carries no systemPermissions, and for a system caller the service answers all fields, so such a context would withhold nothing and warn nothing — one extra service call at most.
  • CI (check-runs on the head, latest per name): TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL), Lint & Repo Gates and Governed Surface Queue Guard all read success. Every other check reads success; the body-scoped checks (Check Changeset, the three claim checks, Part-of PR must not also close its card) re-ran after the seat's body write and read success; Auto Label, Check PR Size, Console Pin Gate and the opt-in tarball smoke read skipped, not red. No red check. The head is on the base repository (no fork), 813 changed lines, and no governed path is in the file list.

Implemented-by: claude/issue-22250-meta-fls-mask-admin
Reviewed-by: session_01EUBvqtauTDmHi2ZgY759p2

VERDICT: PASS


Generated by Claude Code

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/l tests tooling

Projects

None yet

2 participants