Found while implementing #8083 (applying the exposure gate on the stdio MCP data bridge). Filed rather than fixed there: #8083's scope is the bridge, and this is a different seam in a different file.
What differs
#8083 closed the declaration leak on the six object-CRUD verbs, which all flow through createStdioDataBridge (packages/mcp/src/stdio-data-bridge.ts). The ADR-0101 record resource does not use that bridge at all. Its reader is a separate closure built inline in the plugin:
packages/mcp/src/plugin.ts — getRecord = async (objectName, recordId) = ... calls scopedQl.find(objectName, { where: { id: recordId }, limit: 1, context: ec }) directly- handed to
this.runtime.bridgeResources(metadataService, getRecord) - served as
objectstack://objects/{objectName}/records/{recordId}
So after #8083 an object declaring enable.apiEnabled: false is refused by get_record (the tool) and still readable through the record resource, on the same transport, under the same key.
Why it matters
Same grading as #8083, and for the same documented reason: this is a surface-area declaration leak, not an authorization bypass.packages/runtime/src/api-exposure.ts records the gate as a surface-area control rather than the authorization boundary, and this read passes the engine's CRUD / FLS / RLS middleware exactly as before — it goes through ql.find with the key's ExecutionContext. What leaks is the author's exposure declaration. Please do not triage this as a security issue.
Scope note: the gap predates #8034 — the record resource has read rows over ql.find without the gate since #7645, which is the narrower form of the divergence #8083's body records under "Scope note".
Suggested shape (not prescriptive)
#8083 landed the gate as enforceApiExposure inside stdio-data-bridge.ts, taking its decision from the spec's single source of truth (resolveEffectiveApiMethods / isApiOperationAllowed) rather than re-implementing it. Two routes:
Either way the acceptance shape is the one #8083 used: one declaration, both read paths (get_record tool and the record resource), same verdict.
Overlap to check before dispatch
#7279 touches this exact code. It is about a third hand-written ExecutionContext assembly in the stdio MCP plugin dropping tabPermissions / accessToken, and getRecord's resolvePrincipal is in that blast radius. These two should not be in flight at the same time, and there may be a case for doing them as one card.
Generated by Claude Code
Found while implementing #8083 (applying the exposure gate on the stdio MCP data bridge). Filed rather than fixed there: #8083's scope is the bridge, and this is a different seam in a different file.
What differs
#8083 closed the declaration leak on the six object-CRUD verbs, which all flow through
createStdioDataBridge(packages/mcp/src/stdio-data-bridge.ts). The ADR-0101 record resource does not use that bridge at all. Its reader is a separate closure built inline in the plugin:packages/mcp/src/plugin.ts—getRecord = async (objectName, recordId) =... callsscopedQl.find(objectName, { where: { id: recordId }, limit: 1, context: ec })directlythis.runtime.bridgeResources(metadataService, getRecord)objectstack://objects/{objectName}/records/{recordId}So after #8083 an object declaring
enable.apiEnabled: falseis refused byget_record(the tool) and still readable through the record resource, on the same transport, under the same key.Why it matters
Same grading as #8083, and for the same documented reason: this is a surface-area declaration leak, not an authorization bypass.
packages/runtime/src/api-exposure.tsrecords the gate as a surface-area control rather than the authorization boundary, and this read passes the engine's CRUD / FLS / RLS middleware exactly as before — it goes throughql.findwith the key'sExecutionContext. What leaks is the author's exposure declaration. Please do not triage this as a security issue.Scope note: the gap predates #8034 — the record resource has read rows over
ql.findwithout the gate since #7645, which is the narrower form of the divergence #8083's body records under "Scope note".Suggested shape (not prescriptive)
#8083 landed the gate as
enforceApiExposureinsidestdio-data-bridge.ts, taking its decision from the spec's single source of truth (resolveEffectiveApiMethods/isApiOperationAllowed) rather than re-implementing it. Two routes:stdio-data-bridge.tsand call it ingetRecordbefore thefind. Smallest diff. The gated action isget, matching what the HTTP path sends for a single-record read.apiEnabled/apiMethodsexposure gate that HTTP applies #8083 raise.Either way the acceptance shape is the one #8083 used: one declaration, both read paths (
get_recordtool and the record resource), same verdict.Overlap to check before dispatch
#7279 touches this exact code. It is about a third hand-written
ExecutionContextassembly in the stdio MCP plugin droppingtabPermissions/accessToken, andgetRecord'sresolvePrincipalis in that blast radius. These two should not be in flight at the same time, and there may be a case for doing them as one card.Generated by Claude Code