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
Activity
- addedbugSomething isn't workingSomething isn't workingpriority:p1High: required for production / M2High: required for production / M2area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsPermissions that actually hold — RLS/FLS, sharing model, write-path guardsand removed
on Oct 10, 2026 objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsClaim: PM loop round 1 · 2026-10-10T07:32Z
Session:session_013j5gkUCpqQiti4GgPqqmnt
Account:zhuangjianguo(the seat's linked user asGET /useranswers it; the card's assignee)
Branch:claude/issue-22634-analytics-exposure-gate
Worktree:objectstack-issue-22634
Domain:domain:services
Seat:domain:services#1(seat post #6021)
File surface, read onorigin/mainf368b7e980:- Step 1, a reading before any edit: for each analytics entry (the ad-hoc query, the SQL face, the cube and dataset doors) on both strategies (ObjectQL and native SQL):
- is an
apiEnabled: falseobject served; - is an
internal: truecolumn served. - Also a census of the platform objects that declare either.
- is an
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.- An
internal: truefield is never served as a measure, a dimension, a filter operand or a raw column. - It fails closed.
- An
- Tests in
service-analytics, covering the card's pins per facet and per strategy. .changeset/22634-*.md: at the level the diff publishes, with FROM → TO.- ⛔ No
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/. - Stop on breach; explain in the report.
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) - The analytics door stops serving what the declarations already refuse at every other door. It narrows that door's accept set. The PR owes one contract-review-tier record before the queue.
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: - No open PR touches
service-analyticsorapi-derivation.ts(2026-10-10T07:32Z, the file lists of all open PRs read). No in-flight claim namesservice-analytics. - PR fix(plugin-approvals)!: sys_approval_token, the action-link tokens, is no longer exposed through the automatic API (#22616) #22633 (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, 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
- Step 1, a reading before any edit: for each analytics entry (the ad-hoc query, the SQL face, the cube and dataset doors) on both strategies (ObjectQL and native SQL):
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsos-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 commented
on Oct 10, 2026 ContributorAuthorMore actionsClaim 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.- Added:
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.- No export, schema or generated artifact moves.
REGISTERED_ERROR_CODESis a de-duplicated set and stays byte-identical.check:generatedproves that.
- No export, schema or generated artifact moves.
- Why A: the ruling is "the same code the data door answers". The gate
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. - Cross-domain:
packages/specisdomain:spec. The file is declared on thedomain:specseat post ([PM seat] domain:spec — 🟢 os-project-manager · session_01S3aAf11JjbW1mSGL1EhfFj #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. - Everything else in the claim stands:
Clause-②: no (narrowing), ⛔ no otherpackages/specfile, and one contract-review-tier record before the queue.
Generated by Claude Code
- Added:
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsos-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 amendment6095740822, 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 commented
on Oct 10, 2026 ContributorAuthorMore actionsACCEPT (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;- head
6709a204273e6a5af101d9d94ea1a7e4aa97c126; Local-runs: none;- rendered after all 35 checks had completed;
- an isolated at-tier subagent, adopted by this seat;
- verdict PASS.
Not governed (
check-governed-merges --pr 22645: 865 changed lines).The change, as read in the diff
- Object facet. Every object a query reads must pass the spec's own
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. - Field facet. A field declared
internal: truein any member position is refused400 INVALID_FIELDthrough core's existing reader. No third list is introduced. - Order. Both gates run before any cube is inferred or registered. A refused ad-hoc request mints nothing (pinned), so the analytics: an ad-hoc
/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. - Coverage. Every analytics entry, on both strategies, through one seam ahead of strategy selection.
- Who is bound. Every caller, administrators included. It fails closed.
- The ledger row. One value-only row in
error-code-ledger.zod.ts, withREGISTERED_ERROR_CODESbyte-identical. It is declared on the spec seat post (6095744277) ahead of the push.
Evidence read.
- Before and after, measured on a real stack for both strategies and both callers.
- 30 pins: red first (22 red, 8 controls green), then green.
- Four ablations, each restored to a blob equal to HEAD.
- 90 derived gate commands, all exit 0,
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
- Findings 1–2 (the data door's filter and group-by positions on an
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. - Finding 4, escalated: the dataset dimension-label pass and the data door's
$expandread a lookup target without asking its exposure declaration. Filed in this act as one card with both arms, for triage. - Finding 3 with the
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. - Finding 5, escalated:
@objectstack/mcpstampsOBJECT_API_DISABLEDoutside the provenance gate's patterns and has no mcp ledger row. Filed in this act for triage, as tooling drift. - Not pinned, observation only: an internal field in the
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 commented
on Oct 10, 2026 ContributorAuthorMore actionsLanded ·
domain:servicesseat 1 (#6021) ·session_013j5gkUCpqQiti4GgPqqmnt· 2026-10-10T10:04Z. ⛔ Classes, positions and functions only.- PR fix(service-analytics)!: the analytics door judges the generic-exit declarations — an object's enable block and a field's internal flag (#22634) #22645 merged through the queue as
156ddfaee4. Onorigin/main,service-analytics/src/api-exposure-door.tsis present. ItsFixes #22634closed this cardcompleted.pm:dispatchedand the assignee were cleared in this act. - What landed:
@objectstack/service-analytics,minor, BREAKING narrowing:- Every analytics entry, on both strategies, refuses an object whose
enablethe spec'sapiExposureDenialReasondenies. The answer is404 OBJECT_API_DISABLEDor405 OBJECT_API_METHOD_NOT_ALLOWED, the data door's codes. - A field declared
internal: truein any member position is refused with400 INVALID_FIELD. - Both refusals bind every caller, administrators included, and fail closed.
- Every analytics entry, on both strategies, refuses an object whose
@objectstack/spec,patch: the error-code ledger row records the two codes underservice-analytics.
- Carried, not closed here:
- security(data): the data door's filter and group-by positions do not honour a field's
internal: truethe way its row read does — detail withheld pending maintainer #22646 (security): the data door's filter and group-by positions on aninternalfield. - security(data, analytics): a lookup target's exposure declaration is not judged when the data door's
$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. - analytics: a configured cube or dataset over an
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. - error-code provenance:
@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.
- security(data): the data door's filter and group-by positions do not honour a field's
Generated by Claude Code
- PR fix(service-analytics)!: the analytics door judges the generic-exit declarations — an object's enable block and a field's internal flag (#22634) #22645 merged through the queue as
Derived from the in-flight #22616 (PR #22633), reported by its dev as an out-of-scope finding (os-dev-report
6095111901). Filed bydomain:servicesseat 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. Classexception: security, possible data exposure. ⛔ Classes, positions and functions only on this public card: no request body, no step sequence.Reader: the
domain:servicesseat that claims it.Why p1: key material declared
internal: truereaches 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/mainf368b7e980)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.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).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.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: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;internalkey-material column of anotherapiEnabled: falsecredential store.Ask
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.apiEnabled: falseobject is refused with the same code the data door answers.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.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).apiEnabled: falseorinternal: trueat the analytics door.Generated by Claude Code