diff --git a/CHANGELOG.md b/CHANGELOG.md index 9966b62..92c7322 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ - 小节正文写用户能看懂的变化(新能力、修复、注意事项),避免堆砌内部实现细节 - 打 tag 前**必须**为即将发布的版本新增一节;找不到匹配小节时,本次发布不会更新 changelog 字段 +## 1.0.73 + +- 正式发布 Data Policy 契约(`references/POLICY.md` v1.0):可选身份授权,默认 L0 零配置 +- 新增 Snapshot Schema 与示例;`actions.json` 支持顶层 `policy` 与 Action 可选 `authz` 提示 +- ACTIONS 扩展为六职责正交(含 policy) + ## 1.0.72 - 澄清 `placements: "row"` 契约:表示数据管理**行级入口**;技能配置决定菜单项,宿主可呈现为操作列、行右键和/或行内「⋯」(同一批 Action,无独立菜单 DSL / `row-context-menu` placement) diff --git a/SKILL.md b/SKILL.md index e0b7cc3..810746a 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,7 +1,7 @@ --- name: 技能开发模板(通用业务版) description: "OpenClaw 通用业务技能开发模板,供复制后定制新业务 skill。定制步骤见 development/DEVELOPMENT.md。" -version: 1.0.72 +version: 1.0.73 author: 深圳匠厂科技有限公司 metadata: openclaw: @@ -35,6 +35,7 @@ allowed-tools: - **`placements`**:只决定 Action **出现在哪里**(toolbar / row / batch / cron / agent / skill-detail)。其中 `row` 表示**行级入口**(宿主可呈现为操作列、行右键、行内「⋯」等;技能配什么就显示什么,无独立菜单 DSL)。数据管理技能按钮顺序见 `actions` **数组相对顺序**(详见 `references/ACTIONS.md`),无独立 sortOrder。 - **`bind.tables`**:只决定数据管理出现在哪些表;含 `toolbar` 时**必须**显式非空绑定。 - **`executionProfile`**:只决定执行方式——`sync` 当场返回结果,`async` 进**任务中心**。与 placements / 入口**无关**。心智上多数为 sync,仅长任务 / RPA 标 async;宿主省略该字段时按 sync。每个 Action **仍必须**显式写 `sync` 或 `async`。 +- **`policy`(可选)**:需要按用户/角色控制数据管理按钮或行列时,在 `actions.json` 顶层启用 Data Policy,由技能 `policy` CLI 输出 Snapshot;**未启用 = 与今日一致(L0)**。详见 `references/POLICY.md`。用户/角色数据源(飞书/库/API)属技能内部,宿主不托管。 - **同步数据**写技能本地库、不生成导出文件;**导出当前表**由宿主数据管理负责;特殊业务报告才另做 `export-*`。 - **准备 vs 执行(适用则拆)**:若技能存在「先写入/准备本地数据」与「再对外产生副作用」两段,Agent 必须能分开触发;禁止默认绑成不可拆的一步。一键无准备态的技能可跳过本条。 @@ -68,7 +69,7 @@ allowed-tools: 5. **一个业务能力一个 Action**,用多个 `placements` 挂入口;底层仍是同一 `scripts/main.py` CLI。 6. 是否进任务中心**只**看该 Action 的 `executionProfile`,与数据管理 / 定时 / Agent 入口无关。 -Agent 执行契约细节:`references/CLI.md`、`references/ACTIONS.md`、`references/SCHEMA.md`(不要把 `development/` 当 Agent 主读路径)。 +Agent 执行契约细节:`references/CLI.md`、`references/ACTIONS.md`、`references/SCHEMA.md`、按需 `references/POLICY.md`(不要把 `development/` 当 Agent 主读路径)。 ## 共享 Python(必读) diff --git a/assets/examples/skill-policy-snapshot.allowlist.example.json b/assets/examples/skill-policy-snapshot.allowlist.example.json new file mode 100644 index 0000000..812fc8b --- /dev/null +++ b/assets/examples/skill-policy-snapshot.allowlist.example.json @@ -0,0 +1,13 @@ +{ + "schemaVersion": 1, + "skill": "your-skill-slug", + "actor": { + "userId": "u1", + "displayName": "张三", + "roles": ["clerk"] + }, + "actions": { + "mode": "allowlist", + "ids": ["whoami", "create-record", "sync-records"] + } +} diff --git a/assets/examples/skill-policy-snapshot.scope.example.json b/assets/examples/skill-policy-snapshot.scope.example.json new file mode 100644 index 0000000..80adf17 --- /dev/null +++ b/assets/examples/skill-policy-snapshot.scope.example.json @@ -0,0 +1,28 @@ +{ + "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" + } + ] + } + } + } + } + } +} diff --git a/assets/schemas/skill-actions.schema.json b/assets/schemas/skill-actions.schema.json index fd16323..38de9e1 100644 --- a/assets/schemas/skill-actions.schema.json +++ b/assets/schemas/skill-actions.schema.json @@ -21,9 +21,47 @@ "minItems": 1, "items": { "$ref": "#/$defs/action" }, "description": "Action 列表。同一 placement(toolbar/row/batch)下,宿主按本数组中匹配项的相对顺序展示。toolbar/batch:表顶栏全部直接展示(无「更多操作」溢出)。row:行级入口,宿主可呈现为操作列、行右键和/或行内「⋯」(同一批 Action、同一顺序);不在本字段配置排序、截断或菜单树。调整顺序即调整数组先后。无独立 sortOrder 字段。宿主内置刷新/导出/查看等不参与本顺序。表顶栏宽时左右横排,窄时左右分栏且栏内竖排。" + }, + "policy": { + "$ref": "#/$defs/policyDeclaration", + "description": "可选。启用 Data Policy(L1/L2)。缺省或不 enabled = L0,宿主不拉 Snapshot。详见 references/POLICY.md。" } }, "$defs": { + "policyDeclaration": { + "type": "object", + "required": ["enabled"], + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "description": "仅 true 启用;false 等同 L0。" + }, + "entrypoint": { + "$ref": "#/$defs/entrypoint", + "description": "enabled=true 时必填。推荐 command=policy,args 可含 {{table}}/{{skill}}/{{userId}}。" + }, + "appliesTo": { + "type": "array", + "items": { + "type": "string", + "enum": ["data-management"] + }, + "description": "缺省 [\"data-management\"]。本期宿主只实现 data-management。" + } + }, + "allOf": [ + { + "if": { + "properties": { "enabled": { "const": true } }, + "required": ["enabled"] + }, + "then": { + "required": ["entrypoint"] + } + } + ] + }, "placement": { "type": "string", "enum": ["toolbar", "row", "batch", "cron", "agent", "skill-detail"], @@ -199,7 +237,18 @@ }, "description": "按 placement / replaces 覆盖 interaction(只能写 concrete 模式,不能写 auto)。" }, - "bind": { "$ref": "#/$defs/bind" } + "bind": { "$ref": "#/$defs/bind" }, + "authz": { + "type": "object", + "additionalProperties": false, + "properties": { + "capability": { + "type": "string", + "minLength": 1, + "description": "可选文档/静态提示用能力名;宿主不得仅凭此显隐按钮,运行时以 policy Snapshot 为准。" + } + } + } }, "if": { "properties": { diff --git a/assets/schemas/skill-policy-snapshot.schema.json b/assets/schemas/skill-policy-snapshot.schema.json new file mode 100644 index 0000000..71d4322 --- /dev/null +++ b/assets/schemas/skill-policy-snapshot.schema.json @@ -0,0 +1,196 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://openclaw.local/skill-template/skill-policy-snapshot.schema.json", + "title": "SkillDataPolicySnapshot", + "description": "匠厂 Skill Data Policy Snapshot(schemaVersion 1)。由技能 policy CLI 输出到 stdout;宿主用于数据管理按钮/行列过滤。详见 references/POLICY.md。", + "type": "object", + "required": ["schemaVersion", "skill", "actor", "actions"], + "additionalProperties": true, + "properties": { + "schemaVersion": { + "type": "integer", + "const": 1 + }, + "skill": { + "type": "string", + "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$", + "minLength": 1 + }, + "actor": { + "type": "object", + "required": ["userId"], + "additionalProperties": true, + "properties": { + "userId": { "type": "string", "minLength": 1 }, + "displayName": { "type": "string" }, + "roles": { + "type": "array", + "items": { "type": "string", "minLength": 1 } + }, + "attributes": { + "type": "object", + "additionalProperties": { + "type": ["string", "number", "boolean", "null"] + } + } + } + }, + "actions": { + "type": "object", + "required": ["mode"], + "additionalProperties": false, + "properties": { + "mode": { + "type": "string", + "enum": ["allowlist", "open"], + "description": "v1 仅允许 allowlist/open;denylist 非法。" + }, + "ids": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" + }, + "description": "mode=allowlist 时必填;mode=open 时应省略或 []。" + } + }, + "allOf": [ + { + "if": { + "properties": { "mode": { "const": "allowlist" } }, + "required": ["mode"] + }, + "then": { + "required": ["ids"] + } + } + ] + }, + "hostCapabilities": { + "type": "array", + "items": { + "type": "string", + "enum": ["refresh", "export", "view", "edit", "delete", "import"] + }, + "description": "省略=不限制宿主内置能力;写出则按 allowlist(含 []=全禁)。" + }, + "data": { + "type": "object", + "additionalProperties": false, + "properties": { + "tables": { + "type": "object", + "additionalProperties": { "$ref": "#/$defs/tablePolicy" } + } + } + }, + "extensions": { + "type": "object", + "description": "技能私有扩展;宿主必须忽略未知键。" + } + }, + "$defs": { + "predicate": { + "type": "object", + "required": ["field", "op"], + "additionalProperties": false, + "properties": { + "field": { + "type": "string", + "minLength": 1, + "pattern": "^[a-z][a-z0-9_]*$" + }, + "op": { + "type": "string", + "enum": [ + "eq", + "ne", + "in", + "not_in", + "gt", + "gte", + "lt", + "lte", + "is_null", + "is_not_null" + ] + }, + "value": { + "description": "与 valueFrom 互斥;in/not_in 时为非空数组。" + }, + "valueFrom": { + "type": "string", + "const": "actor.userId", + "description": "v1 仅允许 actor.userId。" + } + } + }, + "scope": { + "oneOf": [ + { "$ref": "#/$defs/predicate" }, + { + "type": "object", + "required": ["all"], + "additionalProperties": false, + "properties": { + "all": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/predicate" } + } + } + }, + { + "type": "object", + "required": ["any"], + "additionalProperties": false, + "properties": { + "any": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/predicate" } + } + } + } + ] + }, + "tablePolicy": { + "type": "object", + "additionalProperties": false, + "properties": { + "accessible": { "type": "boolean" }, + "columns": { + "type": "object", + "additionalProperties": false, + "properties": { + "visible": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]*$" + } + }, + "writable": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]*$" + } + } + } + }, + "rows": { + "type": "object", + "additionalProperties": false, + "properties": { + "mode": { + "type": "string", + "enum": ["scope", "open", "delegate"] + }, + "scope": { "$ref": "#/$defs/scope" } + } + } + } + } + } +} diff --git a/development/POLICY_MATRIX.md b/development/POLICY_MATRIX.md index ab20a10..3711b15 100644 --- a/development/POLICY_MATRIX.md +++ b/development/POLICY_MATRIX.md @@ -67,3 +67,6 @@ | `SKILL.md` / `constants.SKILL_SLUG` 一致性 | development/DEVELOPMENT.md §16 | 已有 `tests/test_skill_metadata.py` | | platform_kit_min_version >= 1.2.2 | development/RUNTIME.md | 已有 `tests/test_platform_import.py` | | 宿主按 `bind.tables` 过滤 toolbar 按钮 | references/ACTIONS.md | 待宿主升级后完整生效;模板侧已强制显式绑定 | +| Data Policy 未启用(L0)时宿主不得拉 Snapshot / 不得改按钮与行列行为 | references/POLICY.md | soft;宿主实现契约 | +| Data Policy 启用后 fail-closed(禁止回退 L0 全开);写路径 domain 必须再鉴权 | references/POLICY.md §6–§7 | soft;`POLICY-SKILL-AUTHZ-*` 硬门禁可后续加 | +| `actions.mode` 仅 allowlist/open;`hostCapabilities` 省略=不限制;scope `valueFrom` 仅 `actor.userId` | references/POLICY.md §0.1 | soft;Schema 已约束 | diff --git a/development/SKILL_ACTION_RUNTIME.md b/development/SKILL_ACTION_RUNTIME.md index 8e84412..aa85f6d 100644 --- a/development/SKILL_ACTION_RUNTIME.md +++ b/development/SKILL_ACTION_RUNTIME.md @@ -19,7 +19,7 @@ 1. **宿主管调度,技能管业务** — 不自行实现任务中心 / Job 轮询 / 后台守护进程。 2. **单业务内核、多入口** — 数据管理、定时任务、Agent、技能详情与直接 CLI 复用同一 domain service;禁止按入口复制业务逻辑。 -3. **五职责正交** — `placements` / `bind.tables` / `executionProfile` / `entrypoint` / domain service 各管一事(见 ACTIONS.md)。 +3. **五职责正交** — `placements` / `bind.tables` / `executionProfile` / `entrypoint` / domain service 各管一事(见 ACTIONS.md)。可选第六轴 **policy**(见 `references/POLICY.md`):未启用 = L0 与今日一致。 4. **长耗时建议 async** — 开浏览器、RPA、HTTP 大文件下载等建议 `executionProfile: "async"`,进**任务中心**;这是对**方法**的选择,**不**限制可以出现在哪些 placement / 入口。 5. **Agent:短 CLI / 长 Action** — 短查询可用共享 Python bash CLI;长任务 / RPA **禁止** `exec` + `process poll`,必须 `run_skill_action`。 6. **进度走 Journal** — `emit` / `video.add_step` → Run Journal;UI / Skill Run Card / 任务中心展示;**不靠 stdout 流**。 diff --git a/references/ACTIONS.md b/references/ACTIONS.md index b10035c..93aeec7 100644 --- a/references/ACTIONS.md +++ b/references/ACTIONS.md @@ -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"`(**必须显式写出**,禁止依赖缺省) | diff --git a/references/CLI.md b/references/CLI.md index 5e8f09d..4c9e2b5 100644 --- a/references/CLI.md +++ b/references/CLI.md @@ -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` diff --git a/references/POLICY.md b/references/POLICY.md new file mode 100644 index 0000000..f4dbb2e --- /dev/null +++ b/references/POLICY.md @@ -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;写路径技能自检 diff --git a/references/README.md b/references/README.md index c262198..b722cd5 100644 --- a/references/README.md +++ b/references/README.md @@ -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`。 diff --git a/scripts/util/constants.py b/scripts/util/constants.py index 139c8dd..2716c9e 100644 --- a/scripts/util/constants.py +++ b/scripts/util/constants.py @@ -1,6 +1,6 @@ """技能标识、版本与平台公共库约束(复制后请修改 slug/version/logger)。""" SKILL_SLUG = "your-skill-slug" -SKILL_VERSION = "1.0.72" +SKILL_VERSION = "1.0.73" LOG_LOGGER_NAME = "openclaw.skill.your_skill_slug" PLATFORM_KIT_MIN_VERSION = "1.2.2" diff --git a/tests/test_actions_manifest.py b/tests/test_actions_manifest.py index 1a1e552..f479200 100644 --- a/tests/test_actions_manifest.py +++ b/tests/test_actions_manifest.py @@ -516,6 +516,8 @@ class TestActionsManifest(unittest.TestCase): "左右分栏", "行级入口", "行右键", + "六职责正交", + "POLICY.md", ): self.assertIn(marker, text, msg=f"ACTIONS.md missing {marker!r}") self.assertNotIn( diff --git a/tests/test_docs_standards.py b/tests/test_docs_standards.py index f16ff07..2773c97 100644 --- a/tests/test_docs_standards.py +++ b/tests/test_docs_standards.py @@ -299,9 +299,15 @@ class TestDocsStandards(unittest.TestCase): root = get_skill_root() self.assertTrue(os.path.isfile(os.path.join(root, "assets", "actions.json"))) self.assertTrue(os.path.isfile(os.path.join(root, "references", "ACTIONS.md"))) + self.assertTrue(os.path.isfile(os.path.join(root, "references", "POLICY.md"))) self.assertTrue( os.path.isfile(os.path.join(root, "assets", "schemas", "skill-actions.schema.json")) ) + self.assertTrue( + os.path.isfile( + os.path.join(root, "assets", "schemas", "skill-policy-snapshot.schema.json") + ) + ) self.assertTrue(os.path.isfile(os.path.join(root, "tests", "test_actions_manifest.py"))) self.assertTrue(os.path.isfile(os.path.join(root, "tests", "test_data_management_contract.py")))