14 KiB
Skill Data Policy 契约(v1.0)
状态:正式契约 v1.0(
schemaVersion1)。
目标: 冻结「启用方式 + Snapshot 最小字段 + L2 scope 白名单」,使多数技能(L0)零配置,需要权限的技能(L1/L2)有稳定扩展面。
非目标: 宿主实现业务用户/角色库;规定飞书/HTTP/本地表等权限数据源;本期不强制 L3(菜单级藏表、rows.mode=delegate等)。
注意:
- 行菜单按业务状态显隐请用 Action 契约
bind.visibleWhen(见ACTIONS.md「行菜单显隐」)——宿主已支持,不属于本 Policy L3。- L3 预留的行态
visibleWhen不是 Action 参数表单的inputSchema.properties.*.visibleWhen,也不是bind.visibleWhen。三者同名不同义,勿混用。
配套 Schema:../assets/schemas/skill-policy-snapshot.schema.json;启用块见../assets/schemas/skill-actions.schema.json。
相关正交轴见 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 必冻)
{
"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 级可选提示(不冻为运行时权威)
允许(文档/静态检查用),宿主不得仅凭此显隐按钮:
"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 列表。宿主不理解角色名。
"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 |
规则:
- 宿主先按现有规则得到候选集:
placements∩bind.tables(及 batch 等)。 - 若
mode=allowlist:候选 ∩ids。漏配 id = 少按钮(安全侧失败)。 ids中的未知 id:忽略(不报错)。mode=open:等价「L1 不限制按钮」,可与 L2 行列限制并存。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 形状
"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 布尔结构(冻)
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. 宿主消费算法
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. 安全边界
- Snapshot 只服务宿主 UI 与查询约束。
- 写 Action / 敏感读的 domain service 必须再次鉴权。
- 禁止「只藏按钮」宣称为完整权限方案。
- Cron / Agent 不自动套用本 Snapshot。
8. 最小示例
模板仓库另有可复制示例:
../assets/examples/skill-policy-snapshot.allowlist.example.json../assets/examples/skill-policy-snapshot.scope.example.json
8.1 L0
不写 policy 字段。
8.2 L1 allowlist(不碰宿主内置)
{
"schemaVersion": 1,
"skill": "your-skill-slug",
"actor": { "userId": "u1", "displayName": "张三", "roles": ["clerk"] },
"actions": {
"mode": "allowlist",
"ids": ["whoami", "create-record", "register-submit"]
}
}
8.3 L1 + 限制导出
{
"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
{
"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. 冻结检查清单
- L0 缺省无歧义
- 启用字段仅
enabled+entrypoint+ 可选appliesTo actions.mode仅allowlist/openhostCapabilities省略 = 不限制- scope 小白名单;
valueFrom仅actor.userId - fail-closed;写路径技能自检