Release v1.0.73: Data Policy contract and schemas
All checks were successful
技能自动化发布 / release (push) Successful in 7s

This commit is contained in:
2026-08-09 10:53:31 +08:00
parent 51be1517c0
commit 0b6bf0b2fa
15 changed files with 702 additions and 12 deletions

View File

@@ -74,32 +74,34 @@ Action 是**可选能力**:
| `skill` | 是 | 必须与 `SKILL.md` 的 `metadata.openclaw.slug` 一致 |
| `actions` | 是 | Action 数组,每项 `id` 唯一;**数组相对顺序**决定数据管理 toolbar / row / batch 技能按钮的展示顺序(见下节) |
模板最小示例见 [`assets/actions.json`](../assets/actions.json);JSON Schema 见 [`assets/schemas/skill-actions.schema.json`](../assets/schemas/skill-actions.schema.json)。
模板最小示例见 [`assets/actions.json`](../assets/actions.json);JSON Schema 见 [`assets/schemas/skill-actions.schema.json`](../assets/schemas/skill-actions.schema.json)。可选 Data Policy 见 [`POLICY.md`](POLICY.md) 与 [`assets/schemas/skill-policy-snapshot.schema.json`](../assets/schemas/skill-policy-snapshot.schema.json)。
## 五职责正交(必读)
## 六职责正交(必读)
| 字段 / 层 | 只决定什么 | 不决定什么 |
|-----------|------------|------------|
| **`placements`** | Action **出现在哪里**(toolbar / row / batch / cron / agent / skill-detail;`row`=行级入口,壳由宿主呈现) | 不决定 sync/async,不决定点了是否弹窗,不决定行级用操作列还是右键 |
| **`placements`** | Action **出现在哪里**(toolbar / row / batch / cron / agent / skill-detail;`row`=行级入口,壳由宿主呈现) | 不决定 sync/async,不决定点了是否弹窗,不决定行级用操作列还是右键,不决定谁能点 |
| **`replaces`** | **可选**:接管数据管理默认「新增 / 编辑 / 删除」 | 不强制;无配置时宿主仍直写 SQLite |
| **`bind.tables`** | 数据管理里出现在**哪些业务表** | 不决定业务实现 |
| **`executionProfile`** | 宿主是**同步等待**还是**异步 Job** | 不限制可展示入口,不决定是否弹窗 |
| **`interaction`** | 可点击入口(数据管理 toolbar/row/batch、默认 CRUD 接管、未来 skill-detail)**点了怎么交互** | 不决定 sync/async;**cron 触发与 agent 忽略** |
| **`policy` / Snapshot** | **可选**:当前身份在数据管理下允许哪些 Action / 宿主内置能力 / 行列范围 | 不替代 placements;不替代 domain 写路径鉴权;多数技能不配 = L0 全开 |
| **`entrypoint`** | 实际执行哪个 CLI 命令与参数 | 不写业务逻辑 |
| **domain service** | **真正完成什么业务** | 不感知 toolbar / cron / agent 来源 |
一个业务能力通常只定义**一个** Action,用 `placements` 同时挂到多个入口。不要拆成 `sync-records-toolbar` / `sync-records-cron` 等副本,除非语义确实不同;即便拆分,也必须调用同一个领域业务内核。
一个业务能力通常只定义**一个** Action,用 `placements` 同时挂到多个入口。不要拆成 `sync-records-toolbar` / `sync-records-cron` 等副本,除非语义确实不同;即便拆分,也必须调用同一个领域业务内核。需要按用户/角色藏按钮或滤行时,**不要**按角色复制 Action,应启用顶层 `policy` 并由技能算出 Snapshot(见 [`POLICY.md`](POLICY.md))。
## Phase 1 当前稳定支持(新技能应只使用这些)
| 层级 | 稳定字段 / 能力 |
|------|----------------|
| 顶层 | `schemaVersion`、`skill`、`actions` |
| action | `id`、`label`、`description`、`placements`、`entrypoint`、**`executionProfile`(必填)**;可选 `inputSchema`、`confirmation`、**`interaction`**、**`interactionByPlacement`**、**`bind`**、**`replaces`** |
| 顶层 | `schemaVersion`、`skill`、`actions`;可选 **`policy`**(Data Policy 启用块) |
| action | `id`、`label`、`description`、`placements`、`entrypoint`、**`executionProfile`(必填)**;可选 `inputSchema`、`confirmation`、**`interaction`**、**`interactionByPlacement`**、**`bind`**、**`replaces`**、**`authz`**(仅文档提示) |
| entrypoint | `type = cli`、`command`、`args`(**必填** JSON 数组,项为 `string` / `number` / `boolean`) |
| placements | `toolbar`、`row`、`batch`、`skill-detail`、`agent`、`cron`;仅 `replaces` 时可为空数组 |
| **replaces** | 可选 `"create"` \| `"update"` \| `"delete"` — 接管默认新增/编辑/删除(须 `bind.tables`) |
| **bind** | `{ "tables": ["business_table"] }`;可选 `inputMapping`(行内/批量预填,见下节) |
| **policy** | 可选;`enabled` + `entrypoint`(推荐 `policy` CLI);见 POLICY.md。未启用 = L0 |
| inputSchema | 根 `type: object`;properties 使用 `string` / `number` / `boolean`;支持 `enum`、`default`、`required`、`sensitive`、`readOnly` |
| confirmation | `{ "message": "..." }` — **风险文案**(confirm/form 弹窗黄条;agent 元数据);**不是**「要不要弹窗」的开关 |
| **executionProfile** | `"sync"` \| `"async"`(**必须显式写出**,禁止依赖缺省) |

View File

@@ -45,6 +45,7 @@ python {baseDir}/scripts/main.py init-db
- **`config-path`**:输出 JSON,包含 `skill`、`env_path`、`example_path`。
- **`version`**:输出 JSON(`version`、`skill`)。
- **`init-db`**:幂等创建/迁移本地 SQLite(含 `_jiangchang_*` 展示元数据)。成功输出一行 JSON(`ok` / `skill` / `db_path`),退出码 `0`。宿主可在技能安装或更新成功后静默调用;不要并入 `health`。
- **`policy`(可选)**:仅当 `actions.json` 顶层启用 Data Policy 时需要;向 stdout 输出 Snapshot JSON(见 `references/POLICY.md`)。模板默认不实现;多数技能保持 L0 即可。
- **`run` / RPA / 长任务**:CLI 仍是同一内核;**Agent 禁止**用 bash 干等长任务结束。须走宿主 Skill Action(`run_skill_action`),且该 Action 一般为 `executionProfile: "async"` → **任务中心**。长时间无 stdout **不代表卡死**;进度看任务中心 / Run Journal。
- **排查顺序**(由快到慢):
1. `python {baseDir}/scripts/main.py logs`

382
references/POLICY.md Normal file
View File

@@ -0,0 +1,382 @@
# Skill Data Policy 契约(v1.0)
> **状态:正式契约 v1.0(`schemaVersion` 1)。**
> **目标:** 冻结「启用方式 + Snapshot 最小字段 + L2 scope 白名单」,使多数技能(L0)零配置,需要权限的技能(L1/L2)有稳定扩展面。
> **非目标:** 宿主实现业务用户/角色库;规定飞书/HTTP/本地表等权限数据源;本期不强制 L3(`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** | (预留,本契约不冻结行为) | 如行态 `visibleWhen`、菜单级藏表、`rows.mode=delegate` 等 |
**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 中;不在则宿主隐藏该接管按钮。`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 或永不做 |
| `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;写路径技能自检

View File

@@ -19,6 +19,7 @@
| [`CLI.md`](CLI.md) | 共享 Python、命令行入口、参数与短操作契约 |
| [`ACTIONS.md`](ACTIONS.md) | Skill Action、sync/async、任务中心、多入口(可选能力) |
| [`SCHEMA.md`](SCHEMA.md) | 本地 SQLite 与数据管理展示契约 |
| [`POLICY.md`](POLICY.md) | Data Policy:可选身份授权、Snapshot、行 scope(多数技能 L0 不配) |
| [`../development/`](../development/) | 开发、测试、RPA 深度规范(给人 / 编码助手) |
按需阅读:短操作优先 `CLI.md`;Action / 任务中心读 `ACTIONS.md`;结构化字段读 `SCHEMA.md`。
按需阅读:短操作优先 `CLI.md`;Action / 任务中心读 `ACTIONS.md`;结构化字段读 `SCHEMA.md`;权限 opt-in 读 `POLICY.md`。