"""Skill 表权限沙箱(ARCH §5.6 / PRODUCT_SPEC §5.5)——纯函数,无 IO。 这**不是进程级沙箱**:声明式 Skill 不执行代码。强制点在编排/写库层("声明式权限 + apply 层白名单"): - `filter_reads`:注入时只把 skill 声明 `reads` 的表数据喂给它(未声明的表丢弃,不喂)。 - `partition_writes`:写库时只应用 skill 声明 `writes` 的产出(越权字段丢弃 + 审计), 返回 `(allowed, rejected)`——调用方落 `allowed`、log `rejected`。不变量 #3: allowed 仍须经验收 gate 才真入库,本层只裁剪白名单,不开写后门。 - `validate_declaration`:注册/加载 skill 时校验 `reads/writes` 全在已知创作表白名单内 (杜绝声明指向不存在/系统表);越界 → AppError(VALIDATION)。 不可变:所有函数返回新 dict/list,从不原地改入参(全局 immutability 约定)。 """ from __future__ import annotations from typing import Any from ww_agents import AgentSpec from ww_shared import AppError, ErrorCode # 声明式 Skill 允许声明读/写的创作表白名单(ARCH §3.1 创作表 + rules; # 系统/运营表 users/jobs/usage_ledger/provider_credentials/tier_routing/skills 不在此列)。 KNOWN_TABLES: frozenset[str] = frozenset( { "projects", "characters", "world_entities", "outline", "chapter_digests", "foreshadow", "style_fingerprint", "rules", "chapters", "chapter_reviews", } ) def filter_reads(spec: AgentSpec, available: dict[str, Any]) -> dict[str, Any]: """注入白名单:只保留 skill 声明 `reads` 且 `available` 里确有的表(新 dict)。""" declared = set(spec.reads) return {table: data for table, data in available.items() if table in declared} def partition_writes(spec: AgentSpec, produced: dict[str, Any]) -> tuple[dict[str, Any], list[str]]: """写库白名单:把产出分成 (allowed, rejected)。 `allowed`:产出表 ∈ skill 声明 `writes`(新 dict,调用方落库)。 `rejected`:越权产出表名(排序,调用方审计 log + 丢弃)。 """ declared = set(spec.writes) allowed: dict[str, Any] = {} rejected: list[str] = [] for table, data in produced.items(): if table in declared: allowed[table] = data else: rejected.append(table) return allowed, sorted(rejected) def validate_declaration(spec: AgentSpec) -> None: """校验 skill 的 `reads/writes` 全在 `KNOWN_TABLES`;越界 → AppError(VALIDATION)。""" unknown = sorted((set(spec.reads) | set(spec.writes)) - KNOWN_TABLES) if unknown: raise AppError( ErrorCode.VALIDATION, f"Skill「{spec.name}」声明了未知表:{unknown}", {"skill": spec.name, "unknown_tables": unknown}, )