Skip to content

feat(spec): publish ISecurityService and enforce it at both ends#3660

Merged
os-zhuang merged 3 commits into
mainfrom
claude/security-getreadablefields-query-9bcm4u
Jul 27, 2026
Merged

feat(spec): publish ISecurityService and enforce it at both ends#3660
os-zhuang merged 3 commits into
mainfrom
claude/security-getreadablefields-query-9bcm4u

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

背景

#3547 收尾时的观察:security 服务注册了 7 个跨包方法getReadFiltergetReadableFieldsresolvePermissionSetNamesexplain,以及三个 audience-binding suggestion 调用),但 @objectstack/spec/contracts/没有它的契约——而那个目录里已经躺着 40+ 个方法型服务契约(ISharingServiceIEmailServiceIAnalyticsService…)。

消费者只能鸭子类型地探测它,每个消费者各自发明「方法缺失怎么办」和「空答案是什么意思」的回退策略。#3547 之前只有一两个消费者,现在开始变多,这就是一个漂移面。

改动

新增 packages/spec/src/contracts/security-service.ts,并把两端都对着它做类型检查 —— 关键是enforced 而非 declared(Prime Directive #10):

  • 生产端 —— plugin-security 把注册对象赋给 ISecurityService。方法改名、漏掉、返回类型变了,当场构建失败,而不是等消费者的特性探测在运行时静默降级。
  • 消费端 —— REST 层把服务解析为 Partial<ISecurityService>,强制调用点继续做 typeof x === 'function' 探测,而不是假定完整表面。

契约里写明了消费者猜不出来的那件事:这些方法不共享同一套失败约定

方法 失败姿态 undefined 的含义
getReadFilter fail CLOSED 「无行级限制」——仅此一义。解析失败返回匹配零行的 deny 过滤器,永远不是 undefined
getReadableFields fail SOFT 「没有答案,用你自己的投影」——而 [] 是权威答案,意思相反:一个字段都不可读

这个区别是承重的:字段投影只是在「已经发生的强制」之上做外观收窄(读路径早已删除不可读键),退化到更宽的列集不会泄露;行过滤器没有这层兜底。

类型检查当场咬出的一处真实不一致(已修)

getReadFilter 声明返回 Promise<Record<string, unknown> | null | undefined>,但每条 return 路径要么是过滤器对象要么是 undefined——filter ?? undefined 已显式把 null 归一掉了。| null 是死分支,却让「无限制」有了两种表示,正是契约该拒绝的歧义。已删除。纯类型变更,无运行时行为改变。

测试

  • 新增 security-service.test.ts(6 例):完整实现满足表面;getReadFilterundefined 只能表示「无限制」;getReadableFieldsundefined[]相反答案;system context 全字段绕过;部分实现可被特性探测;explain 接受 record 级请求与显式目标用户。
  • 全量:spec 6665 / plugin-security 593 / rest 399 全过;三个包 CJS/ESM/DTS 构建全绿。

关联

#3547 的 follow-up(服务面契约固化)。关联 #3391#3561#3649

🤖 Generated with Claude Code

https://claude.ai/code/session_01KZ2BGusRo58ZW8FMkDhGTb


Generated by Claude Code

The `security` service registers seven cross-package methods but had no
contract in `@objectstack/spec/contracts`. Consumers duck-typed it and each
invented its own fallback for a missing method or an "empty" answer.

Adds `ISecurityService` and types BOTH ends against it, so the surface is
enforced rather than declared: plugin-security assigns its registration to
the interface (a renamed, dropped, or re-typed method fails that build), and
the REST layer resolves the service as `Partial<ISecurityService>` (call
sites must keep feature-detecting rather than assume the full surface).

The contract states the one thing consumers cannot guess — the methods do
not share a failure convention. `getReadFilter` fails CLOSED (a failure is a
deny filter, never `undefined`; `undefined` means "no row restriction" and
nothing else). `getReadableFields` fails SOFT, and its two empty answers are
opposites: `undefined` is "no answer, use your own projection", `[]` is
authoritative "no field is readable".

Typing the producer immediately caught one real discrepancy, fixed here:
getReadFilter declared `Record<string, unknown> | null | undefined` while
every return path yields a filter or `undefined` (`filter ?? undefined`
normalizes the null away). Dropping the dead `| null` leaves "no
restriction" with exactly one representation. Type-level only.

Tests: spec 6665, plugin-security 593, rest 399 — all passing; the three
packages build clean (CJS/ESM/DTS).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZ2BGusRo58ZW8FMkDhGTb
@vercel

vercel Bot commented Jul 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 27, 2026 1:25pm

Request Review

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling and removed size/m labels Jul 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/plugin-security, @objectstack/rest, @objectstack/spec.

107 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/automation/approvals.mdx (via packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/permissions/access-recipes.mdx (via packages/plugins/plugin-security)
  • content/docs/permissions/authorization.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/permissions/explain.mdx (via @objectstack/plugin-security)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via packages/plugins/plugin-security, @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/plugin-security, @objectstack/rest, @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/plugin-security, @objectstack/rest, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via packages/rest, @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/plugin-security, @objectstack/rest, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/audience-based-interfaces.mdx (via packages/plugins/plugin-security)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

The contracts index table and the runtime-services source-of-truth list
enumerate every published service contract; a new contract missing from
them makes the index quietly wrong. Adds the Security Service row and its
source path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZ2BGusRo58ZW8FMkDhGTb
Adding ISecurityService and its companion types exports six new names from
`@objectstack/spec/contracts`; the api-surface gate pins the public surface
so additions have to be acknowledged. Diff is exactly those six additions —
0 removed, 0 narrowed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZ2BGusRo58ZW8FMkDhGTb
@os-zhuang
os-zhuang marked this pull request as ready for review July 27, 2026 13:35
@os-zhuang
os-zhuang merged commit 1659072 into main Jul 27, 2026
19 checks passed
@os-zhuang
os-zhuang deleted the claude/security-getreadablefields-query-9bcm4u branch July 27, 2026 13:35
os-zhuang pushed a commit that referenced this pull request Jul 27, 2026
…rix is not a failed one

Closes #3668. With cancel-in-progress on, every consecutive push cancelled
the in-flight dogfood matrix (the longest job in the workflow) and the
gate's catch-all branch turned that into a red X on the superseded SHA —
two observed on #3660 alone. False reds train everyone to ignore the one
check that must never be ignored.

Safety premise verified experimentally before landing (per the issue's own
ask): run 30271824408 executed a fail-fast matrix where shard 1 really
failed and fail-fast cancelled shard 2 mid-run — the aggregate
needs.<job>.result reads 'failure', not 'cancelled'. Failure dominates, so
an aggregate of 'cancelled' can only come from the whole run being stopped
externally (supersession or manual cancel) and passing it masks no real
regression. The manual-cancel case going green is the issue's accepted
trade-off; the rejected alternative (skipping the gate via !cancelled())
would republish the #3622 required-context deadlock.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECTCrcCdZpCHw5zFSgcmGt
os-zhuang added a commit that referenced this pull request Jul 27, 2026
…rix is not a failed one (#3671)

Closes #3668. With cancel-in-progress on, every consecutive push cancelled
the in-flight dogfood matrix (the longest job in the workflow) and the
gate's catch-all branch turned that into a red X on the superseded SHA —
two observed on #3660 alone. False reds train everyone to ignore the one
check that must never be ignored.

Safety premise verified experimentally before landing (per the issue's own
ask): run 30271824408 executed a fail-fast matrix where shard 1 really
failed and fail-fast cancelled shard 2 mid-run — the aggregate
needs.<job>.result reads 'failure', not 'cancelled'. Failure dominates, so
an aggregate of 'cancelled' can only come from the whole run being stopped
externally (supersession or manual cancel) and passing it masks no real
regression. The manual-cancel case going green is the issue's accepted
trade-off; the rejected alternative (skipping the gate via !cancelled())
would republish the #3622 required-context deadlock.


Claude-Session: https://claude.ai/code/session_01ECTCrcCdZpCHw5zFSgcmGt

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants