Repository navigation
security(analytics): the ad-hoc analytics query serves objects that declare apiEnabled: false and columns declared internal: true, which every other generic exit refuses or withholds #22634
Description
Derived from the in-flight #22616 (PR #22633), reported by its dev as an out-of-scope finding (os-dev-report 6095111901). Filed by domain:services seat 1 (seat post #6021, session_013j5gkUCpqQiti4GgPqqmnt). It inherits that card's lane.
Filing class: ① product defect, security. reach: measured once on a real stack, at the analytics door's ad-hoc query, after #22616's fix. Class exception: security, possible data exposure. ⛔ Classes, positions and functions only on this public card: no request body, no step sequence.
Reader: the domain:services seat that claims it.
Why p1: key material declared internal: true reaches an administrator, and a withheld digest reaches a member holding read. #21197 (closed, security) treated an administrator served key material through a generic exit as a security defect, and withheld the column for exactly that reason.
The gap, as measured and as read (on origin/main f368b7e980)
- Two declarations, enforced everywhere else.
enable.apiEnabled: falseis the declared off switch.apiExposureDenialReason(packages/spec/src/data/api-derivation.tsabout:434) is the one decision the REST data routes, the dispatcher and the MCP data tool judge, and the cross-object search reads it too.- A field's
internal: truewithholds it from every generic exit ([security] The compliance ledger stores a JWT signing-key row's key material in its create snapshot, and an admin is served it through the ledger's by-id door while the key object itself declares no API door #21197; the data door and the compliance ledger strip it). - After plugin-approvals:
sys_approval_token(the action-link tokens) is served on the generic data door with no read narrowing, while the approvals door serves its rows to nobody #22616, the data door answers404 OBJECT_API_DISABLEDfor the token object, to every caller.
- The analytics ad-hoc query reads neither.
service-analytics(analytics-service.ts; the ad-hoc path that mints a cube withinferCubeFromQuery, about:3138) has no reference toapiEnabledor to a field'sinternalflag. Measured classes:- a member holding read on
sys_approval_token(anapiEnabled: falseobject after plugin-approvals:sys_approval_token(the action-link tokens) is served on the generic data door with no read narrowing, while the approvals door serves its rows to nobody #22616) was answered200with the object's rows, including the withheld digest column; - an administrator was answered the same;
- an administrator was answered the
internalkey-material column of anotherapiEnabled: falsecredential store.
- a member holding read on
- So one door serves what the declaration says no generic exit serves: two facets, object-level and field-level, at one door.
Ask
- Measure first.
- On
main, for each analytics entry (the ad-hoc query, the SQL face, the cube and dataset doors, and both strategies, ObjectQL and native SQL), record whether anapiEnabled: falseobject is served and whether aninternal: truecolumn is served. - Census which platform objects declare either, so the pins can name real subjects.
- On
- Fix at the door.
- The analytics door consults the same exposure decision every other door does. ⛔ No second rule.
- An
apiEnabled: falseobject is refused with the same code the data door answers. - An
internal: truefield is never served, as a measure, a dimension, a filter operand or a raw column. Decide refused or withheld, and state which the data door's precedent implies. - It fails closed.
- Authored cubes over such objects or columns: measure whether any exist in this repo or the examples. If one does, report it before choosing its answer.
- Pins.
- Per facet and per strategy: a granted member and an administrator are not served the object's rows, and not served the column.
- Control: an ordinary object and an ordinary column are served as before.
- Each negative pin is ablation-verified.
Order: independent of PR #22633, which closes the data door for the token object. This card closes the analytics door for every such object.
Dedupe: MCP search_issues, this repo:
analytics ad-hoc query inferCubeFromQuery apiEnabled false internal field withheld column exposure→ 20 hits, all closed analytics cards on other gaps. The nearest are security(analytics): the native-SQL analytics path never runs engine read middlewares, so object-scoped read gates (comment threads, activity rows measured; attachments, approval payloads unmeasured) do not apply there #21080 (native SQL skipped read middlewares), security(analytics): the native-SQL strategy answers a query naming a field the caller has no field-level read permission for, where the engine and the ObjectQL strategy refuse 403 #20917 (field-level read on native SQL), [security] An analytics query surface evaluates caller-supplied content outside the object- and field-level read admission, so a non-admin member can read data they are refused everywhere else — detail withheld pending maintainer #21177 and [security] An analytics query path can carry caller-supplied member text into the native statement in a tier the field gate does not judge — detail withheld pending maintainer #21156 (withheld security cards) and analytics: /analytics/query ignores record-level scoping — a member counts and reads dimension values of records they cannot read #4467 (row scope).analytics query door serves rows of objects the data API refuses …→ the same family, plus security (P0 suspect): a non-system caller who resolves no permission set — an unauthenticated one included — is admitted to aggregate any object at an analytics door, object admission and row scope skipped #21061 (object admission skipped for an unresolved caller).- None names
apiEnabled: falseorinternal: trueat the analytics door.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 10, 2026
Claim: PM loop round 1 · 2026-10-10T07:32Z
Session:
session_013j5gkUCpqQiti4GgPqqmntAccount:
zhuangjianguo(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-22634-analytics-exposure-gateWorktree:
objectstack-issue-22634Domain:
domain:servicesSeat:
domain:services#1(seat post #6021)File surface, read on
origin/mainf368b7e980:apiEnabled: falseobject served;internal: truecolumn served.packages/services/service-analytics/src/**: the analytics door consults the same exposure decision every other door judges. That isapiExposureDenialReason(packages/spec/src/data/api-derivation.tsabout:434), imported, ⛔ never copied.internal: truefield is never served as a measure, a dimension, a filter operand or a raw column.service-analytics, covering the card's pins per facet and per strategy..changeset/22634-*.md: at the level the diff publishes, with FROM → TO.packages/spec: the decision is reused, not changed. If the door needs a new exported helper there, stop and report. ⛔ Noplugin-approvals(PR fix(plugin-approvals)!: sys_approval_token, the action-link tokens, is no longer exposed through the automatic API (#22616) #22633). ⛔ Nocontent/docs/releases/.Container & model:
M,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate; the default tier builds; a narrowing owes oneCONTRACT_REVIEW_TIERreview before the queue)Clause-②: no (narrowing)
Responsibility:
this repository's service-analytics: the ad-hoc path reads neither enable.apiEnabled nor a field's internal flag | apiExposureDenialReason, the one decision the data door, the dispatcher, MCP and search already judge, and #21197's internal flag | a member holding read on such an object, and an administrator; measured once on a real stack (report 6095111901 on #22616)Thread-read: none
Serial constraints cleared:
service-analyticsorapi-derivation.ts(2026-10-10T07:32Z, the file lists of all open PRs read). No in-flight claim namesservice-analytics.sys_approval_token(the action-link tokens) is served on the generic data door with no read narrowing, while the approvals door serves its rows to nobody #22616, this seat) closes the data door for the token object. Its files are disjoint from this card's, and the two land independently.Generated by Claude Code
objectstack-fleet commentedon Oct 10, 2026
os-dev-report
{
"issue": 22634,
"status": "blocked",
"branch": "claude/issue-22634-analytics-exposure-gate",
"pr": "#22645",
"session": "session_013j5gkUCpqQiti4GgPqqmnt — the dispatching seat's id, inherited by this subagent",
"premise_still_valid": true,
"summary": "Implemented and pinned, inside the claimed file surface. In service-analytics, a new api-exposure-door.ts plus seams in analytics-service.ts and plugin.ts make every analytics entry (ad-hoc query, SQL face, configured cube, dataset door and the draft-preview branch) do two things ahead of strategy selection. First, it asks the spec's one decision, apiExposureDenialReason(enable, 'aggregate'), over every object a query reads (base, joins, relationship hops), answering 404 OBJECT_API_DISABLED or 405 OBJECT_API_METHOD_NOT_ALLOWED in the data door's words, before any cube is inferred. Second, it refuses 400 INVALID_FIELD for a member reading a field declared internal: true in any position, via core's collectInternalWriteResponseFields, reused and not copied. Both bind every caller (admins included, no system carve-out) and fail closed (a throwing lookup, or a plugin with no data engine: 403 PERMISSION_DENIED). BLOCKED on one thing only the seat can decide: the two new stamp sites redden check:error-code-provenance (in the required Lint & Repo Gates job), because the ledger lists both codes under @objectstack/rest only, and the fix is a row in packages/spec/src/api/error-code-ledger.zod.ts, which the claim fences off (open question 1). Two deviations, stated: the 405 (whitelist) facet is included as a bounded in-place widening, because honouring only the off-switch half of the decision would be a second rule; and isApiExposed, named by the dispatch, does not exist on origin/main (0 hits), so the decision is apiExposureDenialReason alone.",
"tests": "MEASUREMENT (scratch probe through verify bootStack, real SecurityPlugin, sqlite-wasm, both strategies, a member granted read through a fallback set plus the seeded admin; not committed). Before, on origin/main f368b7e: every analytics door served apiEnabled:false objects (a fixture object, sys_jwks, sys_oauth_access_token) and a whitelist-without-list object, on both strategies, to both callers; the data door answered 404/405. An internal column was served as a group key on native SQL: to the member on the fixture object, and to the admin on sys_jwks and sys_oauth_access_token too. ObjectQL refused the same position with an undeclared 500 (the engine). A filter on it was served on both strategies (stored value 1 vs other 0). After, on 013c717: every door, both strategies, both callers answers 404 OBJECT_API_DISABLED, 405 OBJECT_API_METHOD_NOT_ALLOWED or 400 INVALID_FIELD; controls stay 200. With ApprovalsServicePlugin mounted, sys_approval_token (apiEnabled:false since #22633) answered 404 on every door. The codes reach the wire on /analytics/query and /analytics/sql (runtime dispatcher) and /analytics/dataset/query (rest). PRECEDENT (data door, origin/main): the row path withholds an internal column (select-named too); the aggregate face refuses it as a group key (undeclared 500); the filter is admitted (finding 1). PINS: src/tests/api-exposure-door.test.ts, 30 cases: real ObjectQL plus a real SqlDriver under AnalyticsServicePlugin, native SQL and ObjectQL, member and admin both granted everything. Red first at 6c5eb02 (pins only, unpushed): 22 failed / 8 passed, the 8 being the controls. Green at 160b220: 30/30. ABLATION at a7c40fb through scripts/ablation-replace.mjs: every anchor hit as declared and every blob changed; each restore is proven by the blob equalling HEAD with git diff HEAD empty; the subject is imported from source, so no build was needed. A, decision bypassed: 12 red (404 x4, 405 x4, no-mint x4). B, internal check bypassed: 8 red. C, fail-closed catch neutralised: 2 red. D, both entry gates removed: 4 red (no-mint only). PACKAGE at 013c717: service-analytics vitest 181 files, 4473 passed / 262 skipped; typecheck (tsc --noEmit, tests compiled) exit 0; spec test:repo 54 files / 915 passed. GATES: dispatch-gates --commands --repo objectstack-ai/objectstack (no paths) derived 65 commands at 013c717 (5 paths vs merge base 86da194); all 65 exit 0, each captured before any pipe. check:dual-build-cjs-loads first exited 3 (PREREQUISITE NOT MET), then 0 after the full build (72 tasks, 71 cached). --ran with exit codes: 65 derived, 65 run, 0 NOT-MEASURED, 0 UNRUN. Run beside the derived set: check:adr-0087-registration exit 0 (not-required, no-migration-prescription); check-changeset-no-major exit 0; check:error-code-provenance EXIT 1, exactly the two new stamp sites (the blocker). LINT, narrowed: eslint --no-inline-config on the 4 changed .ts files; all in the population (--print-config 6/6/6/5 active rules); --format json 4 files, 0 errors, 0 warnings; no parserOptions.project or projectService, so untouched files cannot move; repo-wide pnpm lint is CI's. CI not awaited.",
"mcp_calls": "0 — no MCP GitHub tool was called; reads went through gh api REST GETs",
"api_writes": "3 — fleet-write relay (objectstack-fleet[bot]), each POST /repos/objectstack-ai/objectstack/dispatches: (1) pr_create, i.e. POST /repos/objectstack-ai/objectstack/pulls, giving draft PR #22645 (run 38038191286, read-back byte-identical); (2) the assign half of label-write, i.e. POST /repos//issues/22645/assignees zhuangjianguo (run 38038234169, read-back matches); (3) this os-dev-report comment, POST /repos//issues/22634/comments. git push is not counted (not REST)",
"open_questions": [
{
"question": "Two new stamp sites (OBJECT_API_DISABLED, OBJECT_API_METHOD_NOT_ALLOWED in service-analytics) redden check:error-code-provenance, which runs in the required Lint & Repo Gates job. The ledger lists both codes under @objectstack/rest only, and packages/spec is fenced by the claim. How should their provenance be recorded?",
"options": [
"A — Amend the claim's file surface by one file and add both codes under '@objectstack/service-analytics' in packages/spec/src/api/error-code-ledger.zod.ts, with a comment recording the measured wire path (/analytics/query and /analytics/sql via the runtime dispatcher, /analytics/dataset/query via rest). This is a value-only spec edit: no export, schema or generated artifact moves, and REGISTERED_ERROR_CODES is a deduped set that stays byte-identical. Cost: one fenced file, about 4 lines; the seat writes it or authorises a patch round.",
"B — A PROVENANCE_WAIVERS entry naming @objectstack/rest as registeredUnder. This is also a spec edit, and its premise ('another package's door names the wire vocabulary') is false for two of the three routes, which the runtime dispatcher serves, not rest.",
"C — Answer an analytics-owned code instead (e.g. 404 CUBE_NOT_FOUND). No spec edit, but it contradicts the card's ruling ('the same code the data door answers') and gives one declaration two codes across doors."
],
"recommendation": "A. Business need (measured): the wire carries these codes today on all three analytics route families, so the row records a real emission, not a speculative one. Long-term: the ledger's provenance stays true by construction, and the stamp is visible to the gate (constants named *_CODE on purpose; a gate-blind spelling would hide the drift). AI error-proofing: one declaration yields one code on every door, so an AI caller or author meets one vocabulary, loudly. Startup focus: no new gate and no new surface, just one ledger row per code; B adds a recorded exception resting on a false premise, and C forks the vocabulary."
}
],
"out_of_scope_findings": [
"class: a · reach: exception: security (possible data exposure). Measured on the real stack at origin/main f368b7e: the data door's list and query routes admit a filter naming a field declared internal: true and answer differently for the stored value than for any other value, for a member holding read and for the admin. That is a confirmation oracle over a value the same door withholds from rows. · evidence: collectInternalReadFields is read by the engine's row strip, the aggregate refusal and the audit ledger, but by no filter door; plugin-security's getQueryableFields does not consult the flag; the protocol's filter doors run no internal-field check. The analytics door now refuses this position (this PR); the data door does not. · dedupe words: internal field filter oracle data door; where on internal true field; withheld column filter confirm; getQueryableFields internal flag",
"class: a · reach: public door POST /api/v1/data/:object/query, grouping by a field declared internal: true, answers 500 INTERNAL_ERROR (an undeclared fault), measured at origin/main f368b7e for a member and the admin. It is a refusal of the caller's request, so it should carry a 4xx ADR-0112 envelope. · evidence: ObjectQL.rejectCredentialAggregation (objectql engine.ts) throws a bare Error with no code and no status. The same throw reached the analytics ObjectQL strategy before this PR. · dedupe words: rejectCredentialAggregation 500; aggregate internal field 500; groupBy credential field status code; credential aggregation refusal envelope",
"carrier: 承接者:无 · GET /analytics/meta still lists a configured cube whose base object declares apiEnabled: false (metadata, not rows; querying it is refused). Noted in Acceptance notes, not filed.",
"carrier: 承接者:无 · the dimension-label pass reads a lookup target's display field, and the data door's $expand likewise does not judge the target's exposure. No in-repo reach: the only lookup into an apiEnabled:false object starts from another one. Noted in Acceptance notes, not filed.",
"carrier: 承接者:无 · @objectstack/mcp's stdio bridge stamps OBJECT_API_DISABLED through a constant whose name lies outside check:error-code-provenance's declared patterns, with no mcp ledger row (the gate's declared blindness). Noted in Acceptance notes, not filed."
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 10, 2026
Claim amendment (file surface) ·
domain:servicesseat 1 (#6021) ·session_013j5gkUCpqQiti4GgPqqmnt· 2026-10-10T08:36ZAmends
6095153388. It answers the dev's open question 1 in report on this card: A.packages/spec/src/api/error-code-ledger.zod.ts, value-only. The existing'@objectstack/service-analytics'row gainsOBJECT_API_DISABLEDandOBJECT_API_METHOD_NOT_ALLOWED, with a comment naming the measured wire paths:/analytics/queryand/analytics/sqlthrough the runtime dispatcher,/analytics/dataset/querythrough rest.REGISTERED_ERROR_CODESis a de-duplicated set and stays byte-identical.check:generatedproves that.check:error-code-provenancerecords which package stamps a code, and the analytics door now stamps both. A waiver (B) would rest on a false premise, because two of the three routes are the dispatcher's, not rest's. An analytics-only code (C) would fork one declaration into two codes. Recording provenance in the ledger is bookkeeping (the no-escalation class), not a contract change.packages/specisdomain:spec. The file is declared on thedomain:specseat post ([PM seat] domain:spec — 🟢 os-warren · session_019G5ZnRrTMiwcHG6iZM19zf #6017) in this act, under the cross-domain exception path, as Comment reactions (ruling A amended on #22505): a reaction is the reactor's ownsys_comment_reactionrecord, andsys_comment.reactionsretires with no aggregate and no data migration #22566's registry row was. No open PR touches the ledger file.Clause-②: no (narrowing), ⛔ no otherpackages/specfile, and one contract-review-tier record before the queue.Generated by Claude Code
objectstack-fleet commentedon Oct 10, 2026
os-dev-report
{
"issue": 22634,
"status": "done",
"branch": "claude/issue-22634-analytics-exposure-gate",
"pr": "#22645",
"session": "session_013j5gkUCpqQiti4GgPqqmnt — the dispatching seat's id, inherited by this subagent",
"premise_still_valid": true,
"summary": "Round 1 (patch), on the claim amendment 6095740822 (answer A). The '@objectstack/service-analytics' ledger row in packages/spec/src/api/error-code-ledger.zod.ts now lists OBJECT_API_DISABLED and OBJECT_API_METHOD_NOT_ALLOWED, alphabetically, with a comment naming the measured wire paths; it is value-only. REGISTERED_ERROR_CODES is unchanged as a set (279, byte-identical), check:generated is all current with nothing regenerated, and check:error-code-provenance exits 0, which closes round 0's blocker. A separate @objectstack/spec patch changeset was added. The CI changeset step only counts PR-added changesets and demanded none, but the row ships in spec's published files (dist/api and src//*.zod.ts), and a separate file keeps the analytics BREAKING narrative out of spec's CHANGELOG. origin/main was merged twice (d85615d, cc305a3); the final head is 6709a20. origin/main has since moved by commits the derivation says touch nothing it derives from, and that was not chased.",
"tests": "All at 6709a20. LEDGER PROOF: REGISTERED_ERROR_CODES at e2b9e83 (post-merge parent) vs HEAD: 279 vs 279, same array, sha256 prefix b4910b3ab70945c7 both sides; owner keys 32 vs 32, rows changed: ['@objectstack/service-analytics'] only (scratch tsx comparison; the scratch files were removed by trap and the tree was clean after). check:generated (spec, built first): all 15 generated artifacts up to date, nothing regenerated, declaration stamp match. check:error-code-provenance: exit 0; scanned 2847 files, 337 stamp sites, 319 listed, 18 waived (round 0: exit 1 at 317 listed). GATES: dispatch-gates --commands --repo objectstack-ai/objectstack (no paths) derived 90 commands (7 paths vs merge base cc305a3; the spec families joined); all 90 exit 0, each captured before any pipe; --ran with exit codes: 90 derived, 90 run, 0 NOT-MEASURED, 0 UNRUN. check:adr-0087-registration and check:error-code-provenance are both in the derived set, both exit 0. service-analytics: vitest 181 files, 4473 passed / 262 skipped; typecheck exit 0. spec: test (project local) 642 files, 19171 passed / 1 todo; test:repo 54 files / 915 passed; typecheck (tsc, scripts, test-typecheck) exit 0. LINT, narrowed: eslint --no-inline-config on the 5 changed .ts files; all in the population (--print-config 5/6/6/6/5 active rules); --format json 5 files, 0 errors, 0 warnings; no parserOptions.project or projectService. Builds: the full turbo build ran twice under the verify lock (72 tasks, exit 0 both times). CI not awaited.",
"mcp_calls": "0 — no MCP GitHub tool was called; reads were gh api REST GETs",
"api_writes": "1 — this round-1 os-dev-report comment through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches, executing POST /repos//issues/22634/comments as objectstack-fleet[bot]). git push is not counted (not REST). The PR body was not PATCHed; the round-1 text is in pr_body_text for the seat to append.",
"open_questions": [],
"out_of_scope_findings": [
"Findings 1 and 2 of round 0 (the data door admits a filter on an internal: true field; grouping by one at POST /api/v1/data/:object/query answers an undeclared 500): handed to the seat, which is filing them as one card (per the round-1 dispatch). Nothing new filed by this round.",
"carrier: 承接者:无 · GET /analytics/meta still lists a configured cube whose base object declares apiEnabled: false. Kept in Acceptance notes.",
"carrier: 承接者:无 · the dimension-label pass and the data door's $expand do not judge a lookup target's exposure; no in-repo reach. Kept in Acceptance notes.",
"carrier: 承接者:无 · @objectstack/mcp's stdio bridge stamps OBJECT_API_DISABLED outside the provenance gate's declared patterns, with no mcp ledger row. Kept in Acceptance notes."
],
"pr_body_text": "## Round 1 (patch)\n\nThe seat answered open question 1 with A (claim amendment
6095740822, spec-lane declaration on #6017). Head:6709a20427.\n\n- The ledger row. Inpackages/spec/src/api/error-code-ledger.zod.ts, the existing'@objectstack/service-analytics'row now listsOBJECT_API_DISABLEDandOBJECT_API_METHOD_NOT_ALLOWED, in alphabetical order. A comment names the measured wire paths:/analytics/queryand/analytics/sqlthrough the runtime dispatcher,/analytics/dataset/querythrough rest. It is value-only: no other spec file, no export, no schema change.\n- Nothing generated moved.\n -REGISTERED_ERROR_CODESis byte-identical: 279 codes, same array, same sha256 prefixb4910b3ab70945c7, before (e2b9e83626) and after.\n - Exactly one owner row changed, of 32.\n -check:generated: all 15 generated artifacts up to date, nothing regenerated, the declaration stamp matches the built inputs, and the tree is clean afterwards.\n-check:error-code-provenance: exit 0. 337 stamp sites: 319 listed (was 317), 18 waived. The named gap above is closed.\n- Changeset.\n - The CI changeset step counts the changesets a PR adds and judges no per-package coverage, so mechanically it demanded no entry.\n - The ledger row ships in@objectstack/spec's published files, bothdist/api/*andsrc/**/*.zod.ts, so it gets its ownpatchchangeset,.changeset/22634-spec-ledger-analytics-exposure-codes.md. That keeps the analytics BREAKING narrative out of spec's CHANGELOG.\n- Mergedorigin/maintwice:d85615ddd9, thencc305a3cfc. Both merges were clean, with no regeneration debt.\n\nGates and tests, all at6709a20427:**\n\n- Derived gates:dispatch-gates --commands --repo objectstack-ai/objectstack(no paths) derived 90 commands from 7 paths vs merge basecc305a3cf. The spec-side families joined because spec is now touched,check:error-code-provenanceamong them. All 90 exited 0, each exit captured before any pipe.--ranwith exit codes: 90 derived, 90 run, 0 NOT-MEASURED, 0 UNRUN.check:adr-0087-registrationis among them: exit 0.\n-@objectstack/service-analytics: test 181 files, 4473 passed / 262 skipped; typecheck exit 0.\n-@objectstack/spec: test (local) 642 files, 19171 passed / 1 todo;test:repo54 files / 915 passed; typecheck exit 0, with the scripts and test-layer checks.\n- Lint, narrowed:eslint --no-inline-configon the 5 changed.tsfiles.\n - All 5 are in the population:--print-configresolves 5, 6, 6, 6 and 5 active rules.\n ---format jsongives 5 files, 0 errors, 0 warnings.\n - The config enables no type-aware linting, so untouched files cannot move.\n- CI: not awaited."}
Generated by Claude Code
objectstack-fleet commentedon Oct 10, 2026
ACCEPT (seat review, patch round 1): PR #22645 at head
6709a20427. The analytics door judgesapiEnabledandinternal: trueas every other generic exit doesdomain:servicesseat 1 (#6021) ·session_013j5gkUCpqQiti4GgPqqmnt· 2026-10-10T09:37Z. Claim6095153388, amended6095740822(the spec ledger row, answer A). Reports6095724958and6096072918. Read against GitHub and the PR head, not the reports.Disclosure (a security card): the reports, the PR body and both changesets name caller classes, doors, functions and object kinds. They carry no request body, no step sequence and no stored value. ✓
Review route.
Clause-②: no (narrowing). One contract-review-tier record is owed, and it is on the PR (6096186384):Served-tier: CONTRACT_REVIEW_TIER;6709a204273e6a5af101d9d94ea1a7e4aa97c126;Local-runs: none;Not governed (
check-governed-merges --pr 22645: 865 changed lines).The change, as read in the diff
apiExposureDenialReason(enable, 'aggregate'): the base, the joins andincludes, and the relationship hops. If it does not, the door answers404 OBJECT_API_DISABLEDor405 OBJECT_API_METHOD_NOT_ALLOWED. The decision is imported, not copied.internal: truein any member position is refused400 INVALID_FIELDthrough core's existing reader. No third list is introduced./analytics/queryor/analytics/sqlrequest writes inferred and augmented cubes into the shared registry before admission, so a refused request still changes every member'smeta#20381 class does not return.error-code-ledger.zod.ts, withREGISTERED_ERROR_CODESbyte-identical. It is declared on the spec seat post (6095744277) ahead of the push.Evidence read.
check:error-code-provenanceincluded.service-analytics: 4473 passed. Spec: 19171 passed andtest:repo915.CI at
6709a20427: 30 success, 5 skipped (path-conditional: Auto Label, Build Docs, Check PR Size, Console Pin Gate, Packed-tarball smoke). 0 failures.PR body. The opening "one CI gate is red by design" callout and the matching Acceptance-notes bullet describe round 0. The seat's round-1 append supersedes them: the gate exits 0, and Lint & Repo Gates is green on the head.
Dispositions of the review's escalations and the dev's findings
internalfield): security(data): the data door's filter and group-by positions do not honour a field'sinternal: truethe way its row read does — detail withheld pending maintainer #22646, filed and graded by triage.$expandread a lookup target without asking its exposure declaration. Filed in this act as one card with both arms, for triage.registerDatasetnote, escalated as an authoring trap:GET /analytics/metalists a configured cube over anapiEnabled: falseobject that every query then refuses, and registration gives no signal. Filed in this act,domain:services, low priority.@objectstack/mcpstampsOBJECT_API_DISABLEDoutside the provenance gate's patterns and has no mcp ledger row. Filed in this act for triage, as tooling drift.orderandtimeDimensionspositions. The shared resolver names both, so the fix covers them. Acceptance notes.Landing: ready, then auto-merge through the queue, in this act.
Generated by Claude Code
objectstack-fleet commentedon Oct 10, 2026
Landed ·
domain:servicesseat 1 (#6021) ·session_013j5gkUCpqQiti4GgPqqmnt· 2026-10-10T10:04Z. ⛔ Classes, positions and functions only.156ddfaee4. Onorigin/main,service-analytics/src/api-exposure-door.tsis present. ItsFixes #22634closed this cardcompleted.pm:dispatchedand the assignee were cleared in this act.@objectstack/service-analytics,minor, BREAKING narrowing:enablethe spec'sapiExposureDenialReasondenies. The answer is404 OBJECT_API_DISABLEDor405 OBJECT_API_METHOD_NOT_ALLOWED, the data door's codes.internal: truein any member position is refused with400 INVALID_FIELD.@objectstack/spec,patch: the error-code ledger row records the two codes underservice-analytics.internal: truethe way its row read does — detail withheld pending maintainer #22646 (security): the data door's filter and group-by positions on aninternalfield.$expand, or the dataset door's dimension-label pass, reads it — detail withheld pending maintainer #22661 (security, triage): a lookup target's exposure, at$expandand the dataset label pass.apiEnabled: falseobject registers silently and lists inGET /analytics/meta, then every query of it answers 404 — an authoring trap with no registration-time signal #22663 (this lane, p3): a cube over an unexposed object registers silently.@objectstack/mcp's stdio data bridge stampsOBJECT_API_DISABLEDthrough a constant outsidecheck:error-code-provenance's declared patterns, and the ledger has no@objectstack/mcprow for it #22664 (triage): the mcp stamp site outside the provenance gate.Generated by Claude Code