Skip to content

feat(spec): enable.aiReadable — a system object's author can open its records to the AI read tools, read-only - #32971

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-lgpfso
Nov 13, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-lgpfso

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread

Fixes #32865

Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: follow-up #32974 — skills/objectstack-data/SKILL.md (the enable capability table has no aiReadable row; it should say the key is refused on any object whose name does not start with sys_, so an app author never writes it)

维护者速读

  • 改了什么:对象的 enable 块新增 aiReadable(只读的 AI 开放声明)。只有系统表(sys_ 开头)可以声明,声明后 AI 的读工具(查询、取单条、聚合、描述、列表)可以读它的记录,权限和行级安全照旧;它永远不开放写,写仍只走动作自己的 ai.exposed。
  • 为什么改:你在 [Decision] Should an approver be able to see and decide their pending approvals over MCP? #32442 裁定 A。今天 MCP 读工具按名字拒绝所有系统表,经理没法问 AI「我有哪些待审批」。本 PR 只做规格那一半(键的形状与规则);让审批请求表真正可读的服务端那一半在 [Decision] Should an approver be able to see and decide their pending approvals over MCP? #32442。
  • 风险与回滚:只是新增,旧元数据解析结果不变。业务对象上写这个键会在解析时被拒(它在业务对象上什么都不改,写 false 会让人误以为锁住了);sys_metadata、sys_metadata_history 两张存元数据正文的表一律拒绝。运行时还没有任何地方读它。回滚即 revert 本 PR。
  • 先看:packages/spec/src/data/ai-read-exposure.ts(拒收与唯一读法 isAiReadExposed,元数据表先于键判断);packages/spec/src/data/object.zod.ts 中 aiReadable 键与写臂提示;packages/spec/src/data/ai-read-exposure.test.ts。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run src/data/ai-read-exposure.test.ts,应见 17 个测试全绿。
  • 需要你做的:无,席位落地。

What changes

@objectstack/spec gains a per-object, read-only AI exposure declaration, executing the maintainer's ruling A on the read half of #32442 (ruling record 6837430865).

  • ObjectCapabilities.aiReadable (enable.aiReadable, optional boolean, absent by default). true declares that the AI read tools (query, get one, aggregate, describe, list) may reach this system object's records. Every gate after admission is unchanged: the caller's permissions, row-level security, field masking, and the object's API exposure. It never opens a write; a write on a system object stays on its actions, each opened by its own ai.exposed (ADR-0011). No deployment switch is added, and allowSystemObjects is untouched.
  • Parse refusal in ObjectSchema's existing refinement (refuseMisplacedAiReadExposure → aiReadExposureRefusal), so every door that parses an object inherits it (ObjectSchema.create, defineStack, os validate, the object save door):
    • the stored-metadata family is judged first. The list is STORED_METADATA_BODY_OBJECTS (sys_metadata, sys_metadata_history) in packages/spec/src/kernel/stored-metadata-body-objects.ts, the one list the action surface already refuses by (ADR-0066 D4, note of 2026-10-30). It is imported, not copied;
    • any object whose name does not start with sys_ is refused: the MCP record tools already read a business object as the caller, so the key would change nothing there, and false would read as a lock that locks nothing (ADR-0049);
    • any declared value is judged, false included. The key is new, so no stored object carries it.
  • No write arm. enable declares no other AI key. aiExposed, aiWritable, aiWrite, aiEditable and ai inside enable are refused with a read-only prescription that points writes at actions' ai.exposed; aiRead, aiReadEnabled, aiReadExposed and mcpReadable get a rename to aiReadable. A value that is not a boolean is refused.
  • isAiReadExposed(schema), exported from @objectstack/spec/data, is the one reading for the server half. It answers false for the stored-metadata family before it reads the key, false off a sys_* object, and true only for the boolean true (fail-closed, the shape of isPublicSharingEnabled).
  • Liveness ledger: object.enable.aiReadable is planned until the server half lands. The Studio object form omits the key (reconciliation ledger row), because the objects that form edits are the ones the parse refuses it on.

Placement and mechanism (measured; for the contract review)

  • enable rather than a new top-level ai block. The MCP layer already reads an object's enable block for its record tools: packages/mcp/src/stdio-data-bridge.ts gates every data verb on enable.apiEnabled / apiMethods, and both bridges carry enableFeatures: def.enable into the object description (packages/mcp/src/mcp-server-runtime.ts, packages/runtime/src/domains/mcp.ts). The new key sits beside the exposure flags it is read with, and enable already holds a read-only opt-in of the same kind (approvalsVisibleToReaders). A top-level ai block would add a second home for object exposure, and its exposed would read as full exposure.
  • The name. aiReadable keeps the ai prefix and states the read-only scope in the name. The exact action spelling, aiExposed, is refused with guidance, because on an object it would read as write exposure too.
  • Parse instead of a packages/lint rule. The card says the off-sys_* declaration is "refused by lint". A parse refusal is a strict superset: os validate reports parse issues, and the parse also covers defineStack, module load and the object save door, which a lint rule does not reach. This follows the precedents in the same refinement (refuseWriteLockedUserActions, refuseExternalBindingOnPrimaryDatasource). The claim's file surface was amended accordingly (claim re-post 6839889768). The contract review should confirm this mechanism change.

Tests

packages/spec/src/data/ai-read-exposure.test.ts, 17 cases: accepted on sys_approval_request (true, false, absent); refused on a business object (true and false, located at enable.aiReadable); refused on each member of STORED_METADATA_BODY_OBJECTS (true and false); isAiReadExposed answers false on each family member without reading the key (getter probe, with a control leg on a non-family system object that is read); no write arm (enable declares exactly one ai/mcp key, the write-arm names are unrecognized_keys, non-boolean values are invalid_type, the reading opens on boolean true only).

Ablations (each rule once; scripts/ablation-replace.mjs with an on-disk anchor proof, EXIT-trap restore, blob == HEAD verified after each; spec tests import src/ directly, so no dist rebuild applies):

Rule ablated Result
A1 refusal off sys_* removed 3 red (both business-object pins, the family-vs-business distinction)
A2 family refusal removed 3 red (both family members, the distinction)
A3 family-before-key in isAiReadExposed removed 2 red (both "without reading the key" pins)
A4 acceptance on sys_* removed 1 red (the acceptance pin)
A5 a write-arm key added to enable 1 red (the one-AI-key pin)

Two existing tests were updated for the new key: alias-integrity.test.ts (the enable guidance table now lists the five write-arm names) and metadata-form-zod-reconciliation.test.ts (an omit ledger row for object.enable.aiReadable).

Acceptance notes

  • For the server half ([Decision] Should an approver be able to see and decide their pending approvals over MCP? #32442, carrier): the attestation should come from isAiReadExposed on the object's registered definition. Whether a tenant object overlay of a sys_* object can carry enable.aiReadable (ADR-0029 D9 overlays) is the server half's to measure; this PR measured only that an object extension cannot carry enable at all (ObjectExtensionSchema is closed and takes no enable).
  • The three runtime copies of the sys_ predicate (packages/mcp/src/mcp-http-tools.ts, packages/runtime/src/action-execution.ts#isSystemObjectName, and the module-private one here) are not unified; spec cannot import either runtime copy. Noted, not filed.
  • After landing, the seat tells [Decision] Should an approver be able to see and decide their pending approvals over MCP? #32442's seat that its Blocked-by: edge has cleared; the merged key is enable.aiReadable and its reading is isAiReadExposed.

Verification

All readings at head 9782f9e7fc (merge of origin/main 2d52e29 into the branch), after a whole-tree turbo run build --concurrency=2 (VERDICT command-exit 0).

Derived gate families — 113 derived, 113 run, all exit 0

node scripts/pm/dispatch-gates.mjs --ran FILE → 113 derived famil(ies) accounted for — 113 run, 0 NOT-MEASURED (a DERIVED zero — all 113 recorded an exit code and none of them is 3). The list covers the @objectstack/spec artifact and source audits (check:api-surface, check:authorable-surface, check:docs, check:liveness, check:strictness-ledger, check:export-origins, check:exported-any, check:generated, …), the changeset gates (check-adr-0087-registration, check-changeset-no-major, check-empty-changeset), check:nul-bytes, check:test-source-alias, check:type-check-coverage, check:type-check-debt (under the verify lock) and the rest of the derivation. Families outside the derivation run in CI and are read from this PR's check-runs.

Package suites
  • pnpm --filter @objectstack/spec test → Test Files 837 passed (837), Tests 26754 passed | 1 todo (VERDICT command-exit 0).
  • pnpm --filter @objectstack/spec typecheck → exit 0.
  • pnpm --filter @objectstack/lint exec vitest run (consumer of the spec contract) → Test Files 200 passed (200), Tests 8579 passed.
  • pnpm --filter @objectstack/spec check:generated → All 14 generated artifacts are up to date.
  • Clause-②: / Metadata-protocol: lines read by scripts/pm/clause2-line.mjs from the changeset: yes (widening), added.

Not run locally: pnpm lint and test:repo (CI's); the dogfood battery (no ! in the title, no (narrowing) arm).

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic (the exported reading is a pure predicate, as isPublicSharingEnabled).
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint is CI's.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English (the maintainer brief is Chinese by fleet rule).

Generated by Claude Code

…a system object

A system (sys_*) object's author may declare enable.aiReadable: true to let
the AI read tools reach its records; it never opens a write. The parse
refuses the key on any non-sys_* object and on the stored-metadata family,
and isAiReadExposed is the one reading, judging the family before the key.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4czFmroo4m96P6zVdd1
@github-actions github-actions Bot added documentation Improvements or additions to documentation protocol:data protocol:system tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 16 documentable anchor(s). ⚠️ 6 changed file(s) yielded no anchor (packages/spec/api-surface/data.json, packages/spec/authorable-surface/data.json, packages/spec/export-origins/data.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/ai/actions-as-tools.mdx (via aiExposed (symbol, a field of const object CAPABILITIES_AI_WRITE_ARM_GUIDANCE))
  • content/docs/ai/connect-mcp.mdx (via aiExposed (symbol, a field of const object CAPABILITIES_AI_WRITE_ARM_GUIDANCE))
  • content/docs/concepts/metadata-driven.mdx (via ObjectSchemaBase (symbol, a top-level const object))
  • content/docs/getting-started/quick-reference.mdx (via ObjectCapabilities (symbol, a top-level const object))
  • content/docs/permissions/system-context.mdx (via isSystemObjectName (symbol, a top-level function))
  • content/docs/protocol/objectql/security.mdx (via ObjectCapabilities (symbol, a top-level const object))

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

  • content/docs/releases/v14.mdx (via ObjectCapabilities (symbol, a top-level const object))

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
  • 6 changed file(s) yielded no anchor (packages/spec/api-surface/data.json, packages/spec/authorable-surface/data.json, packages/spec/export-origins/data.json, …) — pages documenting those are invisible to this run
  • the SDK route bridge reached 65 of 247 client-bound route-ledger rows — the other 182 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 182: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 59 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 123 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 — 171 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 25ee507651f7bae209771fb31661e6edf46f4c8f → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 25ee507651f7bae209771fb31661e6edf46f4c8f

⚠️ 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 25ee507651f7bae209771fb31661e6edf46f4c8f → 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

domain:spec#3 · os-elon-musk · session_016WV4d1T5HEokjRgR6vCBX8

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 9782f9e7fcddd21db4d02371afd79fa8453d2cf9
Local-runs: none
Scope: packages/spec/src/data/ai-read-exposure.ts, packages/spec/src/data/index.ts, packages/spec/src/data/object.zod.ts + packages/spec/src/data/ai-read-exposure.test.ts, packages/spec/src/shared/alias-integrity.test.ts, packages/spec/src/system/metadata-form-zod-reconciliation.test.ts, packages/spec/liveness/object.json, packages/spec/liveness/state-counts/object.md, packages/spec/api-surface/data.json, packages/spec/export-origins/data.json, packages/spec/authorable-surface/data.json, .changeset/32865-object-ai-read-exposure.md (declaration lines only)

Read, not reviewed (outside the diff): packages/mcp/src/stdio-data-bridge.ts, packages/mcp/src/mcp-server-runtime.ts, packages/mcp/src/mcp-http-tools.ts, packages/runtime/src/domains/mcp.ts, packages/runtime/src/action-execution.ts, packages/spec/src/kernel/stored-metadata-body-objects.ts, packages/spec/src/kernel/manifest.zod.ts, packages/spec/src/shared/strict-object.ts, packages/spec/scripts/liveness/check-liveness.mts, packages/lint/src/lint-liveness-properties.ts, ADR-0011, ADR-0029 D9, ADR-0049, ADR-0066 D4 (:78 and its 2026-10-30 note), card #32865 with claims 6839742814 / 6839889768 and report 6840947550, #32442's ruling 6837430865 and decision card 6837228055, the seat order, the PR body. The changeset prose and content/docs/references/data/object.mdx are prose faces: the seat's ACCEPT, not this record. The dev's ablations and suite counts are the dev's readings; nothing was built or run here. Check-runs on this head when read: 15 success, 9 skipped, 21 in progress, none failed. PR author is objectstack-fleet[bot] (relay write).

① Derived judgments

  • Read-only by construction: holds. The key is one optional boolean, aiReadable (object.zod.ts:359); no value can name a write scope. enable declares no other AI key. aiExposed, aiWritable, aiWrite, aiEditable and ai stay refused, each with the read-only prescription (object.zod.ts:243-249, :299). The four aliases (:298) are rename hints on a refusal, not accepted spellings (strict-object.ts:32-34). isAiReadExposed answers reads only (ai-read-exposure.ts:115-121).
  • Clause-② yes (widening): holds. ObjectCapabilities was already closed, so no object that parses on origin/main carries enable.aiReadable or any of the nine newly named keys. The refinement (object.zod.ts:4591) returns early when the key is absent or undefined (ai-read-exposure.ts:78-80), so it can refuse nothing that parses today. The accepted set grows by exactly: a sys_* object outside the family with a boolean aiReadable.
  • Family reused, judged first: holds. ai-read-exposure.ts:3 imports isStoredMetadataBodyObject and STORED_METADATA_BODY_OBJECTS from the leaf module (stored-metadata-body-objects.ts:49,55); no second list. The family is judged before the sys_ test in the refusal (:82 before :91) and before the key is read in the reading (:118 before :120), the same order as action-execution.ts:160-166. Pinned by the getter probe with a control leg (ai-read-exposure.test.ts:78-91).
  • allowSystemObjects untouched, no new switch: holds. The 13 files are packages/spec, one changeset and one generated reference page. No packages/mcp file and no config schema moves.
  • Placement reason verified. stdio-data-bridge.ts:671 gates on enable.apiEnabled; mcp-server-runtime.ts:148 and runtime/src/domains/mcp.ts:624 both carry enableFeatures: def.enable ?? {}. The MCP record tools do read the object's enable block.
  • Name grammar compatible. name is the canonical id (namespace is retired, object.zod.ts:1731-1736), lowercase by regex (:3429), and sys_ is platform-reserved (manifest.zod.ts:561-563). A tenant or package object carries a package prefix, so the refusal reaches it, as intended. ObjectExtensionSchema is closed and has no enable (object.zod.ts:5678 ff.), so an extension cannot carry the key.
  • Pins. The four ordered pins are present: accepted on sys_* (test:27), refused off sys_* for true and false (:47), refused on each family member (:68), no write arm (:103-127). Each rule has one reported ablation (A1 to A5, dev's readings).

② Semver level

Metadata-protocol: added takes at least minor on the package the diff moves (AGENTS.md Post-Task 3). The changeset is '@objectstack/spec': minor. Correct. Clause-②: yes (widening) and Metadata-protocol: added are line-leading in the changeset (lines 7-8) and in the PR body. A Skills-docs: follow-up owed — skills/objectstack-data/SKILL.md line is in both. Not breaking, so no ADR-0087 marker is owed. Three new exports are recorded in api-surface/data.json and export-origins/data.json; one new authorable key in authorable-surface/data.json.

③ Boundary flags

  • For [Decision] Should an approver be able to see and decide their pending approvals over MCP? #32442 (server half), to carry in the seat's hand-off: which definition is attested. ADR-0029 D9.2 resolves an object's base layer as overlay ?? owner, and an overlay body is a whole ObjectSchema body with the sys_* name, so it parses with enable.aiReadable: true. If the tenant save door admits an overlay of a sys_* object, isAiReadExposed(resolved definition) would let a tenant open a system object its author did not open. The server half must either attest from the packaged own contributor or measure that the door refuses such an overlay. The PR body notes this as unmeasured; it does not block a key with no reader.
  • aiReadable: true beside apiEnabled: false on a sys_* object parses and opens nothing (the API gate still closes it). Not judged here; the server half should decide whether to warn.
  • Three copies of the sys_ predicate remain (mcp-http-tools.ts:471, action-execution.ts:131, ai-read-exposure.ts:57). The spec one is now importable by both runtimes; unifying is a follow-up, not this card.
  • The Studio form omits the key (metadata-form-zod-reconciliation.test.ts:220-226). Correct while the form authors business objects only.

④ Design judgment

Open question (parse versus a packages/lint rule): A, keep the parse refusal. Reasons, read in the code:

  • The key is added in this same diff, so the refusal narrows nothing (① above); the usual argument for lint, "do not break stored metadata", does not apply.
  • ADR-0049 forbids a parsed, unmarked, unenforced key. A lint rule leaves defineStack, module load and the save door accepting aiReadable: false on a business object, a lock that locks nothing. Only the parse closes every door.
  • It follows this refinement's own precedents (refuseExternalBindingOnPrimaryDatasource, object.zod.ts:4578) and HookSchema's parse refusal of the same family.
  • It deviates from the card's and order's word "lint". The claim re-post 6839889768 amended the file surface before the PR opened, and the ruling's intent (never declared-but-inert) is met more strictly. Accepted.

enable rather than a top-level ai block: accepted. The reason holds in packages/mcp (① above), enable already holds the exposure flags the key is read with, and an object-level ai.exposed would read as full exposure. The name keeps the ai prefix of ADR-0011 and states the scope; the action spelling aiExposed is refused with guidance. In line.

First consumer. isAiReadExposed(def) gives #32442 one fail-closed attestation in the shape of isPublicSharingEnabled; the export is needed. The key matches the predicate the MCP guards refuse by.

Liveness verdict planned: honest. The checker's own guidance grades a key nothing reads yet as planned ("a consumer is being built against it", check-liveness.mts:1182-1184), and action.json:472 (overrides) is the same note-only precedent. The row claims no reader, names #32442 as the flip, and the state count moves 0 to 1 planned. Under ADR-0049 the hazard is a boundary that reads as enforced and is not; here the restrictive halves (both refusals) are live at parse, and the unenforced half is the opening, which fails closed.

Findings, all nit (no effect on correctness or the contract at this head):

  1. nit: isSystemObjectName is case-insensitive (ai-read-exposure.ts:58) while the family set is case-sensitive, so isAiReadExposed({ name: 'SYS_METADATA', enable: { aiReadable: true } }) answers true, against the docblock's fail-closed claim for unparsed input. Unreachable for a parsed name (object.zod.ts:3429) and the same shape as action-execution.ts:131,166. Dropping the i flag, with one pin, closes it; fold into the next spec touch or [Decision] Should an approver be able to see and decide their pending approvals over MCP? #32442.
  2. nit: the ledger row has no evidence field, so the gate scans nothing for it (check-liveness.mts:549); the refuser is named only in note. packages/spec/src/data/ai-read-exposure.ts#aiReadExposureRefusal as evidence would make the refusal's removal go red. Fold into the flip to live.
  3. nit: the .describe() (object.zod.ts:359) and the reference page say true "lets the AI read tools reach its records" in the present tense, with no planned marker. The changeset discloses that nothing reads it yet. If a release is cut before [Decision] Should an approver be able to see and decide their pending approvals over MCP? #32442 lands, mark the description.
  4. nit: aiReadExposureRefusal and AiReadExposureRefusal are published through export * (index.ts:340) with no named consumer outside the spec. Additive and in line with externalBindingDatasourceRefusal; removing them later would be a narrowing. Accepted deviation.
  5. nit: the one-AI-key pin matches only names starting ai or mcp (ai-read-exposure.test.ts:104); a write arm under another prefix would pass it.

Implemented-by: card thread session_016WV4czFmroo4m96P6zVdd1
Reviewed-by: domain:spec#3 seat (CONTRACT_REVIEW_TIER subagent)

Verdict: PASS

Items: no must-fix, no should-fix; five nits above (final round). Owed outside this PR: the Skills-docs: follow-up card for skills/objectstack-data/SKILL.md, and the overlay-attestation flag in the hand-off to #32442's seat. Landing still waits for the head's check-runs to finish green (21 in progress when read).

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-elon-musk · session_016WV4d1T5HEokjRgR6vCBX8

席位核对 · ACCEPT · PR #32971 · head 9782f9e7fcddd21db4d02371afd79fa8453d2cf9 · 2026-11-13T01:47Z

  • 形态: 13 files, +373/−3, all packages/spec (+ regenerated reference page): ObjectCapabilities gains enable.aiReadable (optional boolean, read-only AI exposure, no write arm), refused at parse (ObjectSchema superRefine) on an object whose name does not start with sys_ and on the stored-metadata-body family (STORED_METADATA_BODY_OBJECTS, reused, judged before the key is read); one reader isAiReadExposed from @objectstack/spec/data; api-surface / export-origins / authorable-surface / liveness rows (planned, reader = [Decision] Should an approver be able to see and decide their pending approvals over MCP? #32442's server half); tests; one changeset. check-governed-merges --pr 32971: NOT governed, 376 changed lines ≤ 3000 → ordinary queue landing.
  • 前提实测: contract review 6841030247 (CONTRACT_REVIEW_TIER, head 9782f9e7, PASS, 0 must-fix, 0 should-fix, 5 nits). Open question (refusal at parse rather than a packages/lint rule, deviating from the order) answered A — keep the parse refusal: the key is new and enable was already closed, so it refuses nothing that parses today, and only parse closes every door (ADR-0049). The reviewed head is the landing head (no push since the review; the seat only rewrote the PR body's Skills-docs line). K on the landing head: 5.
  • 门禁 check-runs on 9782f9e7: 72 runs, 60 success, 12 skipped, 0 failed or pending (read through the API this act).
  • 接受的偏差: review nits 1–5 accepted without a push (each is wording, an unreachable branch for a parsed name, or an export with no outside consumer; none changes the contract face): (1) isAiReadExposed case-insensitive sys_ test vs case-sensitive family set — unreachable for a parsed name; (2) the planned liveness row carries no evidence anchor; (3) .describe() in present tense with no planned marker; (4) aiReadExposureRefusal and its interface exported with no outside consumer; (5) the one-AI-key pin matches ai / mcp prefixes only. The order said a packages/lint rule; the parse refusal is a strict superset (accepted per the open-question answer).
  • 范围外去向: Skills-docs: → skills: the object enable capability table does not teach aiReadable (read-only AI exposure, sys_* objects only) — PR #32971's Skills-docs follow-up #32974 (filed by the seat, domain:skills, Blocked-by this PR; PR body line rewritten). Carried to [Decision] Should an approver be able to see and decide their pending approvals over MCP? #32442's seat (domain:server#5, mailbox pm:mailbox — domain:server#5 · os-elon-musk #32830) at landing: the key name enable.aiReadable / reader isAiReadExposed, and the review's overlay point — ADR-0029 D9.2 resolves overlay ?? owner, so a tenant overlay of a sys_* object could carry the key; the server half attests from the own contributor or measures that the door refuses it.
  • 落地条款: Clause-②: yes (widening), Metadata-protocol: added, @objectstack/spec minor. Contract-review PASS on record, all checks green ⇒ ready + auto-merge now. Fixes #32865.

Seat opinion: land.

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 protocol:data protocol:system size/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec: a per-object, read-only AI exposure declaration so an agent can read a declared system object through the MCP record tools (#32442 read half)

2 participants