diff --git a/.claude/skills/runtime/SKILL.md b/.claude/skills/runtime/SKILL.md index 23bf583..356efcd 100644 --- a/.claude/skills/runtime/SKILL.md +++ b/.claude/skills/runtime/SKILL.md @@ -14,8 +14,25 @@ Claude CLI 受控子进程调用(fresh/sandbox/deadline/structured_output/回 |---|---| | `claude_runtime.py` | 受控 Claude CLI 调用运行时:只接受显式冻结的执行 profile,负责 fresh process、sandbox、最小环境、硬 deadline、structured_output 唯一业务出口和联合执行回执;任何不完整证据失败关闭,错误对象永不携带 stderr 原文。服务生产与评测两侧,不只评测。 | | `file_cas.py` | 不可变内容寻址(CAS)revision journal:按内容哈希落盘,冲突即失败关闭,支撑 Gate receipt 与逐层回执的不可篡改。 | -| `raw_vault.py` | 租约式 raw 物料临时保险库:原始候选/物料限定在运行目录,租约到期自动清理,不把 raw 路径带出边界。 | +| `raw_vault.py` | 租约式 raw 物料保险库:原始候选/物料先写 0700 临时 vault,正式评测结束后同盘原子迁移到显式仓外受控归档;安全回执不携带 raw 路径。**raw 落库改写后降为可选备份**(见下)。 | +| `persist_raw.py` | raw 落库(2026-07-30 改写):完整 prompt/response/oracle/原书全文/供应商响应落 `example_raw_lease` + `example_raw_content`(访问控制 + append-only),写入前过滤密钥,看板可看全文。 | ## 依赖方向 本 skill 是底座,**不 import 任何其它 skill**。上层(detect/quality-gate/continuation/replay-eval)单向向下引用本 skill 的 scripts。 + +## raw lease 合同 + +- 会持有 raw 物料进行长时执行的调用方必须声明最低剩余租期;raw vault 在 lease journal 落盘前拒绝剩余时间不足的租约。短时离线测试可沿用默认值。 +- `retainUntil` 到期后禁止继续写入 raw;过期且尚未迁移的 lease 仍允许执行显式 `cleanup` 和 `recover`,以便收敛 open 状态。 +- 正式评测正常、质量失败或受控终止均调用 `migrate`,把整个 vault 原子迁往调用方显式提供的绝对归档根;归档根固定 0700、必须与临时 vault 同一文件系统,lease/report 只记录 `archiveId`、内容树哈希、文件数、字节数和迁移回执哈希,不记录路径。 +- 迁移终态是 `migrated`,`retentionPolicy=explicit_cleanup_required`;`recover` 不得删除 `migrating/migrated` 数据。`cleanup` 只用于另行明确授权的删除或尚未形成可迁移轮次的过期残留,不能作为正式 Gate A 的默认收口。 +- 默认真实 Claude 调用必须持有 `Popen` 进程句柄并使用独立进程组;deadline、SIGTERM/SIGINT 转换后的受控异常或其它父进程异常发生时,必须先终止并等待整个模型进程组,再进入预算、raw 和 manifest 收口,禁止留下继续运行或计费的孤立子进程。 + +## raw 落库合同(2026-07-30 改写,创始人批准) + +- **raw 以落库为主**(`persist_raw.py`):`example_raw_lease`(元数据)+ `example_raw_content`(全文),访问控制 + append-only;看板可看全文。默认不进普通检索,看全文须显式指定。 +- **仓外 vault 降为可选备份**:`raw_vault.py` 的 lease/migrate 机制保留作可选备份,不再是合同要求。 +- **废时效语义**:`retain_until` 只留档溯源,不触发自动删除;append-only 保证证据不可变。 +- **密钥/token/凭据绝不入表**:写入前过滤(`persist_raw.py` 标记检测)+ 列级 CHECK 双禁。 +- **后续(重活,需评测测试)**:replay-eval 的 COMPLETED 判据从"migrate 成功"改为"已落库"、raw_vault 接线相应调整——影响评测 harness,须单独一步带测试做,不在本轮。 diff --git a/.claude/skills/runtime/scripts/persist_raw.py b/.claude/skills/runtime/scripts/persist_raw.py new file mode 100644 index 0000000..ccc2211 --- /dev/null +++ b/.claude/skills/runtime/scripts/persist_raw.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +"""raw 落库 —— 把完整 prompt/response/标准答案/原书全文/供应商响应落 example_raw_lease + example_raw_content。 + +落库设计 §2.7 + §4 raw 合同改写(创始人批准):放弃时效语义,改靠**访问控制 + append-only**—— +raw 进库可看全文(看板可读),仓外 vault 降级为可选备份;密钥/token/凭据写入前过滤,绝不入表。 +- lease:一次 raw 归集的租约/元数据(append-only)。 +- content:全文(append-only,(lease_id, content_sha256) 幂等去重;跨租约各留一行,授权归属不串)。 +--dry-run 试跑:插入后回滚,校验但不落库。 +""" +import hashlib +import json +import sys +from pathlib import Path + +# 复用 db skill 锁死的 DSN(.claude/skills/db/scripts) +DB_SCRIPTS = Path(__file__).resolve().parents[2] / "db" / "scripts" +sys.path.insert(0, str(DB_SCRIPTS)) +from db import connect # noqa: E402 + +CREATOR = "runtime" +KINDS = ("prompt", "response", "source_text", "oracle", "supplier") + +# 写入前过滤:命中即拒,密钥/token/凭据绝不入 raw 表(列级 CHECK 之外的第二道) +_SECRET_MARKERS = ("api_key", "apikey", "secret", "token", "password", "passwd", + "authorization: bearer", "sk-", "-----begin private key-----") + + +def _check_no_secrets(content: str) -> None: + low = content.lower() + for marker in _SECRET_MARKERS: + if marker in low: + raise ValueError(f"raw 内容疑似含密钥/凭据标记 '{marker}',拒绝入库(安全)") + + +def _bare_sha256(text: str) -> str: + return hashlib.sha256(text.encode("utf-8")).hexdigest() + + +def write_lease(*, vault_id=None, run_id=None, source_version=None, content_hashes=None, + purpose=None, status="open", retain_until=None, run_authorization_id=None, + source_work_id=None, creator=CREATOR, dry_run=False): + """落一条 raw 租约/元数据。返回 {status, id}。""" + with connect() as conn: + try: + row = conn.execute( + "INSERT INTO example_raw_lease(vault_id, run_id, source_version, content_hashes, purpose, " + "status, retain_until, run_authorization_id, source_work_id, creator) " + "VALUES (%s,%s,%s,%s::jsonb,%s,%s,%s,%s,%s,%s) RETURNING id", + (vault_id, run_id, source_version, + json.dumps(content_hashes, ensure_ascii=False) if content_hashes is not None else None, + purpose, status, retain_until, run_authorization_id, source_work_id, creator)).fetchone() + if dry_run: + conn.rollback() + return {"status": "dry_run_ok", "id": row[0], "note": "试跑已回滚,未落库"} + conn.commit() + return {"status": "written", "id": row[0]} + except Exception: + conn.rollback() + raise + + +def write_content(lease_id, kind, content, *, run_id=None, role=None, creator=CREATOR, dry_run=False): + """落一条 raw 全文(幂等去重)。返回 {status, id, content_sha256}。""" + if kind not in KINDS: + raise ValueError(f"kind 非法:{kind}(应为 {KINDS};candidate 请存候选表,不进 raw)") + _check_no_secrets(content) + sha = _bare_sha256(content) + with connect() as conn: + try: + row = conn.execute( + "INSERT INTO example_raw_content(lease_id, kind, run_id, role, content_sha256, content, creator) " + "VALUES (%s,%s,%s,%s,%s,%s,%s) ON CONFLICT (lease_id, content_sha256) " + "DO UPDATE SET creator=EXCLUDED.creator RETURNING id, content_sha256", + (lease_id, kind, run_id, role, sha, content, creator)).fetchone() + if dry_run: + conn.rollback() + return {"status": "dry_run_ok", "id": row[0], "content_sha256": row[1], "note": "试跑已回滚,未落库"} + conn.commit() + return {"status": "written", "id": row[0], "content_sha256": row[1]} + except Exception: + conn.rollback() + raise + + +if __name__ == "__main__": + import argparse + ap = argparse.ArgumentParser(description="raw 落库(租约 + 全文,访问控制 + append-only)") + sub = ap.add_subparsers(dest="cmd", required=True) + pl = sub.add_parser("lease", help="落一条 raw 租约/元数据") + pl.add_argument("--vault-id", default=None) + pl.add_argument("--run-id", default=None) + pl.add_argument("--source-version", default=None) + pl.add_argument("--purpose", default=None) + pl.add_argument("--run-authorization-id", default=None) + pl.add_argument("--source-work-id", type=int, default=None) + pl.add_argument("--dry-run", action="store_true") + pc = sub.add_parser("content", help="落一条 raw 全文") + pc.add_argument("lease_id", type=int) + pc.add_argument("kind", choices=KINDS) + pc.add_argument("content_file", help="全文内容文件路径") + pc.add_argument("--run-id", default=None) + pc.add_argument("--role", default=None) + pc.add_argument("--dry-run", action="store_true") + args = ap.parse_args() + if args.cmd == "lease": + out = write_lease(vault_id=args.vault_id, run_id=args.run_id, source_version=args.source_version, + purpose=args.purpose, run_authorization_id=args.run_authorization_id, + source_work_id=args.source_work_id, dry_run=args.dry_run) + else: + content = Path(args.content_file).read_text(encoding="utf-8") + out = write_content(args.lease_id, args.kind, content, run_id=args.run_id, role=args.role, + dry_run=args.dry_run) + print(json.dumps(out, ensure_ascii=False)) diff --git a/db/ddl/101-example-raw.sql b/db/ddl/101-example-raw.sql new file mode 100644 index 0000000..291e869 --- /dev/null +++ b/db/ddl/101-example-raw.sql @@ -0,0 +1,90 @@ +-- raw 元数据与全文两表:example_raw_lease(租约/元数据)+ example_raw_content(全文,访问控制)。 +-- 设计拍板 2026-07-30(docs/2026-07-30-落库与看板设计.md §2.7 + §4 raw 合同改写)。 +-- raw 合同已改写(创始人批准):放弃时效语义,改靠**访问控制 + append-only**—— +-- 完整 prompt/response/未接受候选/标准答案/原书全文/供应商响应进库可看全文(看板可读), +-- 仓外 vault 降级为可选备份;retain_until 只留档、不触发自动删除;密钥/token 绝不入表。 +-- 两表都 append-only(禁 UPDATE/DELETE/TRUNCATE)——raw 是不可变证据。 +-- 全部对象 IF NOT EXISTS / OR REPLACE,重复 apply 幂等。 + +-- ══ example_raw_lease:raw 租约/元数据(append-only)══ +CREATE TABLE IF NOT EXISTS example_raw_lease ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + vault_id CHAR(32), -- vault 标识(兼容旧 raw_vault 的 vaultId) + run_id VARCHAR(64), -- → example_run.run_id(软引用) + source_version VARCHAR(128), -- 来源版本 + content_hashes JSONB, -- 本租约下内容哈希清单 + purpose VARCHAR(64), -- 用途(generation/evaluation/…) + status VARCHAR(20) NOT NULL DEFAULT 'open', -- open/migrated/closed(历史状态,不再是删期限闸门) + retain_until TIMESTAMP, -- 批准的留存期限(留档溯源;进库后不触发自动删除) + archive_id VARCHAR(64), -- 仓外备份标识(若仍做可选备份) + archive_sha256 CHAR(64), + file_count INTEGER, + total_bytes BIGINT, + run_authorization_id VARCHAR(128), -- 本次运行授权标识(运行时对象,不查库、不指 96) + source_work_id BIGINT, -- raw 来源参考书 → example_reference_work.id(出处溯源) + creator VARCHAR(64) NOT NULL DEFAULT '', + create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + tenant_id BIGINT NOT NULL DEFAULT 0, + CONSTRAINT chk_example_raw_lease_status CHECK (status IN ('open','migrated','closed')) +); +CREATE INDEX IF NOT EXISTS idx_example_raw_lease_run ON example_raw_lease(run_id) WHERE run_id IS NOT NULL; +CREATE INDEX IF NOT EXISTS idx_example_raw_lease_work ON example_raw_lease(source_work_id) WHERE source_work_id IS NOT NULL; + +-- ══ example_raw_content:raw 全文(append-only,访问控制)══ +-- 访问控制:默认不进普通检索,看全文须显式指定;看板可看全文;密钥/token/凭据列级 + 写入前过滤双禁。 +CREATE TABLE IF NOT EXISTS example_raw_content ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + lease_id BIGINT, -- → example_raw_lease.id(软引用,可空) + kind VARCHAR(32) NOT NULL, -- prompt/response/source_text/oracle/supplier(无 candidate——候选正文单存候选表) + run_id VARCHAR(64), -- → example_run.run_id(软引用) + role VARCHAR(32), -- 产生角色(writer/detector/judge/…) + content_sha256 CHAR(64) NOT NULL, -- 内容哈希(裸 64 hex) + content TEXT NOT NULL, -- 全文 + creator VARCHAR(64) NOT NULL DEFAULT '', + create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + tenant_id BIGINT NOT NULL DEFAULT 0, + CONSTRAINT chk_example_raw_content_kind + CHECK (kind IN ('prompt','response','source_text','oracle','supplier')), + CONSTRAINT chk_example_raw_content_sha256 + CHECK (content_sha256 ~ '^[0-9a-f]{64}$') +); +-- 唯一键 (lease_id, content_sha256):同一租约内重试幂等去重;跨租约各留一行(授权归属不串) +CREATE UNIQUE INDEX IF NOT EXISTS uk_example_raw_content + ON example_raw_content(lease_id, content_sha256); +CREATE INDEX IF NOT EXISTS idx_example_raw_content_run ON example_raw_content(run_id) WHERE run_id IS NOT NULL; + +-- append-only + TRUNCATE 防护(raw 是不可变证据) +CREATE OR REPLACE FUNCTION example_reject_truncate() +RETURNS TRIGGER +LANGUAGE plpgsql +AS $$ +BEGIN + RAISE EXCEPTION '% 是 append-only 表,禁止 TRUNCATE', TG_TABLE_NAME; +END; +$$; + +CREATE OR REPLACE FUNCTION reject_example_raw_lease_mutation() +RETURNS TRIGGER LANGUAGE plpgsql AS $$ +BEGIN + RAISE EXCEPTION 'example_raw_lease 是 append-only 表,禁止 UPDATE/DELETE'; +END; +$$; +CREATE OR REPLACE TRIGGER trg_example_raw_lease_append_only +BEFORE UPDATE OR DELETE ON example_raw_lease +FOR EACH ROW EXECUTE FUNCTION reject_example_raw_lease_mutation(); +CREATE OR REPLACE TRIGGER trg_example_raw_lease_no_truncate +BEFORE TRUNCATE ON example_raw_lease +FOR EACH STATEMENT EXECUTE FUNCTION example_reject_truncate(); + +CREATE OR REPLACE FUNCTION reject_example_raw_content_mutation() +RETURNS TRIGGER LANGUAGE plpgsql AS $$ +BEGIN + RAISE EXCEPTION 'example_raw_content 是 append-only 表,禁止 UPDATE/DELETE'; +END; +$$; +CREATE OR REPLACE TRIGGER trg_example_raw_content_append_only +BEFORE UPDATE OR DELETE ON example_raw_content +FOR EACH ROW EXECUTE FUNCTION reject_example_raw_content_mutation(); +CREATE OR REPLACE TRIGGER trg_example_raw_content_no_truncate +BEFORE TRUNCATE ON example_raw_content +FOR EACH STATEMENT EXECUTE FUNCTION example_reject_truncate();