Repository navigation
feat(spec): declare the optional ISecurityService.describeEffectiveAccess and its EffectiveAccess answer types - #31256
Conversation
…cess and its EffectiveAccess answer types Three TS interfaces beside DelegableScope in contracts/security-service.ts (EffectivePermissionSetOrigin, EffectivePermissionSet, EffectiveAccess) and one optional member after describeDelegableScope: a user's effective permission sets, each with its origin (default binding, held position or direct grant), resolved by the enforcement resolver and gated as explain is. Optional, so a security service that predates it still satisfies the contract. Type-level pins in security-service.test.ts; spec minor changeset. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5odNVgHTd2MQbQCp5t
…effective-access types gen:api-surface and gen:export-origins, the two artifacts check:generated proved stale; each gains exactly EffectiveAccess, EffectivePermissionSet and EffectivePermissionSetOrigin. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5odNVgHTd2MQbQCp5t
Metadata-protocol is none (a TS contract member, no authorable metadata), and the level follows the metadata protocol alone, so the spec entry is a patch; check-changeset-no-major refuses minor with a none declaration. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5odNVgHTd2MQbQCp5t
📓 Docs Drift CheckThis PR changes 1 package(s): 40 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 13 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 171 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 371414eb22bed04c22e4b1a4cf9a08d025f29f1d && git checkout 371414eb22bed04c22e4b1a4cf9a08d025f29f1d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ebc2cdc52207e0e755a910afeda65a39630a84fa 84a719f5d16889cb592562e25d1f067335cd15ec && git checkout -B drift-repro ebc2cdc52207e0e755a910afeda65a39630a84fa && git merge --no-ff 84a719f5d16889cb592562e25d1f067335cd15ec
node scripts/docs-audit/affected-docs.mjs --json ebc2cdc52207e0e755a910afeda65a39630a84fa
|
|
domain:spec#1 · os-project-manager · session_01FEkQ5mw7HFYn9o4LEGMQdK Contract-review-claim: session_01FEkQ5mw7HFYn9o4LEGMQdK · head 80410d2 · 2026-11-07T20:04Z |
|
domain:spec#1 · os-project-manager · session_01FEkQ5mw7HFYn9o4LEGMQdK Contract reviewServed-tier: Inputs: card #31238 (body, claim 6711887108, dev report 6712357054 with ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
What holds:
What breaks:
M1 is a must-fix: a contract declared ahead of its implementer that cannot express a shipped resolution path binds #31215 to a list that either lies about enforcement or breaks its own invariant. Implemented-by: VERDICT: FAIL |
… and the contract states order, positions and the unknown-user departure from explain Contract review fixes on the describeEffectiveAccess declaration: - a fourth origin kind, derived, with a closed source: platform_admin_config (OS_PLATFORM_OWNER_EMAIL confers admin_full_access with no grant row or position) and ai_access (sys_user.ai_access confers ai_seat), so every name the resolver confers has an origin; - external_default for an external member carries both its position (org_external's static binding) and its default origin; - positions and the permission-set order are the resolver's, spelled out; - the gate sentence names where it departs from explain's 404 for an unknown id. The exhaustive-switch pin covers the new kind and both sources, and plugin-security's registered-member pin lists the optional member. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5odNVgHTd2MQbQCp5t
…origin kind and the resolver order Co-Authored-By: Claude <noreply@anthropic.com>
|
domain:spec#1 · os-project-manager · session_01FEkQ5mw7HFYn9o4LEGMQdK Contract review (delta)Served-tier: Inputs: the previous record (PR comment 6712477832, FAIL: M1 + S1-S3), the seat's ruling and claim amendment 6712494974 on card #31238 (M1 as a fourth origin kind; one plugin-security pin test added to the file surface), the PR body's fix-round section, and the 33 check-runs on M1 — a resolution path the origin kinds could not express: CLOSEDThe union is now
So the four kinds and two sources cover every path by which the resolver, or the service on top of it, names a set for a user; "matches enforcement by construction" and One precision note, not a finding: S1 — the unknown-user departure from
|
…in the position-bound slot, before derived names Co-Authored-By: Claude <noreply@anthropic.com>
|
domain:spec#1 · os-project-manager · session_01FEkQ5mw7HFYn9o4LEGMQdK Contract review (delta)Served-tier: Inputs: the previous delta record (PASS, N1 should-fix open), the commit, and the 21 check-runs on N1 — the test example's set order contradicted the comment above it: CLOSED
Otherwise unchanged
Check-runs on this head are mostly queued or in progress; the one concluded gate is green, and none has failed. The previous head's conclusions carry over for nothing but the identical contract file; the test change itself awaits the Type Check and spec test lanes. Implemented-by: VERDICT: PASS |
|
domain:spec#1 · os-project-manager · session_01FEkQ5mw7HFYn9o4LEGMQdK Test Core (2/8) red on |
|
domain:spec#1 · os-project-manager · session_01FEkQ5mw7HFYn9o4LEGMQdK Contract review (delta)Served-tier: Inputs: the three earlier records (PR comments 6712477832 FAIL, 6713231084 PASS, 6713288167 PASS on ① The PR's surface is unchanged by the merge
② Semver and Clause-② — unchanged, still right
③ Check-runs on
|
|
domain:spec#1 · os-project-manager · session_01FEkQ5mw7HFYn9o4LEGMQdK 维护者速读
席位核对Verdict: ACCEPT · head
Ruling-ref: the maintainer, 2026-11-07T16:00Z: 「你有 fable ,车道契约复审可以自己做」; 2026-11-07T16:59Z: 「你应该使用 pm-dispatch 技能审核和落地pr」; standing landing authorisation 2026-10-25T11:35Z: 「按照 pm-dispatch 规则复核过的都授权」. |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 42428161519 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 分类: 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
|
domain:spec#1 · os-project-manager · session_01FEkQ5mw7HFYn9o4LEGMQdK Queue build 42428161519 failed two Test Core shards, and neither failure comes from this PR:
This PR touches only Both anchors list this PR as S1 · inherited: #31306 and #31294 name #31233 as the S1 root. That makes this PR a bystander to the break, not its cause. The seat has re-queued it once, on the same head ( |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 42429369356 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 分类: 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
|
domain:spec#1 · os-project-manager · session_01FEkQ5mw7HFYn9o4LEGMQdK Queue build 42429369356 failed on the same deterministic root as before. That build stacked on the queue chain behind #31233, whose This PR touches no |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 42430896312 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 分类: 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
|
domain:spec#1 · os-project-manager · session_01FEkQ5mw7HFYn9o4LEGMQdK Queue build 42430896312 failed on the same stack root as build 42430895501 on #31287. Both were built on #31233's older queue entry, before #31233's #31233 has since re-entered the queue on the fixed head. The seat has re-queued this PR on the same head ( |
Requested by Leehom · project thread
Fixes #31238
Clause-②: yes (widening)
Metadata-protocol: none
维护者速读
security-service.ts)里新增三个 TypeScript 类型EffectivePermissionSetOrigin/EffectivePermissionSet/EffectiveAccess,和一个可选成员ISecurityService.describeEffectiveAccess?(userId, callerContext?):查看某个用户实际生效的权限集,以及每个权限集是怎么来的(默认绑定、所任职位、直接授予,或由解析器派生:配置的平台所有者、AI 席位)。另有契约测试里的类型钉、两份再生成的导出清单和一个 spec patch changeset。DelegableScope/describeDelegableScope的先例,您对命名仍有否决窗口。packages/spec/src/contracts/security-service.ts的EffectivePermissionSetOrigin与describeEffectiveAccess?的注释(门禁与「不存在的用户按无权管理拒绝」两句)。pnpm --filter @objectstack/spec exec vitest run src/contracts/security-service.test.ts,预期 29 个全部通过,其中两条是本卡新增的可选成员与三种形状的类型钉。What changes
packages/spec/src/contracts/security-service.ts, besideDelegableScope/PositionGrantSummary, TS interfaces with no Zod (a wire answer, not authorable metadata), text as drafted in #31215's dev report (pm-orders/server/31215-spec-addition.md) and re-read onorigin/main733efbd4:EffectivePermissionSetOrigin:{ kind: 'default' } | { kind: 'position'; position: string } | { kind: 'direct' } | { kind: 'derived'; source: 'platform_admin_config' | 'ai_access' }(the fourth kind added in the fix round, see below).defaultis a binding to theeveryoneaudience anchor (deployment baseline, an app'sisDefaultset, an accepted package default; ADR-0090 D5) or the human baseline the resolution adds (external_defaultfor an external member);positionis a held position's definition naming the set (ADR-0131 D3/D4), nevereveryone;directis asys_user_permission_setrow of the user's.EffectivePermissionSet:{ name: string; origins: EffectivePermissionSetOrigin[] }, at least one origin; a set reached two ways is listed once with both.EffectiveAccess:{ userId: string; organizationId: string | null; positions: string[]; permissionSets: EffectivePermissionSet[] }, the sets in the resolver's own order (defined in the docblock; see the fix round), in the caller's active organization vetted for the described user asexplainvets it.ISecurityService.describeEffectiveAccess?(userId: string, callerContext?: SecurityContext): Promise<EffectiveAccess>, afterdescribeDelegableScope, docblock citing ADR-0090 D6/D12: resolved by the resolver enforcement uses; gated asexplainis (manage_users, organization administration of an organization the user is a member of, or a delegatedadminScopecovering the user, the three armsexplainAccessForCallerinsecurity-plugin-explain-access.tschecks today); an unknown user refused exactly as one the caller may not manage (seat decision 6711854159;explainitself answers an admitted caller 404 for an unknown id, and security: a per-user effective-access read that says where each permission set comes from (default binding, position, direct) #31215 names that difference); OPTIONAL, callers feature-detect and answer "not served" (a REST door answers 501, as the security doors inpackages/rest/src/security-routes.tsdo for an absent member).packages/spec/api-surface/contracts.jsonandpackages/spec/export-origins/contracts.json: regenerated bycheck:generated --fix(the only two artifacts it proved stale); each gains exactly the three new names..changeset/31238-security-service-effective-access.md:'@objectstack/spec': patch. The order namedminor; AGENTS.md (Post-Task Checklist 3) andcheck-changeset-no-majorgrade aMetadata-protocol: nonechangepatch(maintainer ruling A on the level axis), and with this body the gate refusesminor(reproduced locally with apull_requestevent), so the entry ispatch.No plugin-security, rest, client or core change: those are #31215's, which is serial behind this card and does not close with it.
One wording addition to the drafted docblock: the absence paragraph says callers feature-detect and answer "not served", never an empty list, in the words of the
reopenAudienceBindingSuggestion?precedent. The order's name precedent readsdescribeDelegableScope?; onmainthat member is required (REQUIRED_MEMBERSlists it), so the optional-member pins follow thereopenAudienceBindingSuggestion?/checkControlledByParentWrite?pins instead.Tests
packages/spec/src/contracts/security-service.test.ts, two new cases beside the other optional-member pins:describeEffectiveAccess is OPTIONAL: a stub without it satisfiesISecurityService; an unguarded call does not compile (@ts-expect-error); listing it as aRequiredMemberdoes not compile; a stub with it resolves the answer.REQUIRED_MEMBERSdoes not move.EffectiveAccessshapes: the three origins, apositionorigin without its name and an unknownkindrefused (@ts-expect-error),toEqualTypeOfonEffectivePermissionSet,EffectiveAccessand the member's signature, and an exhaustiveswitchwith aneverarm.Reverse check (one-off, not kept): with the member made required,
check:test-typecheckturns red on this file (4 type errors) and is green again aftergit checkout HEAD -- packages/spec/src/contracts/security-service.ts(blob hash matched HEAD).Verification
Local gates on the final head
80410d2f: 82 derived bydispatch-gates --commands, 81 exit 0, 1 NOT MEASURED (check:dual-build-cjs-loads, exit 3: it needs every package'sdist, CI's).--ranreconciles 82 accounted, 0 unrun.pnpm --filter @objectstack/spec test: 821 files / 25966 tests passed (1 todo);pnpm --filter @objectstack/spec run typecheckexit 0 (test layer included);check:generatedgreen after--fix;check-changeset-no-majorwith apull_requestevent carrying this body exit 0. Details in the card'sos-dev-report; CI runs the full set.Fix round (contract review 6712477832, FAIL → this head)
{ kind: 'derived'; source: 'platform_admin_config' | 'ai_access' }, one docblock per source (OS_PLATFORM_OWNER_EMAIL→admin_full_accessand platform-admin standing;sys_user.ai_access→ai_seat), measured over every channel ofresolveUserAuthzGrantsand the plugin's human-baseline join. API-key scopes, an agent's ceiling and the guest anchor are named as not origins (not the user's own resolution). The exhaustive switch covers four kinds and both sources.explain's 404 to an admitted caller.external_defaultreports both{ kind: 'position', position: 'org_external' }and{ kind: 'default' };positionslists the derivedplatform_adminfirst andeveryonelast, neither ever an origin.admin_full_access, thenai_seat), then the human baseline; a set already listed keeps its place and gains the later origin.80410d2f: plugin-security'sregistered-security-service-members.pin.test.tslistsdescribeEffectiveAccessasoptional(claim amendment 6712494974 added that one file).patch).Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_01FEkQ5odNVgHTd2MQbQCp5t