All checks were successful
技能自动化发布 / release (push) Successful in 15s
基于 skill-template 脚手架开发,包含: - 5 张业务表(教职工、请假政策、考勤日历、请假记录、假期余额) - 请假核算引擎(扣薪计算、审批层级判定、年假抵扣) - CLI 入口(import-employees / calculate-leave / render-html) - HTML 审批与扣薪报告生成 - 示例数据与完整测试套件(295 tests passed)
384 lines
14 KiB
Markdown
384 lines
14 KiB
Markdown
# Skill Data Policy 契约(v1.0)
|
||
|
||
> **状态:正式契约 v1.0(`schemaVersion` 1)。**
|
||
> **目标:** 冻结「启用方式 + Snapshot 最小字段 + L2 scope 白名单」,使多数技能(L0)零配置,需要权限的技能(L1/L2)有稳定扩展面。
|
||
> **非目标:** 宿主实现业务用户/角色库;规定飞书/HTTP/本地表等权限数据源;本期不强制 L3(行态 `visibleWhen` 等)。
|
||
> **注意:** L3 预留的行态 `visibleWhen` **不是** Action 参数表单的 `inputSchema.properties.*.visibleWhen`(表单控件显隐,见 [`ACTIONS.md`](ACTIONS.md)「字段条件显隐」)。二者同名不同义,勿混用。
|
||
> **配套 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** | (预留,本契约不冻结行为) | 如**行态** `visibleWhen`、菜单级藏表、`rows.mode=delegate` 等(≠ Action 表单 `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": "process-leave-attendance-default",
|
||
"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": "process-leave-attendance-default",
|
||
"actor": { "userId": "u1", "displayName": "张三", "roles": ["clerk"] },
|
||
"actions": {
|
||
"mode": "allowlist",
|
||
"ids": ["whoami", "create-record", "register-submit"]
|
||
}
|
||
}
|
||
```
|
||
|
||
### 8.3 L1 + 限制导出
|
||
|
||
```json
|
||
{
|
||
"schemaVersion": 1,
|
||
"skill": "process-leave-attendance-default",
|
||
"actor": { "userId": "u1", "roles": ["clerk"] },
|
||
"actions": { "mode": "allowlist", "ids": ["whoami"] },
|
||
"hostCapabilities": ["refresh", "view"]
|
||
}
|
||
```
|
||
|
||
### 8.4 L1 open + L2 scope
|
||
|
||
```json
|
||
{
|
||
"schemaVersion": 1,
|
||
"skill": "process-leave-attendance-default",
|
||
"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;写路径技能自检
|