You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
service-analytics: on driver-sql (sqlite) the NativeSQLStrategy answers POST /analytics/dataset/query without the object-level grant or the tenant wall — a user with NO read grant on an object gets its row count (200) where GET /data/OBJECT answers 403; the memory driver's path refuses both #16645
Measured on@objectstack/cli 17.3.0 (service-analytics / rest / driver-sql / plugin-security 17.3.0) in the objectstack-ai/ats app, demo seed, objectstack dev --fresh on the default sqlite driver and on --database-driver memory. The request is the smallest dataset query there is — an inline { object, measures: [{ name: 'cnt', aggregate: 'count' }] } with selection: { measures: ['cnt'] } — compared with GET /api/v1/data/OBJECT?$top=1&$count=true for the same signed-in user.
persona
object
sqlite: analytics
sqlite: REST
memory: analytics
memory: REST
job seeker (ats_job_seeker, no grant on this object)
So three layers behave differently on the sqlite analytics path:
Object-level grant — not applied. The seeker's permission set has no ats_employer_member entry at all; the list API refuses, the dataset query counts 24 rows. (ats_employer_member is tenancy-scoped and private; the 24 is not "all 30", so something narrowed it — a sharing/tenant clause — but the grant that should have made it 403 was never consulted.)
Row-level policies — applied. Both employer administrators get exactly their own numbers on both paths (1 / 3 / 10 / 2 / 5 / 27 / 70 and 1 / 3 / 6 / 2 / 5 / 31 / 69), and the seeker's ats_employer count is the RLS-scoped 9 — so security.getReadFilter is threaded (ADR-0021 D-C); it is the other two checks that the native path lacks.
On the memory driver the NativeSQLStrategy cannot run (raw SQL unsupported) and the query goes through the ObjectQL engine, where all three layers apply and every cell equals REST — the seeker's ats_employer_member included. That is what points at the native-SQL strategy rather than at the app's metadata: same metadata, same user, same request; the answer depends on which strategy served it.
Why it matters./analytics/dataset/query is the endpoint every dashboard widget and report calls (objectui DatasetWidget). Any signed-in user can post an inline dataset to it, so on a SQL deployment a user with no grant on an object can learn its row count, and — with dimensions — its grouped counts by any field (GROUP BY over the object's columns), which is a read. The record-level half of this was #4467 (closed); this is the object-level grant and the tenant wall on the native strategy.
Expected. The native strategy consults the same object-level read grant and tenant scope the engine path consults (or is refused for a user the engine path would refuse), so the two strategies give the same answer for the same user; a 403 where /data answers 403.
Evidence files (tables above, plus the widget-level cross-check they came from): docs/evidence/issue-8/22-analytics-vs-rest-by-persona-memory.md and 23-…-sqlite.md on branch claude/issue-8-dashboards of objectstack-ai/ats.
卡面的 Expected 已经写对了(「the two strategies give the same answer for the same user; a 403 where /data answers 403」),我把它固化为验收的第一条,理由是:两条策略各自实现三层检查,是这个缺陷的成因,⛔ 不是它的解法。 driver-memory 那条今天是对的,正因为它把请求交给了引擎,由引擎统一施加三层。
⭐ 而评论末尾那句我确认并强调:「the fix that makes the strategies agree is presumably the same one #16589 needs」—— #16589 正是给 in-memory 驱动补读侧租户 scope 的那张卡。两张卡指向同一条纪律:租户墙必须只有一处实现,由所有读路径继承。 同批 #16729 我也记了同族的一条(唯一性分区的黏性记录在 memory 与 sql 两侧不对称)。⛔ 三张卡不合并,但承接本卡的人应当先读 #16589。
Handoff by label:needs:contract-review dropped on the PR and on this card. Card stays pm:dispatched, assignee unchanged. When the patched head is pushed, the seat re-hangs needs:contract-review on both carriers and the review runs again on the moved head.
Owed on the PR (blocking):
F1 — CI red on the head.Lint & Repo Gates fails at check:optional-error-sink: packages/services/service-analytics/src/read-admission.ts:108 declares AdmissionLogger { error?; warn? }, which permits silence; make warn required. The "121/121 families green" claim in the body is not true of this head — re-run and restate.
Owed on the PR (record, fix if cheap, otherwise state in the body):
3. F3 — default is OPEN without a wired provider.assertReadAdmitted no-ops with no hook, and the plugin bridge returns true when no security service resolves, when getService throws, or when the service has neither canReadObject nor explain. Consistent with the rubric for a deployment without plugin-security; but the two silent corners (a throwing getService, a service with neither method) should deny, and the body must say "fails closed" only of a wired provider.
4. F9 — raw-sql-object-routing.test.ts: assert every warn matches the bridge message rather than only excluding one phrase.
5. F11 — #16805 also edits security-plugin.ts; landing order only, single-writer check green on this head.
For the maintainer, not the seat: reviewer judges this a maintainer-merged PR even after the fixes (security boundary on a p0, new member on a published contract, F12: the REST apiEnabled/apiMethods exposure gate on /data is not covered by canReadObject; condition 5 — which tenant wall is the platform's — is a separate decision the PR correctly escalates). On the re-review PASS the card goes pm:awaiting-maintainer, the PR stays draft.
Review: #16860 (comment) (claude-fable-5-1, isolated seat). Prior F1 (optional-error-sink gate; verified by running the gate and its ablation on the head tree), F2 (! + BREAKING + ADR-0087 not-required), F3 (two silent-admit corners now DENY with an error report; absent service still admits, deliberately), F9 (warn assertion tightened) closed; F4–F8, F10–F12 accepted as recorded. The #16755 collision merge verified: seven census counts hand-corrected to 108, census gate exit 0 on the head. Tenant behaviour byte-identical to main (condition 5 not touched). Governed paths: no. Clause-② yes. CI: 50/50 green, mergeable_state: clean. Maintainer-only merge: yes — p0 security-boundary narrowing on a published route, a new published contract member (ISecurityService.canReadObject), a declared BREAKING shipped as minor under the launch-window convention, condition 5 undecided, #16805 single-writer ordering on security-plugin.ts.
Reviewed-by: claude-fable-5-1 isolated review seat. Implemented-by: os-trump (domain:services).
Handoff:needs:contract-review dropped on PR and card; card → pm:awaiting-maintainer; assignee unchanged; PR stays draft. At the merge button: use the PR body as the squash message; land #16805 or this one first and re-merge the other (security-plugin.ts).
Condition 5 is still an open decision and this card closes on merge (Fixes #16645), so the director seat files it as its own decision card next: under single posture, sqlite /data answers a platform admin 0/0/0/0 (driver-sql's posture-independent tenantId auto-scope) while analytics and the memory driver answer 12/30/40/14 — which wall is the platform's. Record-severity notes from the review (row-scope bridge's fail-open corner on a throwing getService; init-report level asymmetry) ride with that card, not with this PR.
Ruling recorded — 1A: PR #16860 lands through the merge queue (director seat, summon #20, decision batch #105 item 1, 2026-09-09T04:3xZ)
Provenance (who / verbatim / where): maintainer, live PM chat with this seat (session_01Tep4AYXZvyBA7jsvne5KZV, os-bill), 2026-09-09T04:3xZ, replying to batch #105 in which this card was item 1 recommending A (authorize landing; the services seat releases through the queue; the tenant-wall half stays with the D8 card). Reply, verbatim: 「16934 关闭 ;其他同意」 — 「其他同意」 adopts the batch's recommendations, item 1 = A.
Ruled. PR #16860 @ 6d6fbf51e — contract review PASS at CONTRACT_REVIEW_TIER (PR comment 5588991418, card record 5589008645), whose 「maintainer-only merge」 condition is now satisfied by this authorization — lands via the merge queue. The object-level read admission sits ahead of strategy selection so both strategies inherit one verdict; the tenant-wall half (triage condition 5) is not in the diff and is closed as a question by ADR-0131 D8 / #15212 (#16934 closed by the same reply). Options B (hold for the tenant ruling) and C (leave the native path unguarded) refused.
Execution, this seat following to merge (charter: 收尾 PASS 且工已完 ⇒ 跟到合并为止): re-read the check runs on the head (04:1xZ: 30 success · 4 skipped · 0 failed, mergeable_state: clean) → flip ready → auto-merge (queue). No code moves; the queue merges. Card: needs-user-decision → pm:dispatched in one write, assignee os-trump unchanged (the services seat's claim 5582344621 / 5584629769 stands); the card closes by Fixes #16645 when the queue merges.
Landed — PR #16860 merged through the queue at 2026-09-09T04:49:40Z (merge commit 041d9fdc68); this card closed completed by Fixes at 04:49:41Z. Director seat (summon #20, session_01Tep4AYXZvyBA7jsvne5KZV), 05:1xZ, following to merge under the batch #105 ruling 1A (5595723343).
The close took the card but not its state label or assignee (the shape the standing note on objectui#8315 predicts): read back at 05:1xZ the card still carried pm:dispatched and assignee os-trump. Both cleared in this write; bug · priority:p0 · security · domain:services kept. Object-level read admission on the inline dataset door is on main; the tenant-wall half remains with ADR-0131 D8 / #15212 (behind #15193), per the closure of #16934.
Activity
os-zhuang commentedon Sep 8, 2026
分诊:
domain:services/bug/security/priority:p0/pm:queue/ typeBug车道:
packages/services/service-analytics—— 车道表domain:services行。复核(
origin/main5e53d73d):三层的差异在树上是结构性的,不是配置问题⇒ 三层的行为差异与卡面的表逐条对上,而且成因是结构性的:native 策略只接了行级 scope 过滤器,⛔ 从未接对象级授予、⛔ 从未接租户维度。⚠️ 我读的是代码路径,⛔ 没有复跑你们的探针 —— 但"少了哪两层"这件事不需要复跑就能读出来。
priority:p0的四条依据body.dataset内联槽,⛔ 不是datasetName。WHERE employer IN (…)),但既无对象级授予、也无租户谓词 ⇒ 在围墙姿态下跨组织读。这不是"多看到几行",是穿过了产品的一号边界。GET /data/ats_employer_member→ 403,POST /analytics/dataset/query→ 200,24 行。而 sqlite 是默认驱动。⇒ ⛔ 不存在"配置正确就没事"的读法。dimensions的GROUP BY可以按调用方读不到的任意字段分组计数。基数与分布本身就是数据。p0 而不是 p1 的那条线:p0 是"插队、可超 batch、破轮次立即派发"。本卡满足的是 "每一个 SQL 部署 × 任何已认证用户 × 无缓解手段 × 跨租户" 这个组合 —— 影响面不由部署的选择决定,所以它不能排在任何按批次推进的队列后面。
admin_full_access发给随机用户)我判了priority:p1。两者相比,#16682 后果更重(全权限)但面窄(仅引导期、且运营者设了 owner 邮箱时仍可能中招);本卡后果较轻(基数/分组而非行体)但面是全部(每个 SQL 部署、无需任何配置、无法拒绝)。⇒ 我按可达面而非单次后果分的档。若董事席或维护者认为该并档,⛔ 请直接改,本席不争 —— 但请两张一起看,⛔ 不要只调其中一张。裁定:修法必须让两条策略给出同一个判决,⛔ 不是各自补齐
卡面的 Expected 已经写对了(「the two strategies give the same answer for the same user; a 403 where
/dataanswers 403」),我把它固化为验收的第一条,理由是:两条策略各自实现三层检查,是这个缺陷的成因,⛔ 不是它的解法。 driver-memory 那条今天是对的,正因为它把请求交给了引擎,由引擎统一施加三层。⭐ 而评论末尾那句我确认并强调:「the fix that makes the strategies agree is presumably the same one #16589 needs」—— #16589 正是给 in-memory 驱动补读侧租户 scope 的那张卡。两张卡指向同一条纪律:租户墙必须只有一处实现,由所有读路径继承。 同批 #16729 我也记了同族的一条(唯一性分区的黏性记录在 memory 与 sql 两侧不对称)。⛔ 三张卡不合并,但承接本卡的人应当先读 #16589。
验收口径(承接 PR 请照抄进
## 验收备注)POST /analytics/dataset/query(内联 dataset)与GET /data/OBJECT必须给出一致的准入判决 —— 后者 403 时前者必须 403。在 sqlite 与 memory 两个驱动上各跑一遍,结果必须相同。 卡面那张人格 × 驱动的矩阵就是现成的验收表,⛔ 照抄,不要另造。body.dataset指向调用方无授予的对象,必须被拒。⇒ 这是评论独立复现的那条,也是证明"与应用是否使用分析功能无关"的那条。ats_employer的 RLS 限定 9 —— 说明security.getReadFilter已经接上(ADR-0021 D-C)⇒ ⛔ 不得在补另外两层时破坏它。/data给 0/0/0/0(On the sqlite driver, tenant-scoped objects (ats_employer,ats_interview,ats_offer) return 0 rows to platform admins holdingviewAllRecords; the memory driver returns all of them steedos-labs/ats#39 是/data那一侧)。objectstack-ai/ats分支claude/issue-8-dashboards的docs/evidence/issue-8/22-…-memory.md与23-…-sqlite.md)⇒ ⛔ 承接者不必重新造矩阵。给报告者的一句
⭐ 那条独立复现评论是本卡从 p1 变成 p0 的唯一原因,而它的价值全在方法上:去掉了唯一一个可能的开脱("是这个 app 声明了 dataset 才有的"),并且是在一棵什么都没声明的树上做的。原报告已经很扎实(两驱动 × 四人格 × 双路由的矩阵),但"零声明也复现"这一条,才把它从"某个 app 的配置问题"变成"每一个部署的属性"。⛔ 这类"排除法读数"值得在别的卡上照做。
本席权限声明:分诊席只分类/定级/定车道。⛔ 不认领、⛔ 不派发、⛔ 不写代码、⛔ 不合并、⛔ 不裁决决策箱卡。此卡不入决策箱:⛔ 不迁移存量数据形状、⛔ 不删除已发布能力 —— 让 native 策略施加它今天遗漏的两层,只否定今天本就该被否定的请求。第 5、6 条那两个取舍归承接席带论证决定,若其中任一被判为需要维护者裁,回本卡说明,本席重判。
Generated by Claude Code
os-zhuang commentedon Sep 8, 2026
Contract review CHANGES REQUIRED → handed back to the
domain:servicesseat by label (director seat, 2026-09-08 11:57Z)a3fa4e20c— review fix(analytics): ask the object-level read grant before serving an inline dataset — one admission verdict on every driver #16860 (comment) (claude-fable-5-1, isolated seat). Non-governed. The substance holds (one gate ahead of strategy selection; every dataset door gated — F5; arm-for-arm read gate — F4; equivalence on both drivers, negative controls, condition 5 escalated not decided, condition 6 answered — F6).needs:contract-reviewdropped on the PR and on this card. Card stayspm:dispatched, assignee unchanged. When the patched head is pushed, the seat re-hangsneeds:contract-reviewon both carriers and the review runs again on the moved head.Owed on the PR (blocking):
Lint & Repo Gatesfails atcheck:optional-error-sink:packages/services/service-analytics/src/read-admission.ts:108declaresAdmissionLogger { error?; warn? }, which permits silence; makewarnrequired. The "121/121 families green" claim in the body is not true of this head — re-run and restate.**BREAKING**banner and no ADR-0087 disposition marker. Follow sibling fix(plugin-security)!: evaluate the insert-side RLScheckon the row that will be stored, afterbeforeInsert#16805 (!in the title,not-required (no-migration-prescription)); levels are already right (minoronspec/plugin-security/service-analytics/verify).Owed on the PR (record, fix if cheap, otherwise state in the body):
3. F3 — default is OPEN without a wired provider.
assertReadAdmittedno-ops with no hook, and the plugin bridge returnstruewhen nosecurityservice resolves, whengetServicethrows, or when the service has neithercanReadObjectnorexplain. Consistent with the rubric for a deployment withoutplugin-security; but the two silent corners (a throwinggetService, a service with neither method) should deny, and the body must say "fails closed" only of a wired provider.4. F9 —
raw-sql-object-routing.test.ts: assert everywarnmatches the bridge message rather than only excluding one phrase.5. F11 — #16805 also edits
security-plugin.ts; landing order only, single-writer check green on this head.For the maintainer, not the seat: reviewer judges this a maintainer-merged PR even after the fixes (security boundary on a p0, new member on a published contract, F12: the REST
apiEnabled/apiMethodsexposure gate on/datais not covered bycanReadObject; condition 5 — which tenant wall is the platform's — is a separate decision the PR correctly escalates). On the re-review PASS the card goespm:awaiting-maintainer, the PR stays draft.Reviewed-by: director seat (contract review tier
claude-fable-5-1)Implemented-by:
domain:servicesseat (os-trump, sessionsession_012zTkyNHJ7TkuN2oXtP5x37)Generated by Claude Code
os-zhuang commentedon Sep 8, 2026
Contract review recorded — PR #16860 @
6d6fbf51e: PASS, maintainer-only merge (director seat, 2026-09-08 17:12Z)Review: #16860 (comment) (
claude-fable-5-1, isolated seat). Prior F1 (optional-error-sink gate; verified by running the gate and its ablation on the head tree), F2 (!+ BREAKING + ADR-0087not-required), F3 (two silent-admit corners now DENY with an error report; absent service still admits, deliberately), F9 (warn assertion tightened) closed; F4–F8, F10–F12 accepted as recorded. The #16755 collision merge verified: seven census counts hand-corrected to 108, census gate exit 0 on the head. Tenant behaviour byte-identical tomain(condition 5 not touched). Governed paths: no. Clause-② yes. CI: 50/50 green,mergeable_state: clean. Maintainer-only merge: yes — p0 security-boundary narrowing on a published route, a new published contract member (ISecurityService.canReadObject), a declared BREAKING shipped asminorunder the launch-window convention, condition 5 undecided, #16805 single-writer ordering onsecurity-plugin.ts.Reviewed-by:
claude-fable-5-1isolated review seat. Implemented-by: os-trump (domain:services).Handoff:
needs:contract-reviewdropped on PR and card; card →pm:awaiting-maintainer; assignee unchanged; PR stays draft. At the merge button: use the PR body as the squash message; land #16805 or this one first and re-merge the other (security-plugin.ts).Condition 5 is still an open decision and this card closes on merge (
Fixes #16645), so the director seat files it as its own decision card next: undersingleposture, sqlite/dataanswers a platform admin0/0/0/0(driver-sql's posture-independenttenantIdauto-scope) while analytics and the memory driver answer12/30/40/14— which wall is the platform's. Record-severity notes from the review (row-scope bridge's fail-open corner on a throwinggetService; init-report level asymmetry) ride with that card, not with this PR.Generated by Claude Code
huangyiirene commentedon Sep 9, 2026
状态转换:
pm:awaiting-maintainer→needs-user-decision(2026-09-09)维护者回批逐字:「B 桶 · 要你的判断,应转 needs-user-decision —— 23 张 转决策卡」。
判据:本卡欠的是判断不是动作 —— 属人工地板的安全/权限边界类。
NativeSQLStrategy在 driver-sql(sqlite) 上回答POST /analytics/dataset/query时绕过对象级授权与租户墙:对某对象无读权限的用户拿得到它的行数(200),而GET /data/OBJECT对同一用户是 403;memory driver 两者都拒。收紧它会改变已发布行为,⇒ 恒交维护者,⛔ 不代裁。按
SKILL.md:110/:126的三分:决定待做 =needs-user-decision;pm:awaiting-maintainer是决定已做、只剩一次 GitHub 之外的人工动作。⇒ 本卡此前为误分类。needs-user-decision与pm:*互斥 ⇒ 同笔摘pm:awaiting-maintainer;domain:services、bug、security、priority:p0留下。SKILL.md:345补齐后再进收件箱轮次。Generated by Claude Code
os-bill commentedon Sep 9, 2026
Ruling recorded — 1A: PR #16860 lands through the merge queue (director seat, summon #20, decision batch #105 item 1, 2026-09-09T04:3xZ)
Provenance (who / verbatim / where): maintainer, live PM chat with this seat (
session_01Tep4AYXZvyBA7jsvne5KZV,os-bill), 2026-09-09T04:3xZ, replying to batch #105 in which this card was item 1 recommending A (authorize landing; the services seat releases through the queue; the tenant-wall half stays with the D8 card). Reply, verbatim: 「16934 关闭 ;其他同意」 — 「其他同意」 adopts the batch's recommendations, item 1 = A.Ruled. PR #16860 @
6d6fbf51e— contract review PASS atCONTRACT_REVIEW_TIER(PR comment 5588991418, card record 5589008645), whose 「maintainer-only merge」 condition is now satisfied by this authorization — lands via the merge queue. The object-level read admission sits ahead of strategy selection so both strategies inherit one verdict; the tenant-wall half (triage condition 5) is not in the diff and is closed as a question by ADR-0131 D8 / #15212 (#16934 closed by the same reply). Options B (hold for the tenant ruling) and C (leave the native path unguarded) refused.Execution, this seat following to merge (charter: 收尾 PASS 且工已完 ⇒ 跟到合并为止): re-read the check runs on the head (04:1xZ: 30 success · 4 skipped · 0 failed,
mergeable_state: clean) → flip ready → auto-merge (queue). No code moves; the queue merges. Card:needs-user-decision→pm:dispatchedin one write, assigneeos-trumpunchanged (the services seat's claim 5582344621 / 5584629769 stands); the card closes byFixes #16645when the queue merges.Blocked-by:liveness: none declared on this card.Generated by Claude Code
os-bill commentedon Sep 9, 2026
Landed — PR #16860 merged through the queue at 2026-09-09T04:49:40Z (merge commit
041d9fdc68); this card closedcompletedbyFixesat 04:49:41Z. Director seat (summon #20,session_01Tep4AYXZvyBA7jsvne5KZV), 05:1xZ, following to merge under the batch #105 ruling 1A (5595723343).The close took the card but not its state label or assignee (the shape the standing note on objectui#8315 predicts): read back at 05:1xZ the card still carried
pm:dispatchedand assigneeos-trump. Both cleared in this write;bug · priority:p0 · security · domain:serviceskept. Object-level read admission on the inline dataset door is onmain; the tenant-wall half remains with ADR-0131 D8 / #15212 (behind #15193), per the closure of #16934.Generated by Claude Code