Skip to content

perf(rest): GET /data/OBJECT no longer computes metadata diagnostics; the exposure gate reads getMetaItemForExecution - #24666

Merged
objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-24535-gate-execution-read
Oct 20, 2026
Merged

objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-24535-gate-execution-read

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Fixes #24535
Clause-②: yes (widening): measured on each package's built dist/index.d.ts at f89d29e38 (before) and at this head (after); see "The export reading" below. @objectstack/metadata-protocol keeps its export list (131 names), and ObjectStackProtocolImplementation gains the public method getMetaItemForExecution. @objectstack/rest keeps its export list (81 names), and RestProtocol gains the optional member getMetaItemForExecution?. Every other member signature is unchanged in both.

The API-exposure gate on the /data doors (enforceApiAccess) reads the object it judges by name, and it uses only enable. It read through the served getMetaItem, which decorates the item with _diagnostics: a full schema safeParse of the object's definition, paid on every GET /api/v1/data/OBJECT. The card measured this at 1.84 ms of 15.60 ms CPU per HotCRM read. The by-name read now has an execution twin, as the list read already had. The gate reads that twin, and no served read changes.

The factoring: what the two reads share and the one place they differ

packages/metadata-protocol/src/protocol.ts:

  • getMetaItem(request) and the new getMetaItemForExecution(request) each fold the type at their own boundary (canonicalizeMetaRequestType), as getMetaItems and getMetaItemsForExecution do. Each then calls one private body, readMetaItemFace(request, face). That body is getMetaItem's old body, moved: the disabled-package refusal (404 OBJECT_PACKAGE_DISABLED), then readMetaItem(request, { face }), then the disabled-package withholding.
  • readMetaItem's option servedRead?: boolean (from Authenticated data requests issue ~20 DB queries in ~17 sequential legs — per-request auth/session/localization/metadata resolution has no cross-request caching, costing ~1.5s/request on remote Postgres #10757 leg 6) becomes face?: 'served' | 'execution'. With either face, the item's active rows are read through the overlay cache (readItemActiveRows(..., cached)). The cache key is { type, name } and does not depend on the face, so both faces share one entry. Authoring callers pass no face. They read the store, decorated, as before.
  • The one place they differ is the serving layer at the read's exits. serveItem is decorateMetadataItem (_diagnostics computed on the raw body, then the type's redaction) on the served face, and the identity on the execution face. It applies at all three exits: the draft-preview return, the strict-draft return and the active return. The active exit also adds two things only on the served face: a view's cross-document reference diagnostics (merged into _diagnostics) and registration. These follow the list read's precedent, whose execution face skips decorateMetadataItems and registrations. Every source, merge, lock, version token and governance step runs on both faces.
  • Naming and the warning follow getMetaItemsForExecution: ⛔ never serve what this returns from a door that answers a caller (HTTP, MCP, an export, a log line). A gate inside such a door may read a decision off it, as the exposure gate reads enable, but no byte of the body may reach the response.

packages/rest:

  • rest-server.ts: RestProtocol = DataProtocol & MetadataProtocol & { getMetaItemForExecution?: MetadataProtocol['getMetaItem'] }. enforceApiAccess asks getMetaItemForExecution when it is a function, else getMetaItem, else the list (the existing fallback). No other caller of getMetaItem changes.
  • meta-item-read-gate.ts is inside packages/rest, so it is this lane's file. Only two doc comments change: getObjectItem and loadObjectItem now say that REST hands in the execution face. loadObjectItem's contract is unchanged, because the execution face answers the same envelope and the same refusals:
    • the { item } envelope;
    • the name must match;
    • a throw fails open and is logged;
    • OBJECT_PACKAGE_DISABLED means no object.

The parity proof

  1. Permanent: packages/metadata-protocol/src/protocol.item-execution-read.test.ts (9 tests). In each row, decorateMetadataItem(type, execution.item) equals served.item: the served item minus _diagnostics and redaction. Every other envelope key is equal except registration. Each row also asserts that the served item is decorated, so the equality cannot hold vacuously.

    • A code item.
    • An environment-overlay item, read through ONE overlay-cache entry for both faces. The served read fills the entry, and the execution read is a hit: one store read. A write retires the entry for both.
    • A redacting type (datasource). The execution item carries the stored password; the served item does not.
    • The strict draft read.
    • The draft-preview read, with and without a draft.
    • NO_DRAFT on both faces.
    • OBJECT_PACKAGE_DISABLED on both faces, with the same message.
    • A missing name.
    • registration, which only the served face carries.
  2. One-off proof that the served body is byte-identical to before. The previous build (dist at f89d29e38) and this head's build were run over identical hosts, and the JSON answers were compared byte for byte. The matrix was:

    • 4 hosts: unscoped, environment-scoped, with the overlay-cache seam, and a disabled package;
    • 3 served entry points: getMetaItem, getMetaItemCached, and the authoring readMetaItem;
    • 20 requests;
    • 2 passes each, cold and warm.

    480 answers, 0 mismatches. The requests cover:

    • object: code, plural spelling, extended by another package, stored, stored and invalid, package-scoped;
    • drafts: strict draft, draft preview, missing draft;
    • a missing name;
    • a disabled package's object (refused) and its view (withheld);
    • datasource (redaction), view (reference diagnostics), and flow (registration and drafts).

    Two normalisations were applied, both outside the change:

    • Each module instance keys version tokens under its own random process key, so the host supplies one deterministic keyed digest.
    • getMetaItemCached stamps lastModified from the wall clock, so the clock is frozen.

    Control: the same harness comparing the old served face with the NEW execution face differs on 16 of 20 answers. Those are exactly the 16 decorated answers; the 3 NO_DRAFT refusals and the one missing name are equal.

The counting test, its ablation and its controls

packages/rest/src/rest-exposure-gate-execution-read.test.ts (12 tests) drives the real REST route handlers, over the real ObjectStackProtocolImplementation, over a real SchemaRegistry. The code object's data call is the real findData. Every diagnostic of an object goes through one seam, computeMetadataDiagnostics, which calls getMetadataTypeSchema('object').safeParse. For this file, the object binding is replaced through the registry's public overlay, registerMetadataTypeSchema: the same schema behind a proxy that records the name of each document it parses. It is restored afterwards.

  • Count: GET /api/v1/data/OBJECT records zero diagnostics for a code-registered object and for an object stored in the environment, unscoped and environment-scoped. A request the gate refuses (405, verdict taken from the stored row's enable) also records zero.

  • Fallback (in-test control on the same /data path): a protocol with no execution face is asked getMetaItem. The answers are the same, and the test records exactly one diagnostic of that object. This row keeps the zeros from being vacuous: the instrument does see the gate's read when the read decorates.

  • Control: GET /api/v1/meta/object/NAME still serves _diagnostics, and the instrument records it, for both objects and both scopes.

  • Ablation (one-off): run with scripts/ablation-replace.mjs against the committed head 32d829ffc. It replaced const byName = typeof p.getMetaItemForExecution === 'function' with const byName = false, so the gate read getMetaItem again (anchor count 1 to 0, blob 1aa8eeb5a2b2 to 68a515a0f7c2). Result: 6 failed, 6 passed.

    • The 6 zero-count rows went red, with expected [ 'code_widget' ] to deeply equal [] and expected [ 'stored_widget' ] to deeply equal [].
    • The fallback and control rows stayed green.
    • Restored: blob equals HEAD (1aa8eeb5a2b2), and git diff HEAD is empty.
  • Three pins in the rest: every /data request's API-exposure gate reads EVERY object's metadata (with a full schema diagnostic) to check ONE object's enable flags — 62-72% of a data request's time, measured #24031 file (rest-exposure-gate-one-object-read.test.ts) named the face the gate reads, and now name the execution face: the by-name spy, the throwing read, and the "no by-name read" case, which now means neither face.

  • Cost reading (optional; shared box, so read the ratio). Measured on the built dist at 32d829ffc: an object with N fields, a warm overlay cache, median per-read time:

    N fields served getMetaItem getMetaItemForExecution ratio
    10 390.9 µs 41.5 µs 0.106
    60 1028.9 µs 32.2 µs 0.031
    200 2723.7 µs 63.1 µs 0.023

The export reading (Clause-②)

  • Method: the TypeScript compiler API over each package's built entry dist/index.d.ts. Every export is listed with its kind, and every member of every exported class, interface and type alias is listed with its full untruncated signature. Lines are sorted, and the before and after lists are diffed order-insensitively. This was not a grep of a barrel line.
  • @objectstack/metadata-protocol: 131 exports before and after, with the same names. The diff is exactly two added lines: ObjectStackProtocolImplementation member getMetaItemForExecution (the request type and the return type are the same as getMetaItem's) and member private readMetaItemFace. getMetaItem's signature line is identical.
  • @objectstack/rest: 81 exports before and after, with the same names. The diff is exactly one added line: RestProtocol member getMetaItemForExecution?: ....
  • Changesets: both packages are graded minor, not patch. A Clause-②: yes takes at least minor (AGENTS.md, Post-Task Checklist step 3), and check-changeset-no-major.mjs's level axis refuses yes when every moved package is patch. Both published surfaces widen, so both are graded.

Acceptance notes

  • scripts/engine-double-contract.pinned.json gains two rows: the two new test files' pinned findOne doubles. check:engine-double-contract prescribed --write for these rows.
  • The other in-process consumers of the served by-name read that act on a definition rather than serve it are the seed loader's getObject adapter and the post-publish build probes' getItem. They run per publish, not per request, and are left on the served face. Carrier: none.

Verification at head 8f2b83379 (round 0; see Patch round 1 for the current head)

All of the following ran after the last commit, 8f2b83379. Every exit code was captured before any pipe.

  • Typecheck: pnpm --filter @objectstack/metadata-protocol typecheck exit 0, and pnpm --filter @objectstack/rest typecheck exit 0. The rest typecheck includes check:test-typecheck over the test layer: 0 errors.
  • Tests, every project of both packages:
    • pnpm --filter @objectstack/metadata-protocol test: 250 files passed, 3 skipped; 20780 tests passed, 19 skipped.
    • pnpm --filter @objectstack/rest test (local): 301 files passed; 6107 tests passed, 326 skipped.
    • pnpm --filter @objectstack/rest test:repo: 5 files passed; 278 tests passed, 18 skipped.
    • The first full metadata-protocol run, at a691f190f, turned protocol.read-verb-canonical-fold.test.ts red. That pin derives its population of /meta request-boundary verbs from the code, and the new face was not yet in it. b163268a0 makes each face fold its type at its own boundary, as the list faces do, and registers the seventeenth verb.
  • Lint: eslint --no-inline-config over the seven changed source and test files exited 0, and pnpm lint (the full union) exited 0.
  • Derived gate families: node scripts/pm/dispatch-gates.mjs --commands derived 78 commands. All 78 exited 0. --ran reports "78 derived, 78 run, 0 NOT-MEASURED, 0 UNRUN".
    • An earlier battery, at a691f190f, turned check:engine-double-contract red because the ledger lacked the two new pinned doubles. They were recorded at 8f2b83379, and the gate is green.
  • Changeset level axis, driven offline with this body (check-changeset-no-major.mjs --event): clean, because Clause-②: yes (widening) is carried by minor grades. On a throwaway local probe commit grading both packages patch, the same command exited 1 and named both lines.
  • Not run locally: families that only CI runs (the wide-population and artifact-roster families). Their verdicts are in this PR's check runs.

Patch round 1 (head a132ad0cf)

  • Red on 8f2b83379: Test Core (6/6). packages/runtime/src/domains/data-write-request-gate.test.ts failed 12 tests: every locked and hidden row answered 400 VALIDATION_FAILED. Its rig gives RestServer a real ObjectStackProtocolImplementation with no metadata store, and it answered only getMetaItem from the registry. The exposure gate now reads getMetaItemForExecution, which the rig left unstubbed, so the gate failed open and the body gate answered first.
  • Fix: a test-only rider in packages/runtime, declared in the seat's REWORK. The rig installs its one registry-backed by-name stub on both faces, and its note names both. Reverse check: with the stub on getMetaItem alone, the file fails 12 of 73, which reproduces the CI red; with both faces stubbed, 73 of 73 pass.
  • Test census: 18 test files build a real protocol and override getMetaItem. Three of them reach RestServer's exposure gate on a real instance: this rig (fixed) and this PR's two exposure-gate files (already on the execution face). The other 15 never mount that instance in RestServer behind a /data door. Beyond those, 98 test files override getMetaItem and name the REST layer; the rest of them hand RestServer an object-literal protocol, which has no execution face, so the gate still asks their override.
  • Production census: nothing in this repository reassigns getMetaItem on a protocol instance or subclasses ObjectStackProtocolImplementation, and RestApiPlugin hands RestServer the kernel's protocol service unwrapped. objectstack-ai/cloud is not measured here.
  • Merged origin/main: e47f72d38, a merge commit, clean.
  • Verification at a132ad0cf:
    • @objectstack/runtime: typecheck exit 0. test: 425 files; 6678 passed, 19 skipped. test:repo: 3 files; 808 passed.
    • @objectstack/metadata-protocol: typecheck exit 0. test: 250 files passed, 3 skipped; 20790 tests passed, 19 skipped.
    • @objectstack/rest: typecheck exit 0. test: 301 files; 6107 passed, 326 skipped. test:repo: 5 files; 278 passed, 18 skipped.
    • Lint: eslint over the changed files and pnpm lint, exit 0.
    • Derived gates: 78 of 78 exit 0; --ran reports 0 NOT-MEASURED and 0 UNRUN.
    • CI on a132ad0cf: every check green, including Test Core (6/6).

Generated by Claude Code

…execution face

The by-name read gains an execution twin, getMetaItemForExecution, sharing
every source, the overlay cache, merge, lock and governance step with
getMetaItem and skipping only the serving decorations at the exits. The
gate in enforceApiAccess reads it where the protocol has it, so a /data
request no longer pays a full diagnostics safeParse of the object.

Claude-Session: https://claude.ai/code/session_01SYEJqDL3NXxrJRBVqaMu9T
Co-authored-by: Claude <noreply@anthropic.com>
…d minus its decorations

Claude-Session: https://claude.ai/code/session_01SYEJqDL3NXxrJRBVqaMu9T
Co-authored-by: Claude <noreply@anthropic.com>
…e reads the execution face

Claude-Session: https://claude.ai/code/session_01SYEJqDL3NXxrJRBVqaMu9T
Co-authored-by: Claude <noreply@anthropic.com>
…own boundary

As getMetaItems and getMetaItemsForExecution do: the shared body takes a
request already folded. The read-verb fold population pin gains the new
verb (seventeen).

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

github-actions Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/metadata-protocol, @objectstack/rest, touching 16 documentable anchor(s).

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

  • content/docs/concepts/metadata-lifecycle.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class), getMetaItem (symbol, a method of class ObjectStackProtocolImplementation), getMetaItem (literal, a string literal in getMetaItemForExecution))
  • content/docs/data-modeling/drivers.mdx (via getMetaItem (symbol, a method of class ObjectStackProtocolImplementation), getMetaItem (literal, a string literal in getMetaItemForExecution))
  • content/docs/deployment/environment-variables.mdx (via getMetaItem (symbol, a method of class ObjectStackProtocolImplementation), getMetaItem (literal, a string literal in getMetaItemForExecution))
  • content/docs/kernel/contracts/metadata-service.mdx (via getPublished (sdk, the bare tail of client method meta.getPublished, bound to GET /api/v1/meta/:type/:name/published; the bare tail of client method meta.getPublished, bound to GET /meta/:type/:name/published))
  • content/docs/kernel/services-checklist.mdx (via getMetaItem (symbol, a method of class ObjectStackProtocolImplementation), getMetaItem (literal, a string literal in getMetaItemForExecution))
  • content/docs/protocol/objectql/state-machine.mdx (via /object/:name/state/:field (route, bridged from symbol getMetaItem — its route source's handler names it))

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

  • content/docs/releases/v15.mdx (via RestProtocol (symbol, a top-level type))
  • content/docs/releases/v16.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/releases/v17/17-0.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class), getLegalNextStates (sdk, the bare tail of client method meta.getLegalNextStates, bound to GET /api/v1/meta/object/:name/state/:field; the bare tail of client method meta.getLegalNextStates, bound to GET /meta/object/:name/state/:field), meta.getLegalNextStates (sdk, the route ledger binds it to GET /api/v1/meta/object/:name/state/:field, selected by route anchor /object/:name/state/:field; the route ledger binds it to GET /meta/object/:name/state/:field, selected by route anchor /object/:name/state/:field))
  • content/docs/releases/v17/17-2.mdx (via getLegalNextStates (sdk, the bare tail of client method meta.getLegalNextStates, bound to GET /api/v1/meta/object/:name/state/:field; the bare tail of client method meta.getLegalNextStates, bound to GET /meta/object/:name/state/:field), meta.getLegalNextStates (sdk, the route ledger binds it to GET /api/v1/meta/object/:name/state/:field, selected by route anchor /object/:name/state/:field; the route ledger binds it to GET /meta/object/:name/state/:field, selected by route anchor /object/:name/state/:field))
  • content/docs/releases/v17/17-3.mdx (via /:type/:name/published (route, bridged from symbol getMetaItem — its route source's handler names it))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 cross-cutting symbol(s) contributed no route anchor: enforceApiAccess (9 routes)
  • 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 — 26 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 eadea572082976b747e0828d56c24fdf62cd8803 → packageMentionDocs.

Which tree this was computed on

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

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

⚠️ 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 eadea572082976b747e0828d56c24fdf62cd8803 → 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

席位核对 — ACCEPT (the lane seat's review of record for a Clause-②: yes declaration)

domain:cli seat 1 (seat post #6024) · zhuangjianguo · session session_01SYEJqDL3NXxrJRBVqaMu9T · 2026-10-19T22:11Z · on head 8f2b83379 · #24535 (p1, maintainer-directed 「24535 你可以处理」; claim 6277236465, report 6278478154). This is the seat's own review at the default tier, read against the diff on GitHub, not the report. For a Clause-②: yes declaration with no published schema or governed rules text, it is the review owed.

  1. What moved, read on the compare f89d29e38...8f2b83379:
    • In protocol.ts, getMetaItem and the new getMetaItemForExecution each fold the type at their boundary and call one private body, readMetaItemFace, which holds the old getMetaItem body (the disabled-package refusal and the withholding).
    • readMetaItem's option servedRead becomes face. Both API faces read the active rows through the ONE overlay-cache entry, and an authoring caller passes no face and reads the store, as before.
    • The faces differ only at the exits. serveItem is decorateMetadataItem on the served face and the identity on the execution face. A view's cross-document diagnostics and the flow registration are served-only, following the list execution face's precedent.
    • In rest-server.ts, RestProtocol gains an optional getMetaItemForExecution?, and enforceApiAccess prefers it, then getMetaItem, then the list.
  2. The undecorated body is not served anywhere: checked by the seat.
    • The execution face also skips credential redaction. The seat read it: a datasource's password is present on that face only, as on getMetaItemsForExecution.
    • Its docblock forbids any door from serving it, and RestProtocol's docblock repeats that.
    • The only reader is the exposure gate, which takes obj.enable and returns no byte of the item. No other caller of getMetaItem changed.
    • The dev noted the seed loader's getObject and the build probes as per-publish, zero-pull readers left on the served face. Acceptance notes.
  3. Served reads are byte-identical: the previous build was compared against this head across 480 JSON answers (4 hosts × 3 served entry points × 20 requests × 2 passes), with 0 mismatches. As a control, served-old against execution-new differs on exactly the decorated answers.
  4. Tests and reverse verification:
    • rest-exposure-gate-execution-read.test.ts (12 tests) counts every computeMetadataDiagnostics safeParse through a recording schema proxy. GET /api/v1/data/OBJECT records 0 (code and stored objects, unscoped and environment-scoped, and a refused 405). The fallback protocol records exactly one, and GET /meta/object/NAME still serves _diagnostics.
    • Ablation (byName = false): exactly the 6 zero rows go red. The restore was proven by blob.
    • protocol.item-execution-read.test.ts (9 tests) checks parity: decorateMetadataItem(type, execution.item) equals the served item across a code item, an overlay item through one cache entry (with write-retire), a datasource, strict draft, preview, NO_DRAFT, OBJECT_PACKAGE_DISABLED and a missing name.
    • Full suites: metadata-protocol 20,780 passed; rest local 6,107 and test:repo 278 passed. Typecheck exited 0 for both.
    • Cost: the gate's read takes 41–63 µs against 391–2,724 µs served, from 10 to 200 fields.
  5. Shape:
    • Fixes #24535, Clause-②: yes (widening) measured on each package's built dist/index.d.ts (export lists unchanged; one public method, one optional member).
    • minor changesets for both packages, per AGENTS.md's Clause-②: yes floor (the dev graded them up from the dispatch's patch, correctly).
    • 10 files, +653 / −30. Not governed.
    • packages/spec/** and metadata-diagnostics.ts are untouched.
  6. Beyond the named surface, each justified: the read-verb fold population pin registers the new verb; engine-double-contract.pinned.json gains the 2 rows its gate prescribed; meta-item-read-gate.ts changes in doc comments only. The protocol.ts surface was declared on [PM seat] domain:engine — ⏳ vacant #6367.
  7. Gates: dispatch-gates ran 78 derived and 78 run, 0 NOT-MEASURED. The full-union pnpm lint exited 0. Landing waits on every check of this head.
  8. Findings: none filed. The per-publish served readers above are Acceptance notes.
  9. Landing: ready plus auto-merge through the queue once every check of 8f2b83379 is green or a rostered skip. The card closes on the merge.

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 — REWORK (patch round 1: Test Core (6/6) is red on this head)

domain:cli seat 1 (seat post #6024) · zhuangjianguo · session session_01SYEJqDL3NXxrJRBVqaMu9T · 2026-10-19T22:38Z · on head 8f2b83379 · #24535 (claim 6277236465). This supersedes the landing half of the ACCEPT 6278511021, which was written before this head's checks converged. The change it reviewed stands. This head is not landable.

What is red: Test Core (6/6), job 118334719467. packages/runtime/src/domains/data-write-request-gate.test.ts fails 12 tests. Each exposure row (locked: 405 OBJECT_API_METHOD_NOT_ALLOWED; hidden: 404 OBJECT_API_DISABLED) now answers 400 VALIDATION_FAILED, on both the RestServer describe and the two-doors parity describe. On main, Test Core is green on every run since f89d29e38; only the Dogfood gate is red there (#24645), so this failure is this PR's.

Cause, read on this head: the rig (bootRestServer, about :195–:205) builds a real ObjectStackProtocolImplementation with no metadata store, and replaces its getMetaItem with a registry-backed stub so the gate can read each object's enable. enforceApiAccess now prefers getMetaItemForExecution whenever the protocol has one. The real protocol has one, and the rig never stubs it. So the gate reads no item, the exposure gate fails open (as the rig note says it would), and the body gate answers first.

Owed in this PR:

  1. Give the rig the face the gate reads: answer getMetaItemForExecution from the same registry stub (or install the one stub on both faces), and name both faces in the rig note at about :66–:73.
  2. Census the other tests that build a real ObjectStackProtocolImplementation and replace getMetaItem while driving RestServer's exposure gate. Each one now falls through to the unstubbed face. A refusal row goes red; an allowed row stays green without exercising the stub, which is the silent half. The seat's grep for a real protocol plus a getMetaItem override returns 11 files, among them packages/runtime/src/http-dispatcher.test.ts, domains/meta-item-query-decision.test.ts and domains/packages-seed-apply-*.test.ts. Fix every one that reaches the gate. List the rest in the report with the reason each does not.
  3. Run packages/runtime's full unit tier (and every package the census touches) locally before pushing, not only rest and metadata-protocol.

Production code: the seat's grep finds no production code that reassigns getMetaItem on a protocol instance, so no wrapper is bypassed by the new preference. Confirm or correct this in the report.

The test files are in packages/runtime (this lane), outside the claim's declared surface (protocol.ts, rest-server.ts and their tests). They are test-only riders, declared here and by the pointer on #24535. The PR body is corrected by the seat from your report's body_edits; do not patch the body.


Generated by Claude Code

…m the registry

enforceApiAccess reads getMetaItemForExecution when the protocol has it,
so the rig's registry-backed by-name stub sits on both faces; with only
getMetaItem stubbed the exposure gate read the unstubbed face, failed
open, and every locked and hidden row answered the body gate's 400.

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

Copy link
Copy Markdown
Contributor Author

席位核对 — ACCEPT (patch round 1)

domain:cli seat 1 (seat post #6024) · zhuangjianguo · session session_01SYEJqDL3NXxrJRBVqaMu9T · 2026-10-20T00:00Z · on head a132ad0cf · #24535 (claim 6277236465; REWORK 6278921779; report 6279996027). This is the seat's own review at the default tier, read against the diff on GitHub. The round-0 ACCEPT 6278511021 stands for the protocol.ts / rest-server.ts change, which this round does not touch.

  1. The red is fixed at its cause: data-write-request-gate.test.ts's rig now installs its one registry-backed by-name stub on both faces, getMetaItem and getMetaItemForExecution, and its note names both. The compare e47f72d38...a132ad0cf touches that one test file and nothing else. The reverse check (stub on getMetaItem alone) reproduces CI's 12 failures out of 73; with both faces, 73 of 73 pass.
  2. Census, as owed:
    • 18 test files build a real protocol and override getMetaItem. Three reach the exposure gate on a real instance: this rig (fixed) and this PR's two exposure-gate files (already on the execution face).
    • Each of the other 15 is listed with its reason: no RestServer, a /meta door only, or an object-literal protocol, which has no execution face, so the gate still asks its override.
    • The seat's 11-file grep is a subset of these 18, and every file in it is accounted for.
  3. Production census: confirmed for this repository. Nothing reassigns getMetaItem on a protocol instance or subclasses ObjectStackProtocolImplementation, and RestApiPlugin mounts the kernel's protocol service unwrapped. objectstack-ai/cloud is not measured from this container. A host that wraps getMetaItem and mounts that object in RestServer would now have the gate read the execution face. That is an Acceptance note for the release text, not a block: the execution face reads the same sources and governance (round 0, item 2).
  4. Merged origin/main: e47f72d38, a merge commit, clean.
  5. Verification at a132ad0cf:
    • runtime: 6,678 passed; test:repo 808.
    • metadata-protocol: 20,790 passed.
    • rest: 6,107 passed; test:repo 278.
    • Typecheck exited 0 for all three.
    • dispatch-gates 78 of 78, 0 NOT-MEASURED. pnpm lint exited 0.
    • CI on a132ad0cf: every check is green, Test Core (6/6) included.
  6. PR body: the seat applied the round's body_edits (the "Patch round 1" section, and the round-0 heading qualified). Read back identical.
  7. Landing: ready plus auto-merge through the queue once every check of a132ad0cf is green or a rostered skip; checks may re-run after the body edit. Fixes #24535 closes the card on the merge.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 20, 2026 00:03
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 20, 2026 00:03
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 20, 2026
Merged via the queue into main with commit 6464df3 Oct 20, 2026
48 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-24535-gate-execution-read branch October 20, 2026 00:33
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