Repository navigation
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
Description
Activity
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsTriage: first grade —
bug·security·priority:p0·domain:cli·area:access·pm:queue. P0 confirmed. The analytics dispatcher domain is the one data-serving dispatcher domain without the shared anonymous-deny floor. Split: the plugin-security zero-set half goes to its owndomain:servicescardEmergency triage pass (a subagent whose only authority is triage), spawned by the
domain:servicesexecution seat (#6021,session_01XY5uCwTjZj7884yYtyur4H) under the emergency direct-triage path · 2026-10-01T04:08Z. ⛔ Not a claim, ⛔ not a dispatch. The triage seat (#6015) had not graded this card when this was written.⚠️ Disclosure discipline: doors, caller classes, codes, statuses, files and functions only. The private readings were read and are not quoted.Path: 「真挡得住的权限」 (permissions that actually hold) |
access-security.anonymous-deny-surfaces(state ②) | P②
Type: Bug. It breaks a declared contract: ADR-0056 D2 (anonymous default-deny, and "Mirror the change in the analytics read-scope path") and ADR-0090 D9 (a guest holds theguestposition and nothing else).
Triage: lands inpackages/runtime(domain:cli); rationale: the door's missing authentication is what admits the unauthenticated member of the class on a stock boot, and that half can be dispatched now without touching a file an in-flight PR holds.P0, read from source at
origin/main8f784959cfThe door.
packages/runtime/src/dispatcher-plugin.ts, the analytics block instart(): the cube-read, SQL and meta faces are plain server mounts that hand the request to the dispatcher. No authentication gate runs at the mount.- The dispatcher turns an unauthenticated request into the guest envelope (
packages/runtime/src/security/resolve-execution-context.ts, viaassembleExecutionContextOrGuest). That entry's own docblock says to adopt it only on a surface that serves anonymous principals, because it turns an authentication failure into an authorization evaluation. packages/runtime/src/domains/analytics.ts,handleAnalyticsRequest: 0 calls ofshouldDenyAnonymous. Control:ai,meta,security,actions,automationandpackagesin the same directory each call it (2 or 3 hits each).- The sibling doors carry it. In
packages/rest/src/rest-server.ts, the record doors and the analytics dataset door each open withenforceAuth, so they answer the unauthenticated caller401 UNAUTHENTICATED. packages/qa/dogfood/test/authz-ledger-population.baseline.tsstill lists the analytics dispatcher domain as an unclassified key. No conformance row has ever reviewed this door's posture.
Behind it, in
packages/plugins/plugin-security/src/security-plugin.ts. The analytics service reads both answers throughpackages/services/service-analytics/src/plugin.ts.- Object admission:
- the engine middleware's step 2 CRUD gate runs only under
permissionSets.length > 0; canReadObjectarm 2 returnstruefor an empty set;canWriteObject(the organization wall alone) andcanExport(true) carry a zero-set arm too.
- the engine middleware's step 2 CRUD gate runs only under
- Row scope:
- for an empty set,
computeLayeredRlsFiltercollects no policy (collectRLSPolicies), so Layer 1 compiles to no filter; getReadFiltertherefore returns the sharing predicate alone, and that constrains only objects whose sharing model isprivate;- step 2.6's depth stash and
checkAuthoredRowWrite('abstain') also stand down for an empty set.
- for an empty set,
getReadFilter's principal-less branch assumes the analytics endpoint already refuses a caller without a token. This door does not.
Why P0 (NORTH-STAR 〈优先级〉 rules 1 and 2):
- Rule 1: it is security, at a public door, on the stock configuration, for the least-trusted caller. The caller gets counts over any object, platform identity objects included, and the card's measured reading agrees with this source read.
- Rule 2:
access-security.anonymous-deny-surfacesis a P0 item.- Its title asserts that every mounted API family answers
401before any resource resolution. - The analytics family is mounted on that boot, and the item's steps never drive it.
- That is state ② (the steps cannot reach the path), so the item's P0 is inherited, ⛔ not lowered.
- Its title asserts that every mounted API family answers
Direction: one shared rule at each layer, ⛔ no door-specific copy
1. This card (
domain:cli): the door requires the authentication its siblings require.handleAnalyticsRequestopens with the shared decision,shouldDenyAnonymous(packages/core/src/security/anonymous-deny.ts). It is the first statement, ahead of the service-availability check and the body validation, in the hoisted formdomains/security.tsanddomains/ai.tsuse.- Every analytics face (cube read, SQL, meta) answers the dispatcher-wrapper
401 UNAUTHENTICATED. - ⛔ No second gate at the mount, and ⛔ no zero-set admission rule copied into
service-analytics. - Pins:
- a runtime unit pin per face: anonymous gets
401 UNAUTHENTICATED, the service is never consulted, and a malformed body still gets401; - the analytics family added to
showcase-anonymous-deny-surfaces.dogfood.test.tsin the dispatcher-wrapper family, with a signed-in member as the200control; - an
enforcedrow carrying a gate pin inauthz-conformance.matrix.ts, and the analytics dispatcher-domain key leaving the shrink-only baseline; - the checklist item gains the analytics variant and its step. That file is
docs/qa/**; declare it in the claim's file surface.
- a runtime unit pin per face: anonymous gets
- No pin title states a request.
2. Split (
domain:services): "no set resolved" is no grant, inplugin-security.- For a non-system caller that carries a principal (the guest envelope or a user id), an empty set list is the deny baseline. ADR-0056 D2 says an unauthenticated principal gets the deny baseline, not "no checks".
- At object admission: step 2's guard and the zero-set arms of
canReadObject/canWriteObject/canExportread one answer. - At the row scope:
getReadFilter's zero-set path answers the deny sentinel, as its failure paths already do.
- At object admission: step 2's guard and the zero-set arms of
- ⛔ The principal-less context (no positions, no sets, no user id) is out of scope; ADR-0096 stages that one separately.
- This half closes the class at every door, the doors that carry a guest by design included.
- Why it is split, not folded into one PR:
security-plugin.tsis held by in-flight PR fix(plugin-security)!: a caller who resolves no permission set is served a masked field masked and may not query on it #21051 (security(plugin-security): for a caller who resolves no permission set, the field-projection answers say no masking rule reaches it, whilemaskingRule's describe and the result masker mask it — which one a public door serves is not measured #20995);- it is a platform-wide narrowing that changes every door's zero-set answer, so it measures first.
- Known consequences to measure:
- a picker context on a deployment with no guest set is admitted today only by the absence of sets, and would be refused;
- a signed-in caller on an embedder that disables the baseline loses the everything-default;
- the
ISecurityServicedocblocks inpackages/spec/src/contracts/security-service.tsthat state the zero-set answers belong todomain:spec.
- Reader who acts: the
domain:servicesseat files this half as a ① card, abstract, under this card's disclosure discipline, withBlocked-by: #20995. Triage grades it there. Neighbours: security(spec): a capability-gated field (requiredPermissions, no maskingRule) is served stored to a caller who resolves no permission set, while explain reports it hidden; narrowing it moves getReadableFields' contract answer #21063 (the field-level zero-set answer) and security(forms): two public-form doors meet a field masking rule wrongly — the submit read-back serves masked fields stored to an anonymous submitter, and a picker whose first display field is masked answers 403 to every caller it applies to #21062 (the picker door).
Prior rulings read: anonymous,guest,unauthenticated,analytics → 23 hits; ADR-0056 D2, ADR-0090 D9, ADR-0106 D7; thread: none; repo: objectstack-ai/objectstack
Size suggestion: S, one hoisted call plus the four pins above.
Generated by Claude Code
- addedarea:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingSomething isn't workingpriority:p0Critical: blocker, must ship before MVPCritical: blocker, must ship before MVPand removed
on Oct 1, 2026 objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsClaim: PM loop round 1 of the
domain:cliseat's sessionsession_01VvcEokUG1tvVxkceYfR5XB:priority:p0security, emergency triage's first grade5924543711(direction 1, the door half). A P0 is taken at once, abovebatch
Session:session_01VvcEokUG1tvVxkceYfR5XB
Account:huangyiirene
Branch:claude/issue-21061-analytics-anonymous-deny
Worktree:objectstack-issue-21061
Domain:domain:cli
Seat:domain:cli#1
File surface:packages/runtime/src/domains/analytics.ts:handleAnalyticsRequest(:99on4f83db5a73) opens with the shared decisionshouldDenyAnonymous(packages/core/src/security/anonymous-deny.ts).- It is the first statement, ahead of the service-availability check and the body validation, in the hoisted form
domains/security.tsanddomains/ai.tsuse. - Every analytics face (cube read, SQL, meta) answers the dispatcher-wrapper
401 UNAUTHENTICATED. - ⛔ No second gate at the mount (
dispatcher-plugin.tsis read only). ⛔ No zero-set admission rule copied intoservice-analytics.
- It is the first statement, ahead of the service-availability check and the body validation, in the hoisted form
- Pins:
- a runtime unit pin per face under
packages/runtime/src/: anonymous gets401 UNAUTHENTICATED, the service is never consulted, and a malformed body still gets401; - the analytics family added to
packages/qa/dogfood/test/showcase-anonymous-deny-surfaces.dogfood.test.ts, in the dispatcher-wrapper family, with a signed-in member as the200control; - an
enforcedrow carrying a gate pin inpackages/qa/dogfood/test/authz-conformance.matrix.ts, and the analytics dispatcher-domain key leavingpackages/qa/dogfood/test/authz-ledger-population.baseline.ts(shrink-only); - the checklist item
access-security.anonymous-deny-surfacesindocs/qa/platform-checklist/areas/access-security.jsongains the analytics variant and its step. This is a declared cross-lane surface (docs/qa/**), per the grade.
- a runtime unit pin per face under
- One
.changeset/21061-*.mdfor@objectstack/runtime. - ⛔ Not here: the
plugin-securityzero-set half (direction 2, split to its owndomain:servicescard;security-plugin.tsis held by PR fix(plugin-security)!: a caller who resolves no permission set is served a masked field masked and may not query on it #21051). Also not here:packages/spec/**and the principal-less context. ⚠️ Disclosure discipline: public text names doors, caller classes, codes and statuses only. ⛔ No request recipe in a PR body, changeset or test title. The private readings are not quoted.
(stop on a breach outside these; explain in the report)
Container & model:S,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable). P0 security; the grade sizes it S.
Clause-②: no (narrowing)
Clause-② reading: the analytics faces newly refuse an unauthenticated request they answered. That is an accept-set narrowing of a door against ADR-0056 D2, so the changeset carries the BREAKING banner under the launch-window convention. The dev re-reads this against the real diff.
Thread-read: 5924543711
Serial constraints cleared:No open PR touches packages/runtime/src/domains/analytics.ts, dispatcher-plugin.ts, the two dogfood files, the matrix, the baseline or docs/qa/platform-checklist/areas/access-security.json (the file lists of all open PRs were read in this act, main 4f83db5a73). PR #21051 holds plugin-security's security-plugin.ts, which is outside this surface. In flight on this seat: no dev. PRs #21043 and #21049 are in the merge queue, and neither touches these paths.
Generated by Claude Code
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsThe
plugin-securityhalf split off by this card's triage (5924543711) is filed as #21079 ·domain:servicesseat (#6021) ·session_01XY5uCwTjZj7884yYtyur4H· 2026-10-01T04:59Z ·⚠️ Classes and positions only.#21079 is
Blocked-by: #20995(PR #21051, in the merge queue). It names the known consequences to measure, and thedomain:specdocblocks the change would touch. This card's door half is unchanged by it.
Generated by Claude Code
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 21061,
"status": "done",
"branch": "claude/issue-21061-analytics-anonymous-deny",
"pr": "#21098",
"session": "session_01VvcEokUG1tvVxkceYfR5XB",
"premise_still_valid": true,
"summary": "handleAnalyticsRequest now opens with shouldDenyAnonymous as its first statement, ahead of the service-availability probe and the body validation, in the hoisted form domains/security.ts and domains/ai.ts use. The three analytics faces (cube read, SQL echo, meta) answer a caller without a session the dispatcher-wrapper 401 UNAUTHENTICATED; a signed-in member, an API-key caller and a system context are unchanged; no gate was added at the mount and nothing was copied into service-analytics. All four ruled pins landed: a runtime unit pin per face (analytics-anonymous-deny.test.ts); the analytics family in showcase-anonymous-deny-surfaces with a signed-in member as the 200 control; an enforced anonymous-deny-analytics matrix row carrying a GATE_PIN, with the /analytics dispatcher-domain key leaving the shrink-only baseline (MAX 33 to 32); and checklist item access-security.anonymous-deny-surfaces at revision 2 with the analytics variant, step and clause. Premise verified on origin/main 5dbeb7d before editing: 0 shouldDenyAnonymous calls in analytics.ts, and a stock boot answered an unauthenticated caller 200 on all three faces (table in repro). The claim's file surface needs one amendment by the seat: two files beyond it were required by gates the ruled change trips (see deviations).",
"tests": "HEAD 8364f40. (1) runtime unit pin: pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 src/domains/analytics-anonymous-deny.test.ts -> 29 passed, 0 skipped. (2) runtime full suite: pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 at 3905d4f -> Test Files 301 passed, Tests 5029 passed, 5 skipped. Later commits touch no file under packages/runtime (git diff --stat 3905d4f 8364f40 lists only system-context.mdx and authz-conformance.matrix.ts). Before the fixture update the same suite had 117 failures in 15 files, all of them analytics route fixtures that drove the route with no identity. (3) pnpm --filter @objectstack/runtime typecheck (tsc --noEmit + check:test-typecheck, tsconfig.test.json reaches src/**/*.test.ts) -> exit 0. (4) dogfood, run against the rebuilt runtime dist: vitest run test/showcase-anonymous-deny-surfaces.dogfood.test.ts test/showcase-anonymous-deny.dogfood.test.ts test/authz-conformance.test.ts -> 3 files, 114 passed. pnpm --filter @objectstack/dogfood typecheck -> exit 0. (5) ABLATION, run after the fix was committed. The mutation replaced the gate condition with a never-true comparison against a planted literal, through scripts/ablation-replace.mjs (anchor x1 to x0, blob 3bb9a967 to 5e8fdfd6). Restore was proven both times: blob equals HEAD and git diff HEAD is empty. A plant marker was used because the deleted spelling is not unique in runtime dist. Source legs: the unit pin went 18 failed / 11 passed (every anonymous case red, every signed-in control green); authz-conformance went 6 failed / 49 passed, naming STALE covers analytics:domains/analytics.ts:anonymous-gate and DEAD PROBE packages/runtime/src/domains/analytics.ts. Dist leg: pristine preflight --absent exit 0; mutate, rebuild, preflight present exit 0 (marker in dist/index.js and dist/index.cjs); showcase-anonymous-deny-surfaces went 9 failed / 46 passed (5 anonymous analytics cases, 3 envelope classifications, 1 shared code-and-message case red; all 5 analytics member controls green); restore, rebuild, preflight --absent exit 0 (marker absent from 6 built files, tree clean). Direction: red as predicted in every leg. (6) Lint, a proven narrowing: eslint --no-inline-config --format json over all 24 changed paths at 8364f40. Population from eslint's own config: 3 non-TS paths ignored by configuration, 21 linted. Count from the json output: 21 files, 0 errors, 0 warnings. Invariance: eslint.config.mjs has no parserOptions.project and no typed rules, and the diff touches no lint config or rule input. (7) pnpm check:platform-checklist: only the pre-existing identity-auth.json problem (see gates).",
"mcp_calls": "0",
"api_writes": "3, all through the fleet-write relay as POST /repos/objectstack-ai/objectstack/dispatches: (a) pr_create executing POST /repos/objectstack-ai/objectstack/pulls, draft #21098, body read back byte-identical (9282 bytes); (b) label-write --assign huangyiirene executing POST /repos//issues/21098/assignees, read back matched; (c) this os-dev-report comment executing POST /repos//issues/21061/comments. Plus git push of the branch (not REST).",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none (no PR or person is known to be touching these files) · noted, not filed — the anonymous-deny.ts docblock in @objectstack/core names the five dispatcher domains ai/meta/security/actions/automation, and the endpoint-policy.ts table names /meta, /ai, /security; /packages and now /analytics also hold the floor (documentation drift, PR Acceptance notes)",
"carrier: none · noted, not filed — check:platform-checklist is red on origin/main b3d7a70: areas/identity-auth.json anchors packages/plugins/plugin-auth/src/auth-plugin.ts#twoFactor, which is not declared there (manual-cadence ledger anchor drift; control run on main exits 1 with the same single problem)",
"carrier: none · noted, not filed — the /analytics rows of packages/runtime/src/route-ledger.ts carry no authz: declaration naming anonymous-deny-analytics (optional, phased field; the classified /actions and /packages rows carry none either)"
],
"gates": "dispatch-gates --repo objectstack-ai/objectstack --commands derived 93 families at 8364f40. All 93 were re-run after the final commit, with exit codes written to disk before reading: 92 exit 0, 1 exit 1. The red one is pnpm check:platform-checklist and predates this branch; control: the same command on origin/main b3d7a70 exits 1 with the identical identity-auth.json problem, and this branch touches neither that file nor the file it anchors. --ran reconciles 93 of 93 (0 NOT-MEASURED). Two gates were red on an earlier pass and are fixed in 8364f40: check-system-context-census (the new isSystem read site needed its anchor) and check:doc-authoring (a tracker id in the new row's summary string). Caveat: at reconcile time origin/main was at least 4 commits ahead (ci.yml, doc-line-anchors.mjs, regen-artifacts.mjs moved); that was not chased, and CI on the merge ref is the authority. CI on head 8364f40 at report time: 32 check runs, 11 success, 2 skipped, 19 in_progress (Lint & Repo Gates, Build Core and Temporal Conformance in_progress; Governed Surface Queue Guard success).",
"deviations": [
"File surface beyond the claim: (1) packages/qa/dogfood/test/authz-conformance.test.ts gets +1 GATE_PIN probe and +1 line in the exact-equality classified-key list. The ruled 'enforced row carrying a gate pin' can only be minted in that file's PROBES table, and its classified-key list is an exact equality that moves when the key leaves the baseline. (2) content/docs/permissions/system-context.mdx gets one anchor on row 51, and check-system-context-census --fix regenerated the counts 108 to 109. The floor reads ExecutionContext.isSystem, and that gate is red without the anchor. Both are declared in the PR body; the claim's file surface needs the matching amendment by the seat.",
"Fifteen existing runtime suites under packages/runtime/src drove the analytics route with no identity. They now carry a signed-in caller: an auth slot answering a session, or the dispatcher's timedResolveExecutionContext stubbed the way the /packages fixtures already do. Only identity changed; no expectation changed.",
"The branch merged origin/main once (3905d4f) so the gate derivation read a current tree, then refreshed install and build. No force-push.",
"shouldDenyAnonymous is passed method as well as userId/isSystem, as actions/automation/packages pass it; the security.ts/ai.ts precedent passes only userId/isSystem. Only OPTIONS differs: it passes the floor and falls through to the existing handled:false. The analytics mounts register no OPTIONS route.",
"The ablation planted a never-true comparison literal instead of deleting the call line. The deleted spelling also appears in automation.ts in runtime dist, so ablation-dist-preflight could not have used it as a unique marker.",
"A shell-scoping slip wrote one pid file to the container root (/build-base.pid). It was moved into the scratchpad within about a minute; no other file landed outside the worktree and scratchpad.",
"Lint was not run as full pnpm lint. A proven narrowing was used instead, with all three pieces of evidence in tests.",
"The private boot runs the invite_only audience posture, so the signed-in member for the private probe was created through the admin create-user door rather than by self sign-up."
],
"files_changed": [
".changeset/21061-analytics-anonymous-deny.md",
"content/docs/permissions/system-context.mdx",
"docs/qa/platform-checklist/areas/access-security.json",
"packages/qa/dogfood/test/authz-conformance.matrix.ts",
"packages/qa/dogfood/test/authz-conformance.test.ts",
"packages/qa/dogfood/test/authz-ledger-population.baseline.ts",
"packages/qa/dogfood/test/showcase-anonymous-deny-surfaces.dogfood.test.ts",
"packages/runtime/src/analytics-authored-cube-format-granularity.test.ts",
"packages/runtime/src/analytics-daterange-refusal-envelope.test.ts",
"packages/runtime/src/analytics-json-dimension-door.test.ts",
"packages/runtime/src/analytics-query-read-scope-withhold.test.ts",
"packages/runtime/src/cross-field-refusal-operand-withhold.test.ts",
"packages/runtime/src/declared-capability-absence-warn-once.test.ts",
"packages/runtime/src/dispatcher-5xx-always-logged.test.ts",
"packages/runtime/src/dispatcher-5xx-demoted-code-withhold.test.ts",
"packages/runtime/src/dispatcher-plugin.declared-5xx-prose-withhold.test.ts",
"packages/runtime/src/dispatcher-plugin.declared-user-message.test.ts",
"packages/runtime/src/dispatcher-plugin.error-envelope.test.ts",
"packages/runtime/src/dispatcher-validation-error.real.test.ts",
"packages/runtime/src/dispatcher-validation-error.test.ts",
"packages/runtime/src/domain-handler-registry.test.ts",
"packages/runtime/src/domains/analytics-anonymous-deny.test.ts",
"packages/runtime/src/domains/analytics.ts",
"packages/runtime/src/http-dispatcher.test.ts"
],
"repro": {
"boot": "stock showcase, pnpm dev -- --fresh; before = BASE 5dbeb7d, after = merged branch 3905d4f build (the fix is identical at 8364f40)",
"before": {
"cube_read": {
"unauthenticated": "200",
"signed_in_member": "200"
},
"sql_echo": {
"unauthenticated": "200",
"signed_in_member": "200"
},
"meta": {
"unauthenticated": "200",
"signed_in_member": "200"
},
"cube_read_malformed_body": {
"unauthenticated": "400 VALIDATION_FAILED",
"signed_in_member": "400 VALIDATION_FAILED"
},
"sql_echo_malformed_body": {
"unauthenticated": "400 VALIDATION_FAILED",
"signed_in_member": "400 VALIDATION_FAILED"
},
"control_record_door": {
"unauthenticated": "401",
"signed_in_member": "200"
}
},
"after": {
"cube_read": {
"unauthenticated": "401 UNAUTHENTICATED",
"signed_in_member": "200"
},
"sql_echo": {
"unauthenticated": "401 UNAUTHENTICATED",
"signed_in_member": "200"
},
"meta": {
"unauthenticated": "401 UNAUTHENTICATED",
"signed_in_member": "200"
},
"cube_read_malformed_body": {
"unauthenticated": "401 UNAUTHENTICATED",
"signed_in_member": "400 VALIDATION_FAILED"
},
"sql_echo_malformed_body": {
"unauthenticated": "401 UNAUTHENTICATED",
"signed_in_member": "400 VALIDATION_FAILED"
},
"control_record_door": {
"unauthenticated": "401",
"signed_in_member": "200"
}
}
}
}
Generated by Claude Code
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsClaim amendment: the file surface of
5924762402gains three gate-compelled items (os-dev-reportdeviations, PR #21098)domain:cliseat ·session_01VvcEokUG1tvVxkceYfR5XB· 2026-10-01T06:35Z ·⚠️ doors, caller classes, codes and statuses onlyAdded to the declared surface. Each item is compelled by a gate the ruled change trips; none widens the direction:
packages/qa/dogfood/test/authz-conformance.test.ts:- one
GATE_PINprobe, because the ruled "enforcedrow carrying a gate pin" can only be minted in that file'sPROBEStable; - one line in its exact-equality classified-key list, which moves when the analytics key leaves the baseline.
- one
content/docs/permissions/system-context.mdx: one anchor row, with the census counts regenerated bycheck-system-context-census --fix(108 → 109).- The floor reads the execution context's system flag, and that gate is red without the anchor.
- This is a declared cross-lane surface (
content/docs/**). It is a gate-maintained row, not prose.
- Fifteen existing runtime suites under
packages/runtime/src/that drove the analytics route with no identity now carry a signed-in caller, the way the/packagesfixtures already do. Only the identity changed; no expectation moved.
The review of record judges all three on the PR.
Generated by Claude Code
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsClaim amendment 2: two census files join the surface, for the
Dogfood Regression Gate (3/3)fix (PR #21098, new headfa039bb8c5)domain:cliseat ·session_01VvcEokUG1tvVxkceYfR5XB· 2026-10-01T06:52Z ·⚠️ doors, caller classes, codes and statuses only · follows5926076065and the PR note5926114686packages/qa/dogfood/test/authz-probe-blind-spot.census.ts, updated the way its header prescribes for a new probe:- the
PROBEStable shape 18 / 13 / 15 → 19 / 14 / 17, and the matrix header claim 18 → 19; - one
PROBE_FILE_CENSUSrow for the analytics domain file (a gate pin, blind spot 0), with its same-file positive controls; - the file added to the domain gate-pin list.
The blind-spot totals are unchanged.
- the
packages/qa/dogfood/test/authz-probe-blind-spot.test.ts: the "every classified key is accounted for" case asserts the table's key count, now 17, with its comment's figures brought current. No assertion is weakened or skipped.
The fix is one fast-forward commit, with no force-push. The four
authzand anonymous-deny dogfood files pass (149 tests), and the dogfood typecheck is green. The review of record is rendered on the new head.
Generated by Claude Code
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsACCEPT: PR #21098 at
fa039bb8(every analytics face the dispatcher routes refuses a caller without a session401 UNAUTHENTICATED, the shared floor first). P0 door halfdomain:cliseat ·session_01VvcEokUG1tvVxkceYfR5XB· 2026-10-01T07:13Z ·⚠️ doors, caller classes, codes and statuses only- Contract review of record:
5926559083on the PR,CONTRACT_REVIEW_TIER, headfa039bb8, PASS.- The gate:
shouldDenyAnonymousis the first executable statement ofhandleAnalyticsRequest, ahead of the availability probe and the body validator. It uses the dispatcher-wrapper envelope and the shared constants. - All three faces (cube read, SQL echo, meta) converge on that handler, and the legacy
HttpDispatcher.handleAnalyticsdelegates to it. An unknown sub-path is refused too. - The only other analytics door, the REST dataset query, already opens with
enforceAuth. No reach is left open at the analytics doors for the unauthenticated member of the class. - Unchanged: a signed-in member, an API-key caller (by the shared decision; not driven by a pin) and a system context.
- There is no gate at the mount and nothing in
service-analytics. ADR-0056 D2 is met at the door, and ADR-0090 D9 holds. - Pins:
- the runtime unit pin, per face × two anonymous shapes;
- the dogfood family, with member
200/400controls; - the
enforcedmatrix row with itsGATE_PIN, the baseline at 33 → 32; - the checklist item at revision 2, with history.
Each reds without the gate.
- The four gate-compelled additions (claim amendments
5926076065and5926289415) are each compelled. The fifteen runtime suites change identity only: everyexpectline is byte-identical. The census update weakens no assertion. - Semver:
@objectstack/runtimeminorwith the BREAKING banner,Clause-②: no (narrowing), ADR-0087not-required (no-migration-prescription). Right.
- The gate:
- Seat actions on adoption:
- The PR body gained an update section for head
fa039bb8c5: the census fix, the two files and its verification. Read back identical. This was the record's housekeeping flag. - At the head,
shouldDenyAnonymous(appears once indomains/analytics.ts. - A local
git merge-treeagainst the currentorigin/mainis clean.
- The PR body gained an update section for head
- Checklist:
- Ready to queue, base
main, first lineFixes #21061,Clause-②: no (narrowing). - 26 files, +657 / −92. NOT governed.
- 35 check-run names: 33 success, 2 skipped (path-conditional), 0 red. All seven required contexts are green, including
Dogfood Regression Gate (3/3), which was red on8364f40c4afor this PR's own census and is fixed on this head.
- Ready to queue, base
Fixescloses the card correctly: direction 2, the zero-set caller behind the door, is security(plugin-security): a non-system caller who resolves no permission set is admitted to every object and read with no row scope; an empty set list grants by absence instead of answering the deny baseline #21079 (domain:services) by the triage split.- Notes, not findings:
- The
anonymous-deny.tsdocblock list and theendpoint-policy.tstable header are drift in@objectstack/coreand@objectstack/runtime. Carrier: none. - The
check:platform-checklistred the report named is moot on currentmain(re-pointed by docs(qa): re-point the two-factor-disable mfa_required anchor at a declaration the resolver reads #21027). - The route-ledger
/analyticsrows may now carryauthz: 'anonymous-deny-analytics'under the docblock's own fill rule. It is a one-line follow-up for the next touch.
- The
- Next: land through the queue. At the merge, the
Fixescloses this card, and the seat removespm:dispatched.
Generated by Claude Code
- Contract review of record:
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsLanded: PR #21098 →
2bddb19ccb(every analytics face the dispatcher routes refuses a caller without a session401 UNAUTHENTICATED). The card is closed by theFixes, and the seat removespm:dispatcheddomain:cliseat ·session_01VvcEokUG1tvVxkceYfR5XB· 2026-10-01T07:42Z ·⚠️ doors, caller classes, codes and statuses only- Landing reading:
2bddb19ccbis onorigin/mainas a single-parent squash. Its diff is byte-identical to the PR's net diff at headfa039bb8c5.shouldDenyAnonymous(appears on 1 line ofpackages/runtime/src/domains/analytics.ts, against 0 at the parent. - State: the merge closed this card as
completed.pm:dispatchedis removed in the same act as this note.domain:cli,area:access,security,bugandpriority:p0stay. - Carried on: security(plugin-security): a non-system caller who resolves no permission set is admitted to every object and read with no row scope; an empty set list grants by absence instead of answering the deny baseline #21079 (
domain:services), the zero-set caller behind the door, by the emergency triage split.
Generated by Claude Code
- Landing reading:
- added 4 commits that reference this issue
on Oct 7, 2026
Filing gate: ① a product defect with a measured⚠️ Disclosure discipline: this card names doors, caller classes, codes and statuses only. Every reading is private.
reach:, under the possible-data-disclosure exception. P0 suspect, for the emergency triage path.reach:measured at a public door on a stock showcase boot (pnpm dev --fresh, no configuration). #20995's dev measured it during that card's reach step (os-dev-report5924254306on #20995,out_of_scope_findingsF1). The readings are in the dev's private scratch space, and this seat has read them. Filed by thedomain:servicesexecution seat (#6021,session_01XY5uCwTjZj7884yYtyur4H). ⛔ Not a claim.What was measured (by class)
The baseline fallback covers only callers with a user id.
200with aggregates over any object on the stock boot, platform identity objects included.401, and so does the analytics dataset door.maskingRule's describe and the result masker mask it — which one a public door serves is not measured #20995) closes the masked-field half only: grouping or filtering by a field whose masking rule applies is now refused403for this class. Grouping by any other field, and the object admission itself, are unchanged.Why it is not closed by #20995
#20995's surface is field masking (the projection answers, the query guards, the result masker). Object admission, the row scope and the door's authentication are other positions:
plugin-security, the permission-set-dependent admission and RLS paths that return early for an empty set;Scope for whoever takes it (⛔ not a ruling)
Reader who acts
Emergency triage (grade, route and P0 confirmation), then the owning seat.
plugin-securityisdomain:services. The analytics route's mount may bedomain:cliordomain:services, depending on where its authentication lives.Dedupe
mcp__github__search_issues, repo-scoped, open and closed, in the act that filed this card:/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, [finding] service-analytics read scope: compileScopedFilterToSql applies no whole-day upper bound and binds a temporal comparand as written, so an RLS $lte on a bare day drops the rest of that day in NativeSQL analytics #20733, finding: the #7929 read-scope disclosure is NOT analytics-only — an RLS$fieldfilter injected by the security middleware discloses the same policy column on the ordinary CRUD path, and predates #7598 #7988, 降级 analytics shim 把 ExecutionContext 丢在门口 ——/analytics/query在没装 service-analytics 的装配里不注入 RLS/租户谓词,契约字段where也被静默忽略 #3891 and security(analytics): read-scope 自动桥是插件顺序依赖的 — security 晚注册则 analytics RLS 静默全关 #3618 are other analytics or RLS defects, all closed.access: privatecredential-bearing identity objects that the sets themselves declare deny-by-design #20027 (closed, the shipped sets' blanket on identity objects), a different mechanism.maskingRule's describe and the result masker mask it — which one a public door serves is not measured #20995 (this caller class's field masking, PR fix(plugin-security)!: a caller who resolves no permission set is served a masked field masked and may not query on it #21051 in review).Dedupe words:
zero permission sets object admission·sessionless caller admission·no-set caller row scope·analytics cube read unauthenticatedGenerated by Claude Code