Repository navigation
security(analytics): a field the caller may only see masked is answered unmasked as a grouped or filtered member on the native-SQL strategy; the published field reader has no masked-for-this-caller answer #20935
Description
Activity
objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsTriage: first grade —
bug·security·priority:p0·domain:spec·area:access·pm:queue. A seam card, dispatched vertically: the security contract publishes "not queryable for this caller", and the analytics gate asks itTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-09-30T20:56Z. ⛔ Not a claim, ⛔ not a dispatch. ⛔ This note carries no request recipe, no field spelling and no returned value (the card's disclosure discipline).Routing. The card names a seam:
spec:FieldSchema.maskingRule→ the security service's published reader → the analytics admission gate. By the lane rule, aSeam:card goes to the spec seat and is dispatched vertically. One claim declares all three surfaces:- the contract member in
packages/spec/src/contracts; - its implementation in
plugin-security; - the gate in
service-analytics.
⛔ No three-way split: at p0, the chain would only add hand-offs.
Re-read at
origin/mainbee75cebe6. The published readergetReadableFieldsis the memberpackages/spec/src/contracts/security-servicedeclares. A field that is readable only masked is, by that reader, readable. So the dev's reading, that no published answer exists for "masked for this caller", holds at the contract.Why p0. A caller entitled only to the masked form gets the unmasked value as a group or filter member on the native strategy. Masked fields are the most sensitive ones, and the engine and the ObjectQL strategy both refuse the same query. It is in 17.5.0, so the fix rides the next release. ⛔ #20639 is not held.
Direction.
- One derivation. The security contract gains a member beside
getReadableFieldsthat answers which fields this caller may not query on (group, filter, sort, aggregate), derived where masking is already decided. ⛔ The analytics layer does not re-derive masking. - The analytics gate (PR fix(service-analytics)!: one field-level read gate at the analytics door, before either strategy (#20917) #20931's, once landed) asks that member as well. ⛔ No second gate.
- Semver: it is an additive contract member. Its absence on an older implementation fails closed: the gate treats "unknown" as not queryable for masked-rule fields, never as open.
- Pins on both strategies: a masked-for-this-caller field as a group member and as a filter member answers the engine's
403. An unmasked reader of the same field is the control. - Serial after PR fix(service-analytics)!: one field-level read gate at the analytics door, before either strategy (#20917) #20931, which edits the same gate. See security(analytics): on the native-SQL strategy an inferred cube's relationship path reads the related object without that object's read admission or its row scope #20933 for the containment note; it applies here too.
Generated by Claude Code
- the contract member in
- 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 Sep 30, 2026 objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsClaim: PM loop round 30 · 2026-09-30T21:06Z
Session:session_01Sfe5YjBLwB9J3y8fvm2xq1
Account:os-justin(the seat's linked user asGET /useranswers it; the card's assignee from this act)
Branch:claude/issue-20935-masked-not-queryable
Worktree:objectstack-issue-20935
Domain:domain:spec(a vertical seam claim under triage5919588833: the contract member, itsplugin-securityimplementation, and the analytics gate)
Seat:domain:spec#5(seat post #19357)
Clause-②: yes (narrowing)
Scope: the triage Direction, taken whole.- One new member on the published security contract (
packages/spec/src/contracts/security-service.ts), besidegetReadableFields. It answers which fields this caller may NOT query on (group, filter, sort, aggregate). ⛔ It is not a second derivation: it is derived where the engine already decides that a masked caller cannot query the field. - The
plugin-securityimplementation of that member (packages/plugins/plugin-security/src/security-plugin.ts, and the service registration besidegetReadableFields). - The analytics gate that PR fix(service-analytics)!: one field-level read gate at the analytics door, before either strategy (#20917) #20931 landed (
packages/services/service-analytics/src/field-read-admission.ts, plus its wiring) also asks that member, before either strategy. ⛔ There is no second gate and no per-strategy copy. - When an older implementation has no such member, the gate fails closed for masked-rule fields. It never fails open.
- Pins, on both strategies: a masked-for-this-caller field as a group member, and as a filter member, answers the engine's
403. An unmasked reader of the same field is the control. Each pin is ablated. ⚠️ The card's disclosure discipline holds throughout. No request recipe, field spelling or returned value from the private measurement appears in any comment, PR body or changeset. Tests use synthetic fixtures.
File surface (stop on breach and explain it in the report):- the three sites above and their tests;
packages/resttests only if the door-level pin needs them (PR fix(service-analytics)!: one field-level read gate at the analytics door, before either strategy (#20917) #20931's precedent);- generated projections, regenerated only;
.changeset/.
⛔ No change to the masking rule itself. ⛔ security(analytics): on the native-SQL strategy an inferred cube's relationship path reads the related object without that object's read admission or its row scope #20933's relationship-path admission belongs todomain:services.
Container & model:S,mode:subagent,model: opus.
Review: an at-tier contract review is owed before enqueue.
Thread-read: 5919588833
Serial constraints cleared: read at this stamp.- PR fix(service-analytics)!: one field-level read gate at the analytics door, before either strategy (#20917) #20931 (the gate) is merged as
1571aedce5. - No open PR touches
contracts/security-service.ts,plugin-security/src, orfield-read-admission.ts. - Open PR fix(service-analytics)!: the nested-relation filter gets the engine's answer on every analytics face — the related object read as the caller, capped (#20887) #20916 touches
service-analytics/src/analytics-service.ts. At p0 this claim does not wait on it: wiring edits there stay minimal, and a conflict is resolved by mergingmain.
Generated by Claude Code
- One new member on the published security contract (
objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 20935,
"status": "done",
"branch": "claude/issue-20935-masked-not-queryable",
"pr": "#20955",
"session": "session_01Sfe5YjBLwB9J3y8fvm2xq1 (mode:subagent; the dispatching session's id, as the claim names it)",
"premise_still_valid": true,
"summary": "ISecurityService gains an optional getQueryableFields(object, context): the fields a caller may filter, sort, group or aggregate by, a subset of getReadableFields that differs from it by exactly the fields the caller is served masked; its absence (or an undefined answer) obliges consumers to fail closed on every maskingRule field. plugin-security's predicate guard (step 2.9) and aggregate-input guard (step 2.5b) now share one private derivation (computeQueryGuardFieldPerms), and getQueryableFields reads that same map, so the published answer and the engine's refusal cannot drift; both guards refuse exactly what they refused before. The #20917 analytics field gate asks the new answer beside the read projection at the same door point, before either strategy, refusing a masked member 403 PERMISSION_DENIED in the engine's own words; AnalyticsServicePlugin bridges it to the security service and, for a service that predates the member or answers undefined, fails closed (read projection less every field declaring a maskingRule, for every caller, system included, so no new elevation read site). Premise re-read at origin/main: the reader counted a masked field readable, the engine guards refused it, and nothing published the difference. Main was merged twice (never rebased); PR #20916 has not landed, and the analytics-service.ts wiring is kept to the config member, the stored provider, a construction-time warning and one call-site argument.",
"tests": "All at fc844c4 unless stated. (1) @objectstack/service-analytics: test 147 files / 3398 passed at e84882a; after the later edits the two field-gate unit files 72/72 and typecheck exit 0 (the new test file is in the tsc program: it reported TS2493 before its fix). (2) @objectstack/plugin-security: test 150 files, 3237 passed / 23 skipped; typecheck exit 0 including check:test-typecheck. (3) @objectstack/spec: typecheck exit 0; src/contracts 45 files / 434 passed; check:generated exit 0, all 15 artifacts up to date against a dist built at the merged tree. (4) @objectstack/rest: all 13 analytics-* route files, 175 passed / 3 skipped; typecheck exit 0 incl. test layer; the two field-gate route files re-run 22/22 at 03153e0 after the second merge. (5) Gates: dispatch-gates --commands --repo objectstack-ai/objectstack derived 87 at fc844c4 (not stale). 84 exit 0 first time; check:dual-build-cjs-loads, check:i18n and check:type-check-debt first exited 3 (PREREQUISITE NOT MET: no dist for 44 packages / CLI unbuilt), then exit 0 after building their stated prerequisites (the i18n closure, then turbo build over ./packages/* and ./packages//). dispatch-gates --ran over the final record: 87 derived, 87 run, 0 NOT-MEASURED, exit 0 (over the raw record it reads the three as NOT-MEASURED, which is why the final record keeps the last reading per command). check-changeset-no-major and check-adr-0087-registration --base origin/main both exit 0. (6) Lint, a proven narrowing, not a run of pnpm lint: population = the 9 changed .ts files, isPathIgnored false for all 9 (eslint's own config); eslint --no-inline-config --format json reads 9 files, 0 errors, 0 warnings; parserOptions.project and projectService unset for all 9, so no type-aware rule runs and no untouched file's verdict can move. (7) Ablations, predicted before running, at 20c56c9, each via scripts/ablation-replace.mjs (anchor hit once, blob moved), dist rebuilt with ablation-dist-preflight marker present in 2 built files where the suite reads dist, restore proven blob == HEAD with git diff HEAD empty, rebuilt, --absent preflight marker gone from all 6 built files and tree clean. A (gate ignores the queryable answer, field-read-admission.ts): route pins 4 red / 18 green exactly as predicted (red on each composition: cube read + SQL echo, and dataset door; native answered rows, ObjectQL printed the statement on the echo and reached the engine on the dataset door; all 12 #20917 route pins green); unit pins 19 red / 53 green as predicted. B (plugin-security stops registering getQueryableFields): route pins 2 red / 8 green as predicted (member refusals held by the fail-closed fallback; the unmasker control red on both compositions); plugin-security 1 red / 9 green. C (the shared derivation stops folding masked fields in; src-resolved, no build leg): 8 red / 38 green, the 6 predicted get-queryable-fields reds plus the two pre-existing field-masking-rule engine pins (filter and aggregate over a masked field), showing both engine guards read the one derivation. Commits after 20c56c9 touched only the fallback's system exemption, a branch none of the three ablations reaches.",
"mcp_calls": "0",
"api_writes": "3 relay strokes (fleet-write repository_dispatch, run as objectstack-fleet[bot]): pr_create POST /repos/objectstack-ai/objectstack/pulls (PR #20955, draft forced; body read back 11492 bytes sent = stored); assign POST /repos//issues/20955/assignees (os-justin, via label-write.mjs, read-back matches); this os-dev-report POST /repos//issues/20935/comments (via post-stamped.mjs). No labels written (the dispatch named none; skip-changeset does not apply because the PR carries changesets). git push is not a REST write. OS_FLEET_SESSION was exported in the shell rather than spelled as a command prefix; the route read it the same way.",
"open_questions": [
{
"question": "The PR body's declaration is the claim's line verbatim, Clause-② yes (widening), while the diff also narrows the analytics accept set (native-SQL and SQL-echo answers for masked members are now refused). The narrowing is declared in the service-analytics changeset (BREAKING banner, its own Clause-② yes (narrowing) line, ADR-0087 not-required (no-migration-prescription)), as #20917's changeset did; every bumped package is minor. Keep, or should the seat change the PR line?",
"options": [
"A keep: the PR line verbatim, the narrowing carried by its changeset where check-adr-0087-registration reads it",
"B the seat rewrites the PR line to yes (narrowing), which the level gate also accepts at minor"
],
"recommendation": "A, because the claim's line is the seat's declaration and the dev does not rewrite it, both gates already exit 0, and the narrowing is declared where the ADR-0087 gate reads it."
}
],
"out_of_scope_findings": [
"class: b · reach: exception: security (could leak data; NOT measured through a public door, read from source only) · Seam: spec:FieldSchema.maskingRule → runtime:plugin-approvals ApprovalService.redactPayloads via payload-redaction.ts | consumer: the approvals inbox serve path · evidence: payload-redaction.ts's header states that the approval snapshot redaction enforces the subject object's field-level read controls including a maskingRule, but it narrows by getReadableFields, which counts a field the caller is served masked as readable, so by reading the snapshot's value for such a field would be served as stored rather than masked. Same family as #20935 (a consumer treating the read projection as the whole field-level answer), different remedy (read plane: apply the mask, not refuse a query position); getQueryableFields does not close it · dedupe words:approval payload masked field·redactPayloads maskingRule·approval snapshot getReadableFields masked",
"carrier: none · noted, not filed: MemoryAnalyticsService (driver-memory's own cube face) is neither touched nor measured here",
"carrier: none · noted, not filed: a live PostgreSQL cell is NOT MEASURED; the gate answers before a strategy is chosen or a statement compiled, so the dialect does not enter the verdict"
]
}
Generated by Claude Code
objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsRuling on report
5921193653· 2026-09-30T23:03Zdomain:specseat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1), holder of claim5919731088.⚠️ The card's disclosure discipline holds in this ruling: no request recipe, field spelling or returned value.- Verified against GitHub: PR fix(security,service-analytics)!: the security contract publishes which fields a caller may query on, and the analytics field gate refuses a masked field as a group or filter member (#20935) #20955 is at head
fc844c433e, the head the report names. It has 12 files (+1243 / -47), is a draft, is assigned to os-justin, and saysCloses #20935followed by the claim's Clause-② line. - Q1 (the PR's
Clause-②line while the analytics accept set also narrows): A, keep it. The gate reads ONE arm per PR (scripts/check-changeset-no-major.mjs,arm: 'widening'|'narrowing'|null), and both arms owe the same grade.yes (widening)is true:getQueryableFieldsis a new member on the published contract. The narrowing is declared where the ADR-0087 gate reads it: the service-analytics changeset's BREAKING banner and its ownyes (narrowing)line. 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's changeset did the same. - Accepted as delivered:
- one derivation (
computeQueryGuardFieldPerms), read by both engine guards and by the published answer; - the gate at the 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 door point, before either strategy;
- fail-closed for an implementation that predates the member.
- Ablation leg C shows that the two engine guards and the new answer read the same map.
- one derivation (
- Out-of-scope findings:
- The approvals snapshot redaction narrows by
getReadableFields, so a masked-for-this-caller field may be served as stored. This was read from source and is not yet measured. It is filed as security(approvals): the approval snapshot redaction narrows bygetReadableFields, which counts a field the caller is served masked as readable, so such a field may reach an approver as stored rather than masked (source-read, not yet measured) #20964 (security family, bare for triage, with the same disclosure discipline). MemoryAnalyticsServiceand a live PostgreSQL cell are noted as NOT MEASURED. The gate answers before a strategy or dialect enters.
- The approvals snapshot redaction narrows by
- Next:
needs:contract-reviewgoes on the PR and this card, then the CI wait, then the at-tier review.
Generated by Claude Code
- Verified against GitHub: PR fix(security,service-analytics)!: the security contract publishes which fields a caller may query on, and the analytics field gate refuses a masked field as a group or filter member (#20935) #20955 is at head
3 remaining items
objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsLanded: PR #20955 →
83480c6a2f(p0); the card is done · 2026-09-30T23:43Zdomain:specseat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1), holder of claim5919731088.⚠️ The disclosure discipline holds.- Landed: merged at 2026-09-30T23:41Z as
83480c6a2f, with one parent (b1aee33f88, read withgit rev-list --parents). All 12 files have the same per-filegit patch-id --stableas the reviewed headfc844c433eagainst the merge base95fed33a20. Review record:5921385686. - Delivered:
ISecurityService.getQueryableFields, derived inplugin-securityfrom the one map the engine's guards read;- the analytics field gate refuses a masked-for-this-caller query member before either strategy;
- an implementation without the member fails closed.
- Closed by
Closes #20935at merge. The seat now removespm:dispatchedand the assignee. - Still owed: a one-file follow-up (Part of this card) that corrects two changeset sentences before the release PR consumes them: "A system context is unaffected" is over-broad, and "on a SQL deployment" is over-narrow. It is dispatched when a slot frees. The approvals read-plane finding is security(approvals): the approval snapshot redaction narrows by
getReadableFields, which counts a field the caller is served masked as readable, so such a field may reach an approver as stored rather than masked (source-read, not yet measured) #20964.
Generated by Claude Code
- Landed: merged at 2026-09-30T23:41Z as
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsDispatched: the changeset-wording follow-up (Part of this card) · 2026-10-01T00:22Z
domain:specseat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1), holder of claim5919731088.⚠️ The disclosure discipline holds.- Branch
claude/issue-20935-changeset-wording, oneos-dev, Part of security(analytics): a field the caller may only see masked is answered unmasked as a grouped or filtered member on the native-SQL strategy; the published field reader has no masked-for-this-caller answer #20935. It edits one file,.changeset/20935-analytics-masked-field-not-queryable.md, which is still pending onmain. - Scope: the two sentences the review adoption
5921385686on PR fix(security,service-analytics)!: the security contract publishes which fields a caller may query on, and the analytics field gate refuses a masked field as a group or filter member (#20935) #20955 named.- "A system context is unaffected" is over-broad: under the fallback for a security service that predates the member, system is refused too.
- "on a SQL deployment" is over-narrow: the SQL echo narrows on both strategies.
- The edits are cuts or corrections. ⛔ No new claims.
- Why now: release PR chore: version packages #20639 consumes pending changesets when it merges, so the correction has to land before that.
Generated by Claude Code
- Branch
- added a commit that references this issue
on Oct 1, 2026 objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 20935,
"status": "done",
"branch": "claude/issue-20935-changeset-wording",
"pr": "#20991",
"session": "session_01Sfe5YjBLwB9J3y8fvm2xq1 — this run's harness-stamped id (commit Claude-Session line); mode:subagent, so it is the seat's own id",
"premise_still_valid": true,
"summary": "Draft PR #20991 (Part of #20935, Clause-②: no) edits one file, .changeset/20935-analytics-masked-field-not-queryable.md, which is pending on main (base 05be352, head fa6f9f7, +6/-4). (1) The BREAKING banner now reads 'BREAKING for analytics queries that group, aggregate, filter or sort by a field the caller may only see masked: on a SQL deployment, and on POST /api/v1/analytics/sql whichever strategy serves the cube.' Evidence: generateSql() runs callCtx, the same admission as query(), before resolveStrategy, and the ObjectQL strategy's generateSql renders the statement itself without reaching the engine's query guards. (2) 'What is not affected' now opens with 'Unless the security service predates getQueryableFields or answers "no answer" (see New hook):' ahead of its three sentences, which are otherwise kept. Evidence: the plugin.ts bridge fallback is getReadableFields minus every field that declares a maskingRule, and it reads no caller property. getReadableFields gives a system context the full set, so a system caller is refused there too. Pinned by field-query-admission-gate.test.ts 'fails CLOSED for a security service that predates getQueryableFields', which also refuses SYSTEM. Scope: one qualifier covers the capability-holder sentence as well, because the same fallback over-refuses it (plugin.ts's comment says so). This is a bounded in-place fix. All four conditions hold: same defect class; mechanical, in the form the review record gave; same file, no other claim on it; no new verification surface. The PR body declares it. Kept as they were: the summary line, the changeset's Clause-② line, the ADR-0087 marker, and the BREAKING marker. Conflict named: the order says to keep the banner 'as it is' and also to correct line 11, which is the banner. I read that as keeping the BREAKING marker and correcting its scope words. breakingDeclaration on the head file still returns breaking:true with signals BREAKING, bang and clause-②-narrowing, and the disposition reads not-required (no-migration-prescription). skip-changeset: not applied. pr-automation.yml route 0, plus check-empty-changeset.mjs's DELIBERATE CORRECTION class and ruling D on #18375, refuse that label on a PR that edits an existing changeset. The PR body carries the written confirmation request, so Check Changeset stays red by design; it is not a required context. Clause-② re-read in scripts/pm/clause2-line.mjs: a text-only edit widens no accept set and adds no surface, so 'no'. An absent arm declares no direction, and no (narrowing) would declare a breaking change this PR does not make.",
"tests": "Gate union at fa6f9f7 (git rev-parse --short HEAD after the final commit; no later push). node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack exited 0: tree objectstack-ai/objectstack at fa6f9f7, --repo holds, 1 path vs merge base 05be352, 19 commands. Results, each captured before any pipe — exit 0 (18): check-adr-0087-registration.mjs --base origin/main and --self-test; check-changeset-no-major.mjs --base origin/main ('This diff introduces no major bump'; the level axis is NOT APPLICABLE locally because there is no pull_request payload) and --self-test; check-closing-keyword-parity.mjs and --self-test; check-comment-mask-corpus.mjs; check-empty-changeset.mjs --self-test; pm/release-rehearsal-clone.mjs --self-test; pnpm check:changeset-gate-self-tests, check:driver-memory-census, check:gitlink-declared, check:nul-bytes ('OK (scanned 9602 text file(s) ... no raw ASCII control bytes)'), check:objectui-changeset, check:pm-changeset-deadline-census, check:published-files, check:refd-timer-probe, check:watch-hint-literal. Exit 1 (1, expected): check-empty-changeset.mjs --base origin/main says the file is 'present on the merge base and CHANGED by this PR', the DELIBERATE CORRECTION class, so the remedy is written confirmation on the PR. Extra: node scripts/check-changeset-fixed.mjs, whose roster is under .changeset, exited 0. Reconciliation: dispatch-gates --ran with the exit-coded record exited 0, '19 derived, 19 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero)'. The ADR-0087 gate prints '1 non-breaking changeset(s) seen'. That count is its skipped list, which also holds inherited-breaking files, and a direct breakingDeclaration call confirms the head file is still breaking. Control-byte self-scan of the changeset and the PR body: zero hits. PR body read-back: the relay reported 8062 bytes sent and 8062 stored, identical, and one REST GET returned draft:true, assignee os-justin, body byte-identical. NOT MEASURED, by design: type-check lanes and package tests, because the diff touches no TypeScript or package source; repo-level lint belongs to CI. Ablation: none — a text-only edit has nothing to ablate. Not run live: the echo behaviour is cited from code reading and the named pin, not from a running server.",
"mcp_calls": "0",
"api_writes": "3 — fleet-write relay dispatches (POST /repos/objectstack-ai/objectstack/dispatches), each executed as objectstack-fleet[bot]: (1) pr_create, draft forced → POST /repos/objectstack-ai/objectstack/pulls, giving #20991 (run 36796883771); (2) label-write --assign os-justin → POST /repos//issues/20991/assignees (run 36796939546); (3) this os-dev-report comment through post-stamped.mjs → POST /repos//issues/20935/comments. Not REST writes: two git pushes (the empty branch as the claim marker and route probe, then fa6f9f7). Reads: three single REST GETs (comment 5922157430, comment 5921385686, pull 20991) plus the tools' own route, pace and label read-backs. Write budget before the first write: 12 of 40 in the trailing hour.",
"open_questions": [
{
"question": "Ruling D route: the corrected note needs written confirmation on PR #20991 before it lands, and Check Changeset stays red. Who confirms?",
"options": [
"A — the seat confirms in writing on #20991, as the owner of the PR #20955 review adoption that ordered these two corrections",
"B — escalate to the maintainer"
],
"recommendation": "A, because the order and the review record are the seat's, the edit is two scope corrections that record already spelled out, and the PR touches no governed surface."
},
{
"question": "The #20917 and #20933 notes carry the same over-narrow banner (see out_of_scope_findings), and release PR #20639 consumes all three when it merges. Correct them before it does?",
"options": [
"A — one more one-file follow-up per note (or one PR for both), under each card's own claim, before #20639 merges",
"B — leave them; once released, correct only by amending the CHANGELOG entry in a docs-only PR"
],
"recommendation": "A, because a pending note is cheap to correct now, while a released one can only be amended in its CHANGELOG entry and its tarball keeps the old text."
}
],
"out_of_scope_findings": [
"class: a · reach: exception: release-text (pending changesets; #20639 publishes them) · The BREAKING banners of .changeset/20917-analytics-field-permission-gate.md ('BREAKING for analytics queries on a SQL deployment that read a field the caller may not read.') and .changeset/20933-analytics-relationship-path-admission.md ('... on a SQL deployment that read a related object through a relationship path the cube does not declare.') are over-narrow in the same way. Each change's gate sits in callCtx, which generateSql() shares, ahead of strategy choice, and the ObjectQL strategy's generateSql never reaches the engine's guards. So the /analytics/sql echo moved from printed to 403 on both strategies. Evidence for #20917: at its pre-merge parent 95555e7, git grep for getReadableFields or assertFieldsReadable over service-analytics src (non-test) exits 1 with zero hits; the control at 1571aed exits 0 with 3 hits in analytics-service.ts; and the parent's ObjectQL generateSql has no engine call. Evidence for #20933: at its parent 83480c6, queryObjects equals cubeObjects (base plus declared joins only) and feeds assertReadAdmitted in callCtx; the head adds relationship-path objects. Read, not run live. Not edited, per the order. Dedupe words: changeset banner SQL deployment; analytics/sql echo both strategies; 20917 changeset wording; 20933 changeset wording.",
"carrier: none (承接者:无) · noted in PR #20991 Acceptance notes, not filed · Two wording observations in the 20935 note, left untouched under 'no new claims'. (i) The banner does not say that the fallback also refuses callers who see a field unmasked; the corrected 'What is not affected' qualifier and 'New hook' cover it. (ii) 'New hook' says 'for every caller', but under the fallback a reader that also answers 'no answer' judges no field. Class: none; text precision, not a defect.",
"carrier: none (承接者:无) · noted, not filed · scripts/check-adr-0087-registration.mjs prints 'N non-breaking changeset(s) seen' from a skipped list that also holds files breaking at the merge base (inherited), so a PR editing a breaking pending note is reported as having seen a 'non-breaking' one. Output wording only; the verdict is right."
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsThe changeset-wording follow-up is closed by the maintainer's answer
domain:specseat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1) · 2026-10-01.- PR docs(changeset): correct two scope sentences in the pending service-analytics masked-field note #20991, which proposed the two corrections from the review adoption
5921385686, is closed unmerged. The maintainer answered B (「20991 20977 都不改」, 2026-10-01). - The note ships as written. Nothing further is owed on this card.
Generated by Claude Code
- PR docs(changeset): correct two scope sentences in the pending service-analytics masked-field note #20991, which proposed the two corrections from the review adoption
- added 8 commits that reference this issue
on Oct 7, 2026
Filing gate: ① a product defect with a measured reach. Class a. Security family, and it takes the "could leak data" exception. Filed by the
domain:servicesseat (#6021, sessionsession_01XY5uCwTjZj7884yYtyur4H) from #20917's dev report (5919006124,out_of_scope_findings[0]). ⛔ Filed bare: grading and routing are triage's. ⛔ Not a claim.What was measured
Measured by #20917's dev on the analytics routes with the real
SecurityPlugin,ObjectQLandSqlDriver, as a caller who sees one field only partially masked: amaskingRulefield whose unmask requirement the caller does not hold. The measurement is private.engine.find,engine.aggregateand the analytics ObjectQL strategy refuse it403.Why #20917's gate does not close it: the gate (PR #20931) is exactly as wide as
ISecurityService.getReadableFields, the published reader. That reader reports a masked field as readable, because it is a served column, masked. The published contract carries no reader for "masked for this caller", so no consumer outside the engine can apply the rule.Seam:
spec:FieldSchema.maskingRule→ the security service's published reader →AnalyticsService's admission gate.Direction (proposed, triage's to set)
getReadableFields(a contract member in thespecandplugin-securitylanes), and the analytics gate asks it. ⛔ No second derivation of the masking rule in the analytics layer.403on both strategies. An unmasked reader of the same field is the control.Reader who acts
Triage's first grade. It spans a contract member (
domain:spec,plugin-securityindomain:services) and the analytics gate (domain:services), so triage sets the lane or the split.Dedupe (queries run before filing, closed included)
security.explainnot reporting partial masking. spec/security: field masking is all-or-nothing — no partial masking (phone last-4, ID middle-8), andmaskingRulewas pruned as dead in 2026-06 #8993 and finding(spec): the protocol declares no masked field-type set — objectql'scollectMaskedReadFieldsand objectui'sMASKED_FIELD_TYPESeach own a copy of one fact #20141 (closed) are the masking feature and its type set. analytics: /analytics/query ignores record-level scoping — a member counts and reads dimension values of records they cannot read #4467, security: the analytics ObjectQL execute face answers a row-level read scope it cannot run withINVALID_FILTER/ 400 whose message echoes the policy's field name and comparands — the disclosure #5367 closed for the native / echo faces #19995 and others (closed) are other analytics positions. None is a masked field answered unmasked on the analytics native path.Dedupe words:
analytics masked field native SQL·maskingRule getReadableFields queryable·masked field group filter analyticsGenerated by Claude Code