diff --git a/content/docs/releases/v15.mdx b/content/docs/releases/v15.mdx index b51ffd14a8..f3d54ec816 100644 --- a/content/docs/releases/v15.mdx +++ b/content/docs/releases/v15.mdx @@ -1,6 +1,6 @@ --- title: v15.0.0 -description: Authorization kernel chain (ADR-0095) — tenant isolation becomes an independent Layer 0, a monotonic posture ladder derived from capabilities, cross-tenant write hardening, and record-grained access explanation. Breaking changes affect multi-org deployments only. +description: Authorization kernel chain (ADR-0095) — tenant isolation becomes an independent Layer 0, a monotonic posture ladder, and record-grained access explanation. The 15.1 line adds attachments v1, declarative connectors (ADR-0096/0097), dashboard-level filters, pinyin search, and a broad write-path security hardening sweep. --- **Authorization kernel hardening (ADR-0095) + supporting features (tracking framework#2920).** @@ -14,7 +14,7 @@ are unaffected. Read this before upgrading a multi-org or enterprise-hierarchy deployment. -## Breaking changes +## Breaking changes (15.0.0) ### Multi-org deployments only @@ -61,7 +61,7 @@ Read this before upgrading a multi-org or enterprise-hierarchy deployment. their position BU; multiple positions union; expired/inactive positions fall back to membership. -## Non-breaking hardening (same effective visibility) +## Non-breaking hardening in 15.0.0 (same effective visibility) - **RLS safety nets recognize canonical `==` (#2936).** The field-existence / tenancy-disabled nets were inert for `==`-form policies; now recognized. Only @@ -74,7 +74,7 @@ Read this before upgrading a multi-org or enterprise-hierarchy deployment. now filters by organization directly, and private hierarchy-scoped objects that lack an `organization_id` column fail closed at boot. -## New features (additive) +## New features in 15.0.0 (additive) - **A3** — the `sys_user` record page gains **Permission Sets** (direct grant) and **Business Units** assignment tabs. @@ -94,8 +94,340 @@ Read this before upgrading a multi-org or enterprise-hierarchy deployment. - **C1** — hierarchy RLS enterprise package reaches GA (subtree/reporting-chain caching, customer docs, license gate). +--- + +# What's new in 15.1.0 + +15.1 is a fast-follow minor with three themes: a **write-path security hardening +sweep** (every hole below was verified exploitable on 15.0.0 and is now closed), +**attachments v1** (parent-derived access, lifecycle, authenticated downloads), and +**declarative connectors** (ADR-0096/0097 — REST/OpenAPI/MCP integrations wired +purely from metadata). No `major` changesets: strict-semver edges are listed under +*Behavior changes* below with their migrations. + +## Behavior changes (15.1.0) + +### Spec: `tenancy.strategy` and `tenancy.crossTenantAccess` removed (#2962) + +Strict-semver breaking, shipped in a minor under the launch-window policy: both +fields had **zero consumers** and no runtime behavior. The `tenancy` block is now +`.strict()` — unknown keys are a loud parse error with tombstone guidance instead +of being silently stripped. **Migration:** + +- `tenancy: { enabled: false, strategy: 'shared' }` → `tenancy: { enabled: false }` +- `tenancy: { enabled: true, strategy: '…', tenantField: 'x', crossTenantAccess: false }` + → `tenancy: { enabled: true, tenantField: 'x' }` +- Per-tenant databases are a deployment concern (environment-per-database, ADR-0002), + not object metadata. Cross-tenant visibility is expressed via sharing rules / OWD + (`externalSharingModel`). + +### 'ObjectOS' retired from the spec's public surface (#2963, #2960) + +Layer vocabulary converges on **ObjectQL (data) / Kernel (control) / ObjectUI (view)**; +"ObjectOS" now refers exclusively to the commercial runtime environment. Three +public identifiers renamed, with deprecated aliases kept for one release: +`ObjectOSCapabilitiesSchema` → `KernelCapabilitiesSchema`, `ObjectOSCapabilities` → +`KernelCapabilities`, `ObjectOSKernel` → `IKernel` (`PluginContext.os` is now typed +`IKernel`). Find/replace is the whole migration; schema shapes and JSON output are +unchanged. Docs moved from `protocol/objectos` to `protocol/kernel`. + +### Anonymous access denied uniformly across HTTP surfaces (#2567) + +Under `requireAuth` (the secure default), anonymous `POST /graphql` and the raw +`/data` routes now return 401 — previously they bypassed the gate and reached +ObjectQL directly. The dispatcher's `requireAuth` default aligns with the REST +plugin (`?? true`). **Migration:** deployments that genuinely serve these surfaces +publicly must opt out explicitly with `requireAuth: false` (boot warns). All +anonymous-deny seams converge on one shared `shouldDenyAnonymous`, and the +authz-conformance matrix ratchets newly added `/data`/`/meta`/`/graphql` routes. + +### Server-enforced `readonly` / `readonlyWhen` on UPDATE (#2948, #3042, #3003) + +`readonly: true` was documented as a server contract but only enforced in the UI — +#3003 confirmed approval/status/amount columns could be forged via a plain REST +`PATCH` on 15.0.0. Non-system UPDATEs now **strip** caller-supplied writes to static +`readonly` fields, and `readonlyWhen` predicates are enforced on the multi-row +UPDATE path too. Server stamps (audit hooks), write middleware, and `isSystem` +writes (import/seed/approvals/lifecycle) are unaffected. Multi-row semantics are +fail-safe: if **any** matched row locks a field, that field is dropped for the whole +batch — narrow the `where` to unlocked rows to write it. + +### Ownership anchor (`owner_id`) is guarded (#3004, #2982) + +Unprivileged writers can no longer forge another user's `owner_id` on insert or +reassign it on update; transfers require a transfer grant (`allowTransfer`, or +`modifyAllRecords` which implies it). Empty `owner_id` on insert is stamped to the +current user — now on bulk rows too (previously bulk-inserted rows were NULL-owned +and invisible even to their creator). Bulk writes (`update({multi:true})`, bulk +delete) are now scoped by the caller's owner/RLS **write** filter and fail closed if +the scoping AST is missing. Engine-internal referential cascades (`set_null` on user +deletion) are exempted via a server-side marker that cannot be forged (#3048). +**Note:** REST import runs as the importing user — a non-privileged user importing a +CSV with someone else's `owner_id` column is now correctly rejected. + +### MCP action surface gated on `ai.exposed`, fail-closed (#2964) + +Action bodies execute as trusted code, yet MCP `run_action` could invoke any +headless action. Actions that should remain invocable by AI agents must now declare +`ai: { exposed: true, description: '…' }` (description ≥ 40 chars). Other surfaces +(UI, REST `/actions/...`) are unchanged. Flow-type actions now receive the caller's +identity as a real `AutomationContext` (`runAs: 'user'` flows evaluate RLS as the +caller), and trusted-body invocations are audit-logged on both MCP and REST paths. + +### Declarative stdio MCP transports are deny-by-default (#3055) + +A declarative `stdio` transport spawns a local child process from metadata — and +Studio runtime publish can introduce such metadata. Previously-materializing +declarative stdio instances are now treated as a **configuration error** (boot +fatal / reload skip), never retried as "upstream unavailable". **Migration:** hosts +opt in explicitly — `new ConnectorMcpPlugin({ declarativeStdio: ['my-mcp-server'] })` +(command allowlist) or `{ declarativeStdio: true }` (full trust). `http` transports +and hand-wired connectors are unaffected. + +### OWD posture enforced at metadata-save time (#3050) + +A new `registerAuthoringGate` seam in `metadata-protocol` runs pre-persistence for +draft and publish saves (environment writes only). `plugin-security` registers the +object posture gate on it: an environment overlay over a packaged object can only +**tighten** `sharingModel` / `externalSharingModel` (ADR-0086 D1), and +`externalSharingModel ≤ sharingModel` (ADR-0090 D11) is now rejected at save time +rather than only flagged by CLI lint. Already-stored metadata loads unchanged. + +### Attachments: dead fields removed, downloads authenticated (#2755, #2970) + +- **REMOVED: `sys_attachment.share_type` / `sys_attachment.visibility`** — modeled + in v1 but with zero runtime consumers. No replacement key: access derives from the + parent record. Stop sending these fields (unknown-field validation rejects them); + existing DB columns remain as unmanaged leftovers. +- **Attachment downloads are no longer anonymous capability URLs.** For + `scope === 'attachments'` files that are not `public_read`, + `GET /storage/files/:fileId` and `/url` require a session (401 `AUTH_REQUIRED`) + plus parent-derived read access (403 `ATTACHMENT_DOWNLOAD_DENIED`) and return a + short-lived signed URL (default `downloadTtl` 300s). Field files, avatars, and org + logos (embedded in ``) keep stable anonymous URLs. +- **`sys_attachment.enable.trash` is now `false`** — attachment deletion is a hard + delete (declaring the honest state; reap guards reclaim bytes). +- **Attaching to a record now requires parent EDIT** (was: parent READ), matching + Salesforce semantics. + +### Other semantics worth knowing + +- `ConnectorSchema.authentication` defaults to `{ type: 'none' }` (a relaxation; + existing connectors unaffected). +- Aggregate queries gate FLS on the **input** side: a groupBy/aggregate referencing + an FLS-unreadable field is rejected fail-closed with the field named (#2976). +- `findData` rejects unknown `$`-prefixed query params with 400 + `UNSUPPORTED_QUERY_PARAM` instead of silently matching zero rows (#2926 ⑩). +- The FormView `buttons` + `defaults` config (#3014) is **EXPERIMENTAL — declared + in the spec but not yet enforced by the Console renderer**. + +## Security hardening (15.1.0) + +Beyond the behavior changes above, an execution-surface sweep (#2849 class) closed +three confirmed-exploitable holes (#2975): + +- **Reports IDOR + scheduled-report RLS bypass (#2980).** `ReportService` dropped + the caller context and read/wrote `sys_saved_report` as system — any authenticated + user could read/delete/overwrite any saved report by id, and scheduled dispatch + emailed entire tables. Now owner-scoped; scheduled dispatch resolves the owner's + real RLS context and **fails closed** when it can't. +- **Knowledge/RAG fall-open (#2981).** `applyPermissionFilter` returned all hits + when the context was missing — an agent omitting `ToolExecutionContext.actor` got + unfiltered semantic search over the whole corpus. Missing identity now fails closed. +- **`$expand` cross-object leak (#2850).** Expanding a reference resolved the + target rows directly against the driver, skipping the referenced object's RLS and + FLS entirely. Expansion now goes through the engine read path; PUBLIC referenced + objects are exempted only from the object-level CRUD gate via a server-side marker. +- **Public-form anchor forgery (#3022).** Zero-declared-field public forms merged + the raw body wholesale, letting anonymous visitors POST `owner_id` / + `organization_id` / audit columns / `id`. Server-managed fields are now stripped at + the data layer and excluded at the route layer, from a single shared definition. +- **Seed-not-clobber (#2909).** An admin's `active: false` on an overshared sharing + rule — and admin-edited capability `scope` values — are no longer resurrected by + boot seeding (`managed_by` / `customized` provenance columns land). +- **`sys_position` system-row write gate re-armed (#2926 ①)** after the A4 + `managed_by` vocabulary rename had silently disarmed it. +- **better-auth pinned to 1.7.0-rc.1** for GHSA-p2fr-6hmx-4528, with the ObjectQL + adapter implementing the new `consumeOne`/`incrementOne` contracts and `sys_jwks` + gaining `alg`/`crv` columns (#2974). + +## New capabilities in 15.1.0 + +### Attachments v1 (#2755, #2970) + +Parent-derived access on the full chain (create/read/list/download/delete — +`sys_attachment` reads inherit parent-record visibility; aggregate counts don't leak +hidden rows), authenticated short-lived download URLs, orphan lifecycle for +`sys_file` (tombstone on last detach, TTL'd reap with a `registerReapGuard` seam +that re-verifies zero references and deletes storage bytes first), and +`sys_upload_session` reaping that aborts backend multipart uploads before the row is +reclaimed (abandoned S3 parts stop accruing). Console ships authenticated uploads +with friendly denial copy, signed-URL downloads, and file/image upload cells inside +inline line-item grids; the showcase demonstrates per-line Receipt attachments on +invoice lines (#3051). + +### Declarative connectors (ADR-0096 / ADR-0097) + +`connectors:` metadata entries with a `provider` (`openapi` / `mcp` / `rest`) +materialize at boot into dispatchable connectors — an AI or a package can wire an +integration end-to-end in pure metadata and call it from flow `connector_action` +nodes (#2994). Credentials go through `credentialRef` (env-var resolution in the +open profile; vault/OAuth2 per ADR-0015 are enterprise). Boot fails loudly on +unknown providers, invalid `providerConfig`, unresolvable credentials, or name +collisions; descriptor-only entries (actions with no runtime registration) get a +boot audit warning unless `enabled: false` (#2985). Metadata reloads reconcile +incrementally (#3001), unreachable upstreams degrade to an action-less husk with +`state: 'degraded'` and exponential-backoff retry instead of failing boot (#3049), +OpenAPI specs load from inline objects, package-relative file paths (confined), or +URLs (#3024), and `GET /api/v1/automation/connectors` reports each descriptor's +`origin` (`plugin` | `declarative`) — which the flow-designer connector picker +surfaces (objectui#2563). The showcase runs a live `provider:'mcp'` demo pinned in +CI (#3062). + +### Dashboard-level filters (#2501) + +`GlobalFilterSchema.name` gives each dashboard filter a variable key (readable as +`page.` in widget expressions; `"dateRange"` reserved), and +`DashboardWidgetSchema.filterBindings` lets each widget retarget a filter to its own +field or opt out with `false` (#3035). The Console renders a dashboard filter bar, +AND-merges active values into every bound widget's query, ships a visual +filterBindings editor in the widget inspector, resolves `optionsFrom` via +server-side distinct, and localizes the bar (objectui#2576/#2586/#2590). The +showcase "Revenue Pulse" dashboard demonstrates interactive filters driving multiple +charts over two objects (#3038). + +### Pinyin search (#3027) + +Locale-gated (`OS_SEARCH_PINYIN_ENABLED`, auto-on when any `zh-*` locale is +configured): display/name fields get a hidden `__search` companion column filled +with full pinyin + initials ("张伟" → `zhangwei zw`) on save, backfilled at boot, +and OR-ed into `$search` — lookup pickers, list quick search, and ⌘K transparently +match `zhangwei` / `zw` against CJK values. FLS-restricted/secret/PII fields are +never indexed. + +### Metadata & spec + +- **Conditional tabs (#2967):** `page:tabs` items accept a `visibleWhen` CEL + predicate over `record` + `current_user` + `page.`; FALSE removes the whole + tab, re-evaluated live as page variables change. +- **View property liveness (#3046):** all 83 view properties classified + (68 live / 2 experimental / 5 dead); `os build` warns on dead props with fix-it + hints. +- **Feature-gate registry (#2965):** actions/params declare `requiresFeature`, + lowered at parse time to canonical `visible` CEL — 17 capability-dependent actions + (platform user management, 2FA, OAuth apps) now hide when their plugin is off + instead of rendering 404 buttons. +- **Page variable `source` renders as a component picker** in Studio (#2968). + +### AI / MCP + +- **`aggregate_records` MCP tool (#2976):** count/sum/avg/min/max/count_distinct + with groupBy (incl. date bucketing), where, and IANA timezones — on the ObjectQL + ENGINE read path, so RLS/tenant/delegation intersect exactly like `find`. +- **Standalone authored actions reach the MCP bridge (#3010):** `list_actions` / + `run_action` now union `object.actions` with standalone `action` metadata rows — + Studio-authored actions with `ai.exposed: true` are callable by agents without + re-declaring them inside the object. + +### Deployment + +- **Official Docker runtime image (#2952):** `ghcr.io/objectstack-ai/objectstack` + (Node 22, pinned CLI, `os start`, non-root, `/api/v1/health` HEALTHCHECK, + multi-arch). Deployment is a two-line Dockerfile — or plain `docker run` with a + mounted artifact / `OS_ARTIFACT_PATH` URL. +- **`create-objectstack` scaffolds container-ready projects (#2979):** Dockerfile + (two-stage onto the official image), docker-compose (app + Postgres), + `.dockerignore`. + +### Kernel & protocol + +- **`kernel:bootstrapped` lifecycle anchor (#2989):** fires after all `kernel:ready` + handlers settle, before `kernel:listening` — the correct anchor for + reconcile/backfill work (also fixed: `everyone` auto-bind ordering, so app-level + auto-bind happens on fresh deployments). +- **`app:seeded` event (#3000):** membership backfill re-runs when background + seeding settles — seeded users no longer wait for a restart. +- **ADR-0087 completed (#2972):** the protocol handshake now runs on boot-time + durable-package rehydration (incompatible `sys_packages` rows are refused with + structured `OS_PROTOCOL_INCOMPATIBLE` diagnostics, boot continues) and on + code-defined `AppPlugin` loads (fail-fast); conversion chain backfilled so + `migrate meta --from 10` reaches protocol 15; `spec-changes.json` ships with the + npm artifact; the upgrade guide is generated and drift-checked. +- **Honest capability discovery (ADR-0076 slice):** services self-describe + stub/degraded/fallback status instead of discovery hardcoding `available`; + discovery stops advertising a `/realtime` route and `websockets: true` that + always 404'd. + +### i18n + +- `os i18n extract` emits action-param keys — action dialog forms (e.g. Setup → + Create User) are translatable; platform bundles regenerated for + en/zh-CN/ja-JP/es-ES (#3030). +- Audit/feed activity summaries localize to the workspace locale (ADR-0053 verb + templates, localized object labels) with English fallback (#3045, #3029). + +## Notable fixes (15.1.0) + +- **Studio metadata editing under spec 15:** lazy schema proxies stay + identity-compatible with `z.toJSONSchema` — Page/View inspectors no longer crash + (#3021). +- **Sharing rules apply to seeded demo data out of the box:** an idempotent boot + backfill materializes `sys_record_share` for seed-loaded records (#2926 ③). +- **Package management REST:** `PATCH /packages/:id` is actually routed (#2995); + duplicate `POST /packages` ids return 409 (#2971); `packages.install` passes + `overwrite` for upgrade flows (#3007). +- **Create User:** an explicitly typed password wins over the dialog's default + `generatePassword: true` instead of erroring 400 (#3031). +- **REST status passthrough:** record-level 403s from sharing no longer degrade to + a bare 400 (#2926 ⑦). +- `gen:schema` regression ratchet restores the lost `PageTabsProps` schema and fails + CI on future disappearances (#3012). + +## New in Console (Studio) — bundled objectui 14.1 + +The pinned Console picks up the objectui 14.1 line (94 commits). Highlights: + +- **Record-level inline edit (#2542, #2549, #2604):** one shared draft across the + detail body *and* the highlight strip, one atomic Save + (`update` with `ifMatch` optimistic concurrency, only edited keys sent), 409 + conflict dialog, approval-lock preflight, Esc / Cmd+Enter shortcuts. +- **CEL editors everywhere (#1582 series):** the formula editor edits the engine's + real `expression` key (the old textarea wrote a `formula` key the engine never + read) and live-infers the result type; conditional rules + (visibleWhen/readonlyWhen/requiredWhen), list-view conditional formatting, RLS + policies (with lint, autocomplete, and per-policy test-run against a real record), + and condition-builder escape hatches all get lint + field autocomplete instead of + bare textareas. Summary fields get a structured roll-up editor. +- **Dashboard filter bar + visual `filterBindings` editor** (see Dashboard-level + filters above). +- **Studio Access pillar:** package-level OWD overview with inline batch edit and + `external ≤ internal` validation (#2508); unsaved-changes guards across matrix + edits, SPA header navigation, and OWD rows (#2588/#2606/#2610); read-only packages + actually lock the matrix (#2570); record-grained AccessExplainPanel (#2502); + three-state provenance badges (#2501). +- **Flow designer:** searchable add-node palette with keyboard navigation and + cloud-synced recents (#2543/#2553); Decision branches pick their target node + inline (#2568); the connector picker lists runtime-dispatchable connectors and + marks declarative instances (#2563); positions as approver/escalateTo references + (#2515). +- **Kanban:** lanes honor the ADR-0085 `stageField` role, cards default to + `highlightFields`, conditional formatting accepts CEL rules (#2596/#2541/#2550). +- **Record header:** metadata-driven multi-button layout (3 desktop / 1 mobile, + `order` + `variant:'primary'` tie-break, ⋯ overflow menu) (#2574). +- **Fixes:** error-envelope objects no longer crash the page via `toast.error` + (React #31, #2580); bare-string sort from spec forms no longer crashes ListView + (#2601); CSV import sniffs encoding with a GB18030 fallback — zh-CN Excel CSVs + import cleanly (#2557); Studio/metadata-admin follow the in-app locale instead of + `navigator.language` (#2602); relative dates localize via `Intl.RelativeTimeFormat` + (#2593); action-visibility traps defused — `os.user.*` alias in client predicate + scope, `record_more` placement, loud warnings for throwing predicates (#2611); + related lists respect the current user's child-object read permission (#2565); + FLS fail-open fixed (#2537). + ## Upgrade checklist +### 15.0.0 + - **Multi-org:** review any workflow that relies on members editing each other's records org-wide (grant an explicit edit permission set) and any integration that supplied a cross-tenant `organization_id` on user-context writes (route it through system context @@ -107,7 +439,27 @@ Read this before upgrading a multi-org or enterprise-hierarchy deployment. end-to-end smoke test in a licensed environment is still owed** — open-core CI cannot exercise the multi-org path. +### 15.1.0 + +- Remove `tenancy.strategy` / `tenancy.crossTenantAccess` from object metadata + (parse now fails loudly). +- Rename `ObjectOSCapabilitiesSchema` / `ObjectOSCapabilities` / `ObjectOSKernel` + imports to `KernelCapabilitiesSchema` / `KernelCapabilities` / `IKernel` + (deprecated aliases last one release). +- If you serve `/graphql` or raw `/data` anonymously on purpose, set + `requireAuth: false` explicitly. +- Audit clients that write `readonly` fields or `owner_id` — those writes are now + stripped/denied; grant `allowTransfer` where ownership transfer is legitimate. +- Declare `ai: { exposed: true, description }` on actions AI agents should keep + invoking via MCP. +- Stop sending `sys_attachment.share_type` / `visibility`; expect authenticated, + short-lived attachment download URLs; attaching now requires parent EDIT. +- Hosts using declarative **stdio** MCP connectors must opt in via + `ConnectorMcpPlugin({ declarativeStdio: [...] })`. + ## References ADR-0095 (kernel chain) · ADR-0090 Addendum (position-anchor) · ADR-0094 (overlay -preservation) · framework#2920 (tracking). +preservation) · framework#2920 (tracking) · ADR-0096/0097 (declarative connectors) · +ADR-0087 (protocol handshake) · ADR-0086 D1 / ADR-0090 D11 (OWD authoring gate) · +#2849 (execution-surface sweep) · #2970/#2755 (attachments v1). diff --git a/docs/plans/release-15.1-test-plan.md b/docs/plans/release-15.1-test-plan.md new file mode 100644 index 0000000000..cb98cced3c --- /dev/null +++ b/docs/plans/release-15.1-test-plan.md @@ -0,0 +1,112 @@ +# 15.1 发布测试清单 + +> 范围:framework `@objectstack/*@15.0.0 → 15.1.0`(87 commits, 65 changesets)+ +> 配套 Console `objectui 14.0.0 → 14.1.0`(94 commits,framework 钉在 `b8967495be73`)。 +> 环境:`examples/app-showcase` + `objectstack dev --ui --seed-admin`(独立端口 + 独立 file DB)。 +> 标注:🖥 = 浏览器验证;🔌 = API/CLI 验证;🤖 = 已有自动化钉死(dogfood/e2e 测试,CI 覆盖)。 + +## A. 安全加固(本次最大主题) + +| # | 项 | 方式 | 验证要点 | 来源 | +|---|---|---|---|---| +| A1 | 静态 `readonly` UPDATE 强制 | 🔌🤖 | REST PATCH 一个 `readonly: true` 字段 → 该字段被 strip,响应中值不变;`isSystem` 写不受影响 | #2948/#2957/#3003,`showcase-static-readonly.dogfood.test.ts` | +| A2 | `readonlyWhen` 多行 UPDATE 强制 | 🔌🤖 | bulk update 命中任一锁定行 → 字段对整批丢弃 | #3042 | +| A3 | `owner_id` 锚点:insert 伪造/update 转移拒绝、批量 stamp | 🔌🤖 | 非特权用户 insert 他人 owner_id → 拒;bulk insert 空 owner → stamp 为当前用户 | #3004/#3018,`owner-anchor-and-bulk-writes.dogfood.test.ts` | +| A4 | bulk write 按 owner/RLS write filter 收窄 | 🔌🤖 | member 对 OWD-private 对象 `update({multi:true})` → 只命中自己行 | #2982 | +| A5 | 引擎级联豁免(删用户 set_null 不被 A3 拦) | 🔌🤖 | 删除 sys_user → 其拥有记录 owner_id 被正常清空,级联不中断 | #3023/#3048 | +| A6 | `$expand` 强制被引对象 RLS/FLS | 🔌🤖 | 以受限用户 `?expand=` 引用 → RLS 隐藏行不返回、FLS 字段掩码 | #2850/#2961 | +| A7 | public form 剥离 server-managed 锚点 | 🖥🔌 | 公开表单渲染不出现 owner_id/organization_id 等;POST 携带被剥离 | #3022/#3036 | +| A8 | 匿名 deny 全 HTTP 面统一 | 🔌🤖 | 未登录 `POST /graphql`、raw `/data` → 401(`requireAuth` 默认) | #2567 系列,`showcase-anonymous-deny-surfaces.dogfood.test.ts` | +| A9 | MCP `run_action` 需 `ai.exposed`,fail-closed | 🔌 | 未声明 action 经 MCP 调用被拒;声明后可调且入 audit log | #2964 | +| A10 | reports IDOR / 定时报表 RLS / RAG fall-open | 🔌🤖 | 跨 owner 按 id 读/删 saved report → 拒 | #2980/#2981/#2975 | +| A11 | OWD 保存门:packaged 对象只能收紧 sharingModel;`external ≤ internal` | 🖥 | Studio 里把 packaged 对象 OWD 往宽改 → 保存被拒;OWD 总览 inline 校验 | #3050 + objectui#2508 | +| A12 | 附件访问链(读继承 parent、下载 401/403+签名 URL、attach 需 parent EDIT) | 🖥🔌🤖 | 见 C 组;非授权下载 → 401/403 | #2755/#2970 | +| A13 | sharing rule seed-not-clobber + capability scope seed-once | 🔌 | 管理员改动不被重启 seed 覆盖 | #2909 | +| A14 | better-auth 1.7.0-rc.1(GHSA 修复)登录链路 | 🖥 | 登录成功且后续请求 200(sys_jwks alg/crv 列) | #2974 | + +## B. 声明式 Connector(ADR-0096/0097) + +| # | 项 | 方式 | 验证要点 | 来源 | +|---|---|---|---|---| +| B1 | provider-bound 实例 boot materialization | 🔌🤖 | showcase live `provider:'mcp'` demo:boot 后 `GET /api/v1/automation/connectors` 有该实例,`origin:'declarative'` | #2994/#3062 | +| B2 | flow designer connector picker 标注声明式实例 | 🖥 | connector_action 节点下拉列出实例并带标注 | objectui#2563 | +| B3 | 上游不可达 → degraded husk + 退避重试,恢复原子换入 | 🔌 | 断掉 upstream 重启 → boot 不挂,descriptor `state:'degraded'` | #3049 | +| B4 | declarative stdio 默认拒绝,host allowlist opt-in | 🔌 | 未 opt-in 的 stdio 实例 → boot fatal/reload skip | #3059 | +| B5 | openapi spec 文件路径(package 相对,拒绝逃逸) | 🔌 | `providerConfig.spec: './openapi.json'` 可解析;`../` 拒 | #3024 | +| B6 | descriptor-only 契约 boot 审计 | 🔌 | actions 无运行时注册 → boot warning;`enabled:false` 静音 | #2985 | + +## C. 附件 v1 + +| # | 项 | 方式 | 验证要点 | 来源 | +|---|---|---|---|---| +| C1 | 记录详情 RecordAttachmentsPanel 认证上传/下载 | 🖥 | 上传成功;下载经签名 URL;无权限时友好拒绝文案 | objectui#2532 | +| C2 | 发票行 per-line Receipt(inline grid 内上传单元格) | 🖥 | showcase Invoice → 行内 Receipt 列出现上传控件 + chips/缩略图,非文本框 | #3051 + objectui#2585 | +| C3 | 附件读继承 parent 可见性 | 🔌🤖 | 受限用户列附件 → 不可见 parent 的行与计数均不出现 | #2970 | +| C4 | sys_file 孤儿 tombstone/reap + upload_session 报废(abort multipart) | 🔌🤖 | 生命周期测试覆盖 | #2755/#2970 | + +## D. Dashboard 全局筛选 + +| # | 项 | 方式 | 验证要点 | 来源 | +|---|---|---|---|---| +| D1 | Revenue Pulse:filter bar 驱动多图表 | 🖥 | 切 filter/日期预设 → 多 widget 同步 re-scope;opt-out widget 不动;dirty 出 Reset | #3038/#2576 | +| D2 | widget inspector 可视化 filterBindings 编辑 | 🖥 | Studio 选中 widget → "Dashboard filter bindings" 区,开关 Apply/换字段 | objectui#2586 | +| D3 | spec-form filter options 不再崩 dashboard;select 显示 label | 🖥 | 有 `{value,label}` options 的 dashboard 正常渲染 | objectui#2597 | + +## E. Console 核心体验(objectui 14.1) + +| # | 项 | 方式 | 验证要点 | 来源 | +|---|---|---|---|---| +| E1 | Record 级 inline edit:共享 draft + 单次原子 Save | 🖥 | 双击正文字段 + highlight 各改一处 → 同一 Save 条;Network 仅一次 update(只含改动 key + ifMatch);Esc/Cmd+Enter | objectui#2542/#2549/#2604 | +| E2 | 多按钮 record header(order + primary tie-break,⋯ 溢出) | 🖥 | ≥3 动作对象详情页头并排按钮;Delete/Share 进 ⋯ | objectui#2574 | +| E3 | related list 权限门控 | 🖥 | 对子对象无 read 权限用户 → section 整体不渲染 | objectui#2565 | +| E4 | kanban:stageField 默认 lane + highlightFields 默认卡片 | 🖥 | 未配 lane/cardFields 的看板按语义角色渲染 | objectui#2596/#2541 | +| E5 | 拼音搜索:列表快搜/lookup picker/⌘K 输 `zw` 命中中文 | 🖥 | showcase 种子开箱可演示 | #3027/#3034 | +| E6 | conditional tabs(page:tabs item visibleWhen) | 🖥 | 谓词为 FALSE 时整个 tab 消失,变量变化 live 重估 | #2967 + objectui#2516 | +| E7 | B3 级联选项:client re-filter + server 拒绝 | 🖥🤖 | cascading-select fixture;live e2e 已钉 | #3006 + objectui#2547/#2562 | +| E8 | GB18030 CSV 导入 | 🖥 | 导入 GBK 编码 CSV → 中文表头正常、可映射 | objectui#2557 | +| E9 | toast 不再因 error 对象崩页(React #31) | 🖥 | 触发 400 的操作 → 可读 toast,页面不白屏 | objectui#2580 | +| E10 | Studio/metadata-admin 跟随应用 locale | 🖥 | 切 en/zh → Studio 同步切换,无混语言 | objectui#2602 | +| E11 | 活动 feed/audit 摘要 workspace locale 本地化 | 🖥 | zh 环境下动词模板 + 对象 label 中文 | #3045/#3029 | +| E12 | Create User:param 可翻译 + 显式密码胜出 | 🖥 | zh 下对话框字段中文;输显式密码不报 400 | #3030/#3031/#3033 | +| E13 | action visible 三陷阱(os.user.* 别名/record_more/报错谓词 warn) | 🖥🤖 | `locations:['record_more']` 动作出现在 ⋯ 菜单 | objectui#2611 | + +## F. Studio(设计期) + +| # | 项 | 方式 | 验证要点 | 来源 | +|---|---|---|---|---| +| F1 | CEL formula 编辑器:推断结果类型、summary 结构化 roll-up | 🖥 | `record.amount * 1.1` → Result type: Number;裸字段名报错给修法 | objectui#2609 | +| F2 | 字段条件规则 CEL 编辑器(visibleWhen/readonlyWhen/requiredWhen) | 🖥 | `previous.` 成员补全 + did-you-mean | objectui#2571 | +| F3 | list-view 条件格式化编辑器(first-match-wins) | 🖥 | 加 CEL+颜色规则 → 运行时行着色 | objectui#2558/#2544 | +| F4 | RLS 编辑器 lint/补全/test-run | 🖥 | 每策略选记录试跑命中 | objectui#2533 | +| F5 | Access:OWD 总览批量编辑 + 深链 + external≤internal 校验 | 🖥 | 见 A11 | objectui#2508 | +| F6 | Access:unsaved 三连守卫(矩阵/pillar SPA 导航/OWD 行) | 🖥 | 不保存点走 → confirm;取消保留编辑 | objectui#2588/#2606/#2610 | +| F7 | 只读包锁定权限矩阵 | 🖥 | read-only 包矩阵置灰、无 Save | objectui#2570 | +| F8 | AccessExplainPanel record 粒度 + 三态溯源徽标 | 🖥 | 选记录 → visible 结论横幅 + 逐层归因 | objectui#2502/#2501 | +| F9 | flow palette 搜索/键盘导航/云同步 recents | 🖥 | 搜索框 + ↑↓ Enter;recents 分组 | objectui#2543/#2553 | +| F10 | Decision Branches 行内 Target 选择 | 🖥 | 表内选目标 → 画布连边;清除只断边 | objectui#2568 | +| F11 | Page/View inspector 不崩(lazySchema × toJSONSchema) | 🖥 | Studio 打开 Page/View 编辑面 → 无 `setting 'ref'` 崩溃 | #3021 | +| F12 | package spec-form modal + namespace 前缀编辑期强制 | 🖥 | New package modal;"+ new object" 自动 `_` 前缀 | objectui#2535/#2524 | +| F13 | feature-gated action UI 门控 | 🖥 | 关插件 → 相关按钮消失而非 404 | #2965 + objectui#2536 | + +## G. 平台 / 协议 / 部署(API·CLI 面) + +| # | 项 | 方式 | 验证要点 | 来源 | +|---|---|---|---|---| +| G1 | `aggregate_records` MCP 工具走 ENGINE 读路径 | 🔌 | RLS/tenant 与 find 一致;FLS 输入门 fail-closed | #2976 | +| G2 | standalone authored action 上 MCP 桥 | 🔌 | Studio 授权独立 action `ai.exposed:true` → list_actions 可见 | #3010/#3020 | +| G3 | packages REST:PATCH 路由/POST 重复 409/install overwrite | 🔌 | 三条包管理修复 | #2995/#2971/#3007 | +| G4 | ADR-0087:durable-package rehydration 握手 + `migrate meta --from 10` | 🔌 | 不兼容包结构化拒载,boot 继续 | #2972 | +| G5 | honest capabilities:discovery 不虚报 realtime/stub 状态 | 🔌 | discovery JSON 无 `/realtime` 虚假路由 | ADR-0076 slice | +| G6 | `kernel:bootstrapped` + `app:seeded` membership backfill | 🔌 | seed 完成后无需重启即有 membership | #2989/#3000 | +| G7 | Docker 镜像 + 脚手架 container-ready | 🔌 | `docker run` + `/api/v1/health` 200 | #2952/#2979 | +| G8 | tenancy strict block:未知 key 响亮 parse error + tombstone | 🔌 | `strategy:` 残留 → 报错含迁移指引 | #2962 | +| G9 | 拼音 `__search` 伴随列 boot backfill + reconcile | 🔌 | 存量行重启后可拼音搜到 | #3027 | +| G10 | REST 403 透传 / 未知 `$` 参数 400 | 🔌 | 记录级 403 带 code;`$bogus=1` → 400 UNSUPPORTED_QUERY_PARAM | #2926 ⑦⑩ | + +## 回归门(发布前必须全绿) + +- [ ] `pnpm build` 全绿(71 包) +- [ ] `pnpm test` 全绿(含 dogfood 套件:owner-anchor / static-readonly / anonymous-deny / attachments matrix / detail-shapes e2e / MCP connector CI-pinned demo) +- [ ] objectui `e2e/live/showcase-smoke.spec.ts`(全 showcase surface 白屏/空图表/占位符泄漏扫描) +- [ ] `pnpm check:release-notes`(release 页 drift guard) +- [ ] `os validate` 对 examples(schema+表达式+widget 三道门)