# Skill Data Policy 契约(v1.0) > **状态:正式契约 v1.0(`schemaVersion` 1)。** > **目标:** 冻结「启用方式 + Snapshot 最小字段 + L2 scope 白名单」,使多数技能(L0)零配置,需要权限的技能(L1/L2)有稳定扩展面。 > **非目标:** 宿主实现业务用户/角色库;规定飞书/HTTP/本地表等权限数据源;本期不强制 L3(菜单级藏表、`rows.mode=delegate` 等)。 > **注意:** > - 行菜单按业务状态显隐请用 Action 契约 **`bind.visibleWhen`**(见 [`ACTIONS.md`](ACTIONS.md)「行菜单显隐」)——宿主已支持,**不属于**本 Policy L3。 > - L3 预留的行态 `visibleWhen` **不是** Action 参数表单的 `inputSchema.properties.*.visibleWhen`,也**不是** `bind.visibleWhen`。三者同名不同义,勿混用。 > **配套 Schema:** [`../assets/schemas/skill-policy-snapshot.schema.json`](../assets/schemas/skill-policy-snapshot.schema.json);启用块见 [`../assets/schemas/skill-actions.schema.json`](../assets/schemas/skill-actions.schema.json)。 相关正交轴见 [`ACTIONS.md`](ACTIONS.md)。本契约增加第六轴:**policy(当前身份允许什么)**。 --- ## 0. 能力档位(必须按此理解) | 档位 | 技能配置 | 宿主行为 | |------|----------|----------| | **L0** | 无 `policy`,或 `policy.enabled !== true` | **与今日完全一致**:不拉 Snapshot,不按身份过滤按钮/行/列 | | **L1** | `policy.enabled: true`,Snapshot 至少给出 Action 授权 | 数据管理展示 skill Action 前与 allowlist 求交;可选限制宿主内置能力 | | **L2** | 在 L1 基础上 Snapshot 带 `data.tables[table]` | 列表查询应用声明式 `rows.scope`;列按 `columns` 投影 | | **L3** | (预留,本契约不冻结行为) | 如菜单级藏表、`rows.mode=delegate`、更复杂的行态策略字段等(≠ `bind.visibleWhen` 行菜单,≠ 表单 `inputSchema.visibleWhen`) | **80% 技能停留 L0。** 启用 policy 的技能必须实现约定 CLI,并在写路径自行再鉴权(见 §7)。 --- ## 0.1 已冻结最佳实践(v1.0) | 议题 | 决定 | 技能开发者怎么做 | |------|------|------------------| | Skill Action 授权模式 | **仅** `allowlist` / `open`;v1 **禁止** `denylist` | 有角色就算出允许的 action id 列表;只控行不控按钮时用 `open` | | `hostCapabilities` | **省略 = 不限制**宿主内置;写出则按 allowlist(`[]` = 全禁内置) | 只控业务按钮时**不要写**该字段;要限导出/删除时再显式列出 | | scope 运算符 | 保留小白名单(见 §3.4);**禁止** like/raw/嵌套 | 日常优先 `eq` / `in` / `is_null`;比较符按需 | | `valueFrom` | v1 **仅** `actor.userId` | 角色只在技能内用来算 Snapshot,**不要**用 `actor.roles` 做行过滤 | --- ## 1. 启用字段(冻在 `assets/actions.json` 顶层) `actions.json` 根对象增加**可选**字段 `policy`(与 `schemaVersion` / `skill` / `actions` 同级)。 ### 1.1 形状(L0/L1 必冻) ```json { "schemaVersion": 1, "skill": "your-skill-slug", "policy": { "enabled": true, "entrypoint": { "type": "cli", "command": "policy", "args": ["--table", "{{table}}"] }, "appliesTo": ["data-management"] }, "actions": [] } ``` | 字段 | 必填 | 冻结说明 | |------|------|----------| | `policy` | 否 | 缺省 = L0 | | `policy.enabled` | 若存在 `policy` 则**必填** | 仅 `true` 启用;`false` 等同 L0 | | `policy.entrypoint` | `enabled: true` 时必填 | 与 Action `entrypoint` 同构:`type` 目前仅 `"cli"` | | `policy.entrypoint.command` | 是 | 子命令名;**推荐**固定为 `policy` | | `policy.entrypoint.args` | 是 | JSON 数组;可含占位符,由宿主替换后再拼接 CLI | | `policy.appliesTo` | 否 | 缺省 `["data-management"]`。本期宿主**只实现** `data-management`;其它值忽略但不报错 | ### 1.2 占位符白名单(冻) | 占位符 | 含义 | 例 | |--------|------|----| | `{{table}}` | 当前数据管理选中的**物理表名** | `customs_timelines` | | `{{skill}}` | 当前技能 slug | 与 manifest `skill` 一致 | | `{{userId}}` | 当前宿主用户 id | 与注入的 `JIANGCHANG_USER_ID` 一致 | 未列出的 `{{…}}`:**禁止**;宿主应视为契约错误(fail-closed,见 §6)。 ### 1.3 显式不放进启用块的内容(防推翻) 以下**不得**出现在 `actions.json.policy` 中(属于技能内部或 Snapshot): - 角色列表、人员表、飞书 app id、外部 IAM URL - 具体 allowlist / scope(那是 Snapshot 的事) - 按角色复制的 Action 定义 ### 1.4 Action 级可选提示(不冻为运行时权威) 允许(文档/静态检查用),**宿主不得仅凭此显隐按钮**: ```json "authz": { "capability": "optional.skill.defined.string" } ``` 运行时权威仅为 Snapshot §2 的 `actions`。 --- ## 2. Snapshot 最小字段集(`policy` CLI 标准输出) - 编码:UTF-8 JSON 对象,写在 **stdout**(可先打日志到 stderr/文件,但 stdout 必须是单 JSON)。 - 成功退出码 `0`;业务上「无权限看任何东西」仍是 `0` + 合法 Snapshot(例如空 allowlist),不是靠非 0 表示未授权。 - 非 0 或非合法 JSON → §6 fail-closed。 ### 2.1 根对象(L0 不出现;L1 必冻) | 字段 | 必填 | 类型 | 说明 | |------|------|------|------| | `schemaVersion` | 是 | `integer` | 本期冻结为 **`1`** | | `skill` | 是 | `string` | 必须等于 manifest `skill` | | `actor` | 是 | `object` | 见 §2.2 | | `actions` | 是 | `object` | 见 §2.3 | | `hostCapabilities` | 否 | `string[]` | 见 §2.4;**省略 = 不限制宿主内置按钮**(兼容) | | `data` | 否 | `object` | 见 §3;省略 = 不限制行/列(L1-only) | | `extensions` | 否 | `object` | 技能私有扩展;宿主**必须忽略**未知键 | 根级未知字段:宿主**忽略**;**不得**依赖未写入本契约的字段做安全决策。 ### 2.2 `actor`(L1 必冻) | 字段 | 必填 | 说明 | |------|------|------| | `userId` | 是 | 非空;应与 `JIANGCHANG_USER_ID` / `{{userId}}` 对齐 | | `displayName` | 否 | 展示用 | | `roles` | 否 | `string[]`,角色**码**;仅供展示 / 技能自检。**不得**用于 v1 scope 的 `valueFrom` | | `attributes` | 否 | `object`,仅允许 JSON 标量值;本期 scope **不**解引用 | ### 2.3 `actions`(L1 必冻)— Skill Action 授权 **最佳实践:** 角色 → 技能内计算 → `allowlist` 的 id 列表。宿主不理解角色名。 ```json "actions": { "mode": "allowlist", "ids": ["whoami", "create-record", "register-submit"] } ``` | 字段 | 必填 | 冻结取值 | |------|------|----------| | `mode` | 是 | **仅两值**:`"allowlist"` \| `"open"` | | `ids` | `mode=allowlist` 时必填 | `string[]`;元素为 **`actions.json` 里的 action.id** | | `ids` | `mode=open` 时 | 必须省略或 `[]`;宿主视为**不限制** skill Action | 规则: 1. 宿主先按现有规则得到候选集:`placements` ∩ `bind.tables`(及 batch 等)。 2. 若 `mode=allowlist`:候选 ∩ `ids`。漏配 id = 少按钮(安全侧失败)。 3. `ids` 中的未知 id:**忽略**(不报错)。 4. `mode=open`:等价「L1 不限制按钮」,可与 L2 行列限制并存。 5. **`denylist` 不是 v1 合法值**;出现则 §6 fail-closed。 `replaces` 接管的默认新增/编辑/删除/导入:视为对应 Action 的 id 是否在 allowlist 中;不在则宿主隐藏该接管按钮(导入按钮是否可用还受 `hostCapabilities` 含 `import` 影响)。`mode=open` 时不按 id 限制 replaces。 ### 2.4 `hostCapabilities`(L1 可选冻) 控制数据管理**宿主内置**能力,与 skill `action.id` **命名空间分离**。 本期枚举冻结:`refresh` | `export` | `view` | `edit` | `delete` | `import`。 | 写法 | 含义 | 何时用 | |------|------|--------| | **省略字段** | **不限制**(与今日一致) | **默认推荐**:只控 skill 按钮 / 行数据时 | | 写出非空数组 | 仅允许列出的内置能力(allowlist) | 要限导出、删除等时 | | 写出 `[]` | 禁止全部上表内置能力 | 极少见;慎用 | 未知字符串:忽略。**禁止**把「省略」解释成全禁。 --- ## 3. L2:`data` 与 scope 语法白名单 ### 3.1 `data` 形状 ```json "data": { "tables": { "customs_timelines": { "accessible": true, "columns": { "visible": ["id", "contract_no", "status"], "writable": ["status"] }, "rows": { "mode": "scope", "scope": { "all": [ { "field": "owner_user_id", "op": "eq", "valueFrom": "actor.userId" } ] } } } } } ``` | 字段 | 必填 | 说明 | |------|------|------| | `data.tables` | 若存在 `data` 则建议有 | `key` = 物理表名 | | `accessible` | 否 | 缺省 `true`。`false`:宿主可拒绝打开该表 | | `columns.visible` / `writable` | 否 | 物理列名;省略 = 不额外限制 | | `rows` | 否 | 见下 | `writable` 应 ⊆ `visible`;若违规,宿主按交集降级,不 fail-closed。 ### 3.2 `rows.mode`(冻) | 值 | 本期 | 含义 | |----|------|------| | `scope` | **实现目标** | 宿主将 `scope` 编译为安全查询约束 | | `open` | 允许 | 显式不限制行 | | `delegate` | **仅占位** | 本期未实现时 fail-closed / 提示;**禁止**当 `open` | 缺省:无 `rows` = `open`。 ### 3.3 `scope` 布尔结构(冻) ```text Scope = | { "all": Predicate[] } // AND,长度 ≥ 1 | { "any": Predicate[] } // OR,长度 ≥ 1 | Predicate // 单个谓词的语法糖 ``` **禁止**:嵌套超过一层。 ### 3.4 `Predicate` 白名单(冻) **运算符(宿主必须实现):** `eq` | `ne` | `in` | `not_in` | `gt` | `gte` | `lt` | `lte` | `is_null` | `is_not_null` **推荐日常只用:** `eq` / `in` / `is_null`(及 `is_not_null`)。 **明确排除:** `like` / `contains` / `sql` / `raw` / `expr`。 **`valueFrom` v1 仅:** `actor.userId`。 `actor.roles` / `actor.attributes.*` **非法** → fail-closed。 `value` 与 `valueFrom` 互斥。标量:`string` | `number` | `boolean` | `null`(与 null 比较优先用 `is_null`)。 ### 3.5 与宿主原有过滤的关系 最终行集 = **原有表查询**(搜索、排序、分页、用户 Filter) **AND** policy `scope`。宿主必须把 scope 下推到查询层。 --- ## 4. 宿主消费算法 ```text IF manifest.policy?.enabled !== true: → L0;END 调用 entrypoint(替换占位符),解析 Snapshot IF 失败 / 非法 / skill 不匹配 / mode 非法: → fail-closed(§6);END 候选 = filter(placements, bind.tables, …) IF mode == allowlist: 候选 ∩ ids IF mode == open: 候选不变 渲染 skill 按钮 ← 候选 渲染宿主内置 ← hostCapabilities(省略则不限制) 应用 data.tables[T](若有) ``` 拉取时机(建议):进入表、切换表、刷新。短缓存不得跨 `userId`。 --- ## 5. 身份与环境 | 项 | 约定 | |----|------| | 用户主键 | `JIANGCHANG_USER_ID` | | 技能解析 | 飞书 / 本地表 / HTTP IAM 等,**契约不规定** | --- ## 6. Fail-closed(启用后必冻) 当且仅当 `policy.enabled === true`: | 情况 | 宿主行为 | |------|----------| | CLI 非 0 / 超时 / 无 JSON | 不渲染该表 skill 写操作与(建议)导出;明确错误;**禁止**回退 L0 全开 | | `schemaVersion` ≠ 1 / `skill` 不匹配 / `actions.mode` 非法 | 同上 | | `rows.mode=scope` 但 predicate 非法 | 不返回行或整表不可用;**禁止**忽略 scope | | `rows.mode=delegate`(未实现) | 提示不支持;**禁止**当 `open` | --- ## 7. 安全边界 1. Snapshot **只**服务宿主 UI 与查询约束。 2. 写 Action / 敏感读的 **domain service 必须再次鉴权**。 3. 禁止「只藏按钮」宣称为完整权限方案。 4. Cron / Agent **不**自动套用本 Snapshot。 --- ## 8. 最小示例 模板仓库另有可复制示例: - [`../assets/examples/skill-policy-snapshot.allowlist.example.json`](../assets/examples/skill-policy-snapshot.allowlist.example.json) - [`../assets/examples/skill-policy-snapshot.scope.example.json`](../assets/examples/skill-policy-snapshot.scope.example.json) ### 8.1 L0 不写 `policy` 字段。 ### 8.2 L1 allowlist(不碰宿主内置) ```json { "schemaVersion": 1, "skill": "your-skill-slug", "actor": { "userId": "u1", "displayName": "张三", "roles": ["clerk"] }, "actions": { "mode": "allowlist", "ids": ["whoami", "create-record", "register-submit"] } } ``` ### 8.3 L1 + 限制导出 ```json { "schemaVersion": 1, "skill": "your-skill-slug", "actor": { "userId": "u1", "roles": ["clerk"] }, "actions": { "mode": "allowlist", "ids": ["whoami"] }, "hostCapabilities": ["refresh", "view"] } ``` ### 8.4 L1 open + L2 scope ```json { "schemaVersion": 1, "skill": "your-skill-slug", "actor": { "userId": "u1" }, "actions": { "mode": "open" }, "data": { "tables": { "orders": { "rows": { "mode": "scope", "scope": { "all": [ { "field": "owner_user_id", "op": "eq", "valueFrom": "actor.userId" } ] } } } } } } ``` --- ## 9. 刻意延后 | 项 | 备注 | |----|------| | `actions.mode: denylist` | 另开 `schemaVersion` | | `valueFrom: actor.roles` | **明确不做** | | `visibleWhen` / 嵌套 scope / `like` | L3 或永不做(此处指**行态**策略字段;Action 表单 `inputSchema.visibleWhen` 见 ACTIONS.md,不属本表) | | `rows.mode: delegate` 宿主实现 | 字段名已占坑 | | 平台全局 Role UI / cron·agent 套用 Snapshot | 另议 | --- ## 10. 冻结检查清单 - [x] L0 缺省无歧义 - [x] 启用字段仅 `enabled` + `entrypoint` + 可选 `appliesTo` - [x] `actions.mode` 仅 `allowlist` / `open` - [x] `hostCapabilities` 省略 = 不限制 - [x] scope 小白名单;`valueFrom` 仅 `actor.userId` - [x] fail-closed;写路径技能自检