feat(spec)!: 未知键在 flow / permission 授权 schema 上大声拒绝(#4001 Tier-A 切片) - #4071
Conversation
…emas (#4001 Tier-A) Zod's default .strip silently discarded undeclared keys on authorable schemas — the instance kept parsing, so a mis-spelled or wrong-layer key shipped as metadata that quietly ignored the author's config (#3405, #1535). This lands the #4001 Tier-A slice inside the v17 breaking window: - security/permission.zod.ts: PermissionSetSchema, ObjectPermissionSchema, FieldPermissionSchema, AdminScopeSchema are .strict() with fixable errors (semantic aliases, wrong-layer pointers, tombstones for the retired contextVariables / isProfile). EffectiveObjectPermissionSchema explicitly .strip()s back — response shapes stay wire-tolerant. - automation/flow.zod.ts: FlowSchema, FlowNodeSchema, FlowEdgeSchema, FlowVariableSchema are .strict(); a node's config record stays open (the executor's configSchema, #4027/#4040, and the ADR-0087 conversion layer own it). - shared/suggestions.zod.ts: new strictUnknownKeyError factory generalizing the #3746 hand-rolled map; ui/action.zod.ts re-homes onto it with byte-identical messages. - PermissionSetSchema gains the declared `description` key — the strict gate's first catch: the built-in default sets author it and the Setup projection reads it, but the schema could not represent it, so it was silently stripped at every parse (ADR-0078 §3 inverse drift). - docs/audits/2026-07-unknown-key-strictness-ledger.md: the authorable / wire / open triage (#4001 step 1) with the ratchet's next targets. Verified zero breakage: spec 6960 tests + tsc clean; lint / metadata / platform-objects / metadata-core / metadata-protocol / sdui-parser / service-automation / plugin-security / core / rest / objectql / runtime suites green; showcase / crm / todo `objectstack validate` clean; all 12 spec check gates pass; no doc teaches a now-rejected key. Refs #4001 (Tier-A slice: ledger + first ratchet step; the long tail stays verify-then-enforce per the ledger). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0147tNF4Snk7Ry1KGt4a5PY4
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 2 package(s): 106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…; drop a dead flow key The dogfood gate caught two more instances of the exact class #4001 targets — both invisible until the .strict() gate made them loud: - PermissionSetSchema could not represent the ADR-0010 runtime protection envelope. MetadataPlugin's artifact loader calls applyProtection on EVERY metadata type, and getMetaItemLayered -> saveMetaItem round-trips a body carrying the stamped _packageId/_provenance; every sibling registered type (object/view/app/dashboard/report/dataset/flow/agent/tool/skill/ email_template) spreads MetadataProtectionFields and permission was the outlier, so the envelope was stripped at every parse. Now declared, with the author-facing `protection` block alongside it. Without this a package-owned set cannot round-trip through an ADR-0094 environment overlay. - flow-touch-fixture.ts declared `sharingModel` on a FLOW — an object-level OWD key copy-pasted with an ADR-0090 grandfather-stamp comment describing a gate flows do not have. Authored in good faith, silently discarded, believed to be in effect. Removed; the identical stamp on the fixture's object is real. Verified: dogfood 71 files / 410 tests pass locally; spec 6961 tests; all 12 spec check gates green. Refs #4001 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0147tNF4Snk7Ry1KGt4a5PY4
|
Dogfood gate 的两个失败已修复并推送( 1. 2. dogfood fixture 在一个 flow 上写了 连同首轮的 顺带记录一个同类隐患(本 PR 不扩大范围): 本地验证:dogfood 71 文件 / 410 用例全过,spec 6961 用例,12 个
Generated by Claude Code |
…positions (#4001 step 2) (#4099) Second click of the unknown-key strictness ratchet (first: flow + permission, #4071), covering the remaining small security-class authoring surfaces from the ledger's next-target list: - security/rls.zod.ts: RowLevelSecurityPolicySchema is .strict() — a silently dropped key meant a row-level restriction was never compiled into the filter. The runtime shapes stay tolerant; the retired `priority` keeps its tombstone. - security/sharing.zod.ts: the sharing-rule base, criteria extension, and sharedWith recipient are .strict() (strictness and the error map ride the base into the extension). - identity/position.zod.ts: PositionSchema is .strict() and gains the protection block + ADR-0010 runtime envelope, closing the sibling gap the #4071 ledger flagged. Fourth ledger finding: position.test.ts asserted a fictional hierarchy — a pre-ADR-0090 `parent` key that only ever "passed" because .strip ate it. Rewritten to assert rejection with the flatness guidance. Verified: spec 6973 tests + tsc clean; all 12 check gates; plugin-security 677 / lint 546 / metadata 276 / platform-objects 239 / metadata-protocol 99; dogfood 72 files / 418 tests; showcase / crm / todo validate clean.
…4001 step 3) (#4119) Third click of the unknown-key strictness ratchet (flow + permission in #4071, RLS / sharing / position in #4099). Approval is a v17-new authoring surface — tightened while young, before stored volume exists: - ApprovalNodeConfigSchema, ApprovalNodeApproverSchema, ApprovalEscalationSchema and DecisionOutputDefSchema are .strict() with fixable errors via the shared strictUnknownKeyError factory. - ApprovalNodeConfig's guidance is the ADR-0019 re-home map: steps -> successive approval nodes on the canvas, entryCriteria -> the entering edge's condition, onApprove/onReject -> the approve/reject out-edges, rejectionBehavior -> a declared back-edge (ADR-0044) with maxRevisions. - The published JSON schema (getApprovalNodeConfigJsonSchema) carries additionalProperties:false into the Studio property form AND registerFlow's per-node config validation (#4027/#4040). Verified the #3746 hazard: z.toJSONSchema on the strict lazySchema does not throw. Verified: spec 6995 tests + tsc clean; all 12 check gates; plugin-approvals 326 / service-automation 457; dogfood 72 files / 418 tests; showcase / crm / todo validate clean.
#4001 的第一步落地:分类账本 + Tier-A 收紧 + 迁移/释放说明,赶在 v17 RC 窗口内(strict 收紧是破坏性变更,只能搭 major)。按 issue 自己的棘轮原则,不做全量收紧——只做有实锤证据、且逐项实证零破坏的切片。
做了什么
1. 分类账本(#4001 建议第 1 条) —
docs/audits/2026-07-unknown-key-strictness-ledger.md「这个 schema 的输入由谁写」三分法(可授权 / 线上 / 开放),五个可授权目录逐文件triage(含暂定标记),其余目录粗分类,并列出棘轮下一步目标(
app.zod.ts、rls/sharing、approval等)。2. Tier-A 收紧(#3746 模板 → 共享工厂)
shared/suggestions.zod.ts新增strictUnknownKeyError工厂:把 feat(spec): reject unknown keys on an action param instead of stripping them (#3405) #3746 手写的错误映射泛化成一个调用(别名表 + 长度相对编辑距离 + 墓碑/错层指引),ui/action.zod.ts原样 re-home,消息字节一致——这是后续长尾每个 schema 的「一次调用」成本。security/permission.zod.ts—PermissionSetSchema/ObjectPermissionSchema/FieldPermissionSchema/AdminScopeSchema全部.strict()。权限容器上的静默剥离就是 ADR-0049 那种不对称本身:作者以为声明了授权/限制,运行时根本没见过。退役键带墓碑:contextVariables(ADR-0105 D11)、isProfile(ADR-0090 D2)。响应侧EffectiveObjectPermissionSchema显式.strip()回宽(.extend()会继承 strict,线上形状必须保持前向兼容)。automation/flow.zod.ts—FlowSchema/FlowNodeSchema/FlowEdgeSchema/FlowVariableSchema全部.strict()。节点config保持开放:那是各节点类型 executor 的configSchema(A designerconfigSchemaand the executor's wire payload are two unchecked lists — nothing validates flow node config keys at author time #4027/fix(automation,lint,spec): validate the expression slots a node'sconfigSchemadeclares (#4027) #4040)和 ADR-0087 转换层的地盘。别名表覆盖 AI 高频借词:steps→nodes、边from/to→source/target、guard/when→condition、流顶层object/objectName/schedule指路到 start 节点config。3. 闸门开启当天就抓到一个真 bug —
PermissionSetSchema无法表达description,但 plugin-security 的内置权限集一直在写它、Setup 投影(permission-set-projection.ts)一直在读它——此前每次 parse 都被静默剥离(ADR-0078 §3 逆向漂移类)。按 contract-first 修 spec:description现已声明,liveness ledger 同步分类。这也证伪了「仓内元数据不带未知键」的推断——实证闸门不是流程仪式。4. 迁移 / 释放说明 — changeset(major,含 FROM→TO 全表)+
content/docs/releases/v17.mdx新增小节。为什么迁移是行为保持的
被 strict 拒绝的键此前全部被静默丢弃——它们从未产生过任何运行时效果。所以对既有正常应用,删除或改名这些键不改变任何行为;变化只是 validate 不再说谎。错误消息本身携带修复方案,AI 作者走 ADR-0038 循环即可自修。
零破坏实证(#3746 同款证据链)
@objectstack/spec:266 文件 / 6960 用例全过,tsc --noEmit干净objectstack validate全过(showcase 29 流 + 8 权限集)check:*闸门 PASS(permission/description已在 liveness ledger 分类)content/docs/**/*.mdx扫过:0 份文档在教会被新校验拒绝的键(state-machine 的transitions、布局 DSL 的tabs/steps是别的 surface,不受影响)踩坑对照(#4001「照做能省一轮 CI」清单)
/** */之后,check:docs一次过;permission.mdx的再生成 diff 只有description一行(预期内的真实新键,非漂移)check:api-surface在build之后跑(新公共导出:strictUnknownKeyError/StrictUnknownKeyErrorOptions).strict().transform()管道,lazySchema的toJSONSchema隐患未触发(gen:schema 1783 个 schema 正常生成)后续(不在本 PR)
按账本 verify-then-enforce:
ui/app.zod.ts(导航递归 union 需要单独设计错误呈现)→security/rls.zod.ts+sharing.zod.ts→automation/approval.zod.ts→data/hook/datasource;长尾在 minor 里先走 lint 告警(非破坏),v18 收口。账本本身后续可升级为机器校验闸门(check:liveness模式)。Refs #4001(不关闭——这是战役的第一个棘轮步)
🤖 Generated with Claude Code
https://claude.ai/code/session_0147tNF4Snk7Ry1KGt4a5PY4
Generated by Claude Code