将角色与 Skill 从 .claude 迁入 .agent,移除 Claude CLI 运行时并接入固定 Opus 角色 profile、完整 schema、预算 deadline、raw 与回执证据链。 同步拆分 Skill 职责、复利 lesson、Gate 回放、Dashboard 人审入口、数据库登记和机械门禁;候选设计正文不包含在本提交中。
442 lines
16 KiB
Python
442 lines
16 KiB
Python
#!/usr/bin/env python3
|
||
"""用当前 writer prompt 合同刷新 provider-neutral 角色运行探针。
|
||
|
||
探针只使用固定合成输入,通过 muse_role.run_role 执行一次受治理角色调用,验证
|
||
profile、prompt、schema、输入、结构化输出、模型策略、成本和回执绑定。成功结果写到
|
||
新文件;任何失败都不修改原配置,也不输出 prompt、response 或 raw 路径。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import copy
|
||
import json
|
||
import sys
|
||
from dataclasses import dataclass
|
||
from datetime import datetime, timezone
|
||
from decimal import Decimal, InvalidOperation
|
||
from pathlib import Path
|
||
from typing import Any, Mapping, Protocol
|
||
|
||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||
SKILLS_DIR = SCRIPT_DIR.parents[1]
|
||
GATE_ADJUDICATION_DIR = SKILLS_DIR / "adjudicate-quality-gate" / "scripts"
|
||
WRITER_REPLAY_DIR = SKILLS_DIR / "replay-writer-gate" / "scripts"
|
||
EVIDENCE_DIR = SKILLS_DIR / "record-run-evidence" / "scripts"
|
||
for _import_dir in (GATE_ADJUDICATION_DIR, WRITER_REPLAY_DIR, EVIDENCE_DIR):
|
||
if str(_import_dir) not in sys.path:
|
||
sys.path.insert(0, str(_import_dir))
|
||
|
||
from muse_role import ( # noqa: E402
|
||
HASH_PATTERN,
|
||
RUNTIME_ADAPTER,
|
||
RUNTIME_ADAPTER_VERSION,
|
||
RoleExecutionProfile,
|
||
RoleExecutionReceipt,
|
||
RoleInvocationResult,
|
||
RoleRuntimeError,
|
||
model_matches_profile,
|
||
run_role,
|
||
sha256_json,
|
||
)
|
||
from gate_input_builder import canonical_sha256 # noqa: E402
|
||
from persist_llm_call import persist_call as persist_llm_event # noqa: E402
|
||
from run_writer_replay import profile_from_mapping # noqa: E402
|
||
|
||
|
||
PROBE_BUSINESS_INPUT: dict[str, Any] = {
|
||
"fineOutline": {
|
||
"hardConstraints": ["本段为运行探针合成任务,不引用任何真实作品或真实正文。"],
|
||
"adjustableBeats": ["写一个守塔人在黎明前点亮灯塔的极短瞬间。"],
|
||
"declaredNewFacts": [],
|
||
},
|
||
"narrativeState": {
|
||
"asOfChapter": 0,
|
||
"summary": "合成探针场景:一座孤岛灯塔,黎明前。无任何真实作品人物或情节。",
|
||
},
|
||
"factConstraints": [],
|
||
"proseExcerpts": [],
|
||
"patternReferences": [],
|
||
"lengthContract": {
|
||
"targetChars": 60,
|
||
"minChars": 20,
|
||
"maxChars": 120,
|
||
"frontmatterRequired": False,
|
||
},
|
||
"styleConstraints": [],
|
||
}
|
||
DRY_RUN_CANDIDATE_BODY = (
|
||
"黎明前最暗的一刻,守塔人划亮火柴,灯芯燃起一小团光,海面被照出一线金边。"
|
||
)
|
||
PROBE_SCHEMA_VERSION = "runtime-probe-v2"
|
||
|
||
|
||
class ProbeRefreshError(RuntimeError):
|
||
"""携带稳定错误码的失败关闭错误,不附带模型原文。"""
|
||
|
||
def __init__(self, code: str, message: str) -> None:
|
||
super().__init__(f"{code}: {message}")
|
||
self.code = code
|
||
self.message = message
|
||
|
||
|
||
class ProbeInvoker(Protocol):
|
||
"""角色运行边界;真实实现是 run_role,测试可注入纯内存假实现。"""
|
||
|
||
def __call__(
|
||
self,
|
||
profile: RoleExecutionProfile,
|
||
business_input: Mapping[str, Any],
|
||
) -> RoleInvocationResult: ...
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class RefreshResult:
|
||
refreshed_config: dict[str, Any]
|
||
probe: dict[str, Any]
|
||
summary: dict[str, Any]
|
||
|
||
|
||
def build_writer_profile(config: Mapping[str, Any]) -> RoleExecutionProfile:
|
||
"""复用正式执行门的构造器重建 writer 冻结 profile。"""
|
||
|
||
profiles = config.get("executionProfiles")
|
||
if not isinstance(profiles, Mapping):
|
||
raise ProbeRefreshError("PROBE_CONFIG_INVALID", "配置缺少 executionProfiles")
|
||
return profile_from_mapping(profiles.get("writer"), role="writer")
|
||
|
||
|
||
def _validate_writer_output(value: Any) -> None:
|
||
"""独立复核 writer-draft-v2 的最小闭集结构。"""
|
||
|
||
if (
|
||
not isinstance(value, Mapping)
|
||
or set(value) != {"candidateBody"}
|
||
or not isinstance(value.get("candidateBody"), str)
|
||
or not value["candidateBody"]
|
||
):
|
||
raise ProbeRefreshError("PROBE_SCHEMA_INVALID", "结构化输出不符合 writer schema")
|
||
|
||
|
||
def verify_probe_result(
|
||
profile: RoleExecutionProfile,
|
||
result: RoleInvocationResult,
|
||
) -> None:
|
||
"""纵深复核角色调用结果,任一绑定不成立即失败关闭。"""
|
||
|
||
_validate_writer_output(result.structured_output)
|
||
receipt = result.receipt
|
||
if receipt.execution_profile_sha256 != profile.execution_profile_sha256:
|
||
raise ProbeRefreshError(
|
||
"PROBE_PROFILE_BINDING_MISMATCH",
|
||
"回执身份哈希与当前 writer profile 不一致",
|
||
)
|
||
if receipt.json_schema_sha256 != profile.json_schema_sha256:
|
||
raise ProbeRefreshError("PROBE_SCHEMA_BINDING_MISMATCH", "回执 schema 哈希不一致")
|
||
if receipt.input_sha256 != sha256_json(PROBE_BUSINESS_INPUT):
|
||
raise ProbeRefreshError("PROBE_INPUT_HASH_INVALID", "回执未绑定固定探针输入")
|
||
output_hash = receipt.structured_output_sha256
|
||
if (
|
||
not isinstance(output_hash, str)
|
||
or not HASH_PATTERN.fullmatch(output_hash)
|
||
or output_hash != sha256_json(result.structured_output)
|
||
):
|
||
raise ProbeRefreshError(
|
||
"PROBE_OUTPUT_HASH_INVALID",
|
||
"结构化输出哈希缺失、非法或与产出不一致",
|
||
)
|
||
if (
|
||
receipt.requested_model_id != profile.model_alias
|
||
or receipt.model_match is not True
|
||
or not model_matches_profile(profile, receipt.actual_model_id)
|
||
):
|
||
raise ProbeRefreshError("PROBE_MODEL_MISMATCH", "实际模型不属于冻结治理策略")
|
||
if receipt.is_error is not False:
|
||
raise ProbeRefreshError("PROBE_RECEIPT_ERROR", "回执标记为错误")
|
||
if receipt.exit_code is not None:
|
||
raise ProbeRefreshError("PROBE_TRANSPORT_INVALID", "角色运行回执不得携带进程退出码")
|
||
if receipt.terminal_reason != "completed":
|
||
raise ProbeRefreshError("PROBE_TERMINAL_REASON_INVALID", "角色调用未正常完成")
|
||
if receipt.api_error_status is not None:
|
||
raise ProbeRefreshError("PROBE_API_ERROR", "回执携带 API 错误状态")
|
||
if receipt.total_cost_usd is None:
|
||
raise ProbeRefreshError("PROBE_BUDGET_EXCEEDED", "回执缺少可信成本")
|
||
try:
|
||
cost = Decimal(receipt.total_cost_usd)
|
||
except (InvalidOperation, ValueError) as exc:
|
||
raise ProbeRefreshError("PROBE_BUDGET_EXCEEDED", "回执成本不可解析") from exc
|
||
if not cost.is_finite() or cost < 0 or cost > profile.max_budget_usd_per_call:
|
||
raise ProbeRefreshError("PROBE_BUDGET_EXCEEDED", "单次成本超过冻结预算 cap")
|
||
|
||
|
||
def build_probe_record(
|
||
profile: RoleExecutionProfile,
|
||
result: RoleInvocationResult,
|
||
*,
|
||
checked_at: str,
|
||
) -> dict[str, Any]:
|
||
"""构建 runtime-probe-v2,并使用 Gate 同源算法计算自哈希。"""
|
||
|
||
receipt = result.receipt
|
||
record: dict[str, Any] = {
|
||
"schemaVersion": PROBE_SCHEMA_VERSION,
|
||
"status": "successful",
|
||
"checkedAt": checked_at,
|
||
"runtimeAdapter": RUNTIME_ADAPTER,
|
||
"runtimeAdapterVersion": RUNTIME_ADAPTER_VERSION,
|
||
"modelPolicyVersion": profile.model_policy_version,
|
||
"role": profile.adapter_role,
|
||
"profileVersion": profile.profile_version,
|
||
"modelAlias": profile.model_alias,
|
||
"resolvedModelId": profile.resolved_model_id,
|
||
"executionProfileSha256": receipt.execution_profile_sha256,
|
||
"jsonSchemaId": profile.json_schema_id,
|
||
"jsonSchemaSha256": profile.json_schema_sha256,
|
||
"systemPromptId": profile.system_prompt_id,
|
||
"systemPromptSha256": profile.system_prompt_sha256,
|
||
"inputSha256": receipt.input_sha256,
|
||
"requestedModelId": receipt.requested_model_id,
|
||
"actualModelId": receipt.actual_model_id,
|
||
"modelMatch": receipt.model_match,
|
||
"executionReceiptSha256": canonical_sha256(receipt.as_dict()),
|
||
"structuredOutputSha256": receipt.structured_output_sha256,
|
||
"terminalReason": receipt.terminal_reason,
|
||
"totalCostUsd": receipt.total_cost_usd,
|
||
}
|
||
record["receiptSha256"] = canonical_sha256(record)
|
||
return record
|
||
|
||
|
||
def apply_probe(config: Mapping[str, Any], probe: Mapping[str, Any]) -> dict[str, Any]:
|
||
"""只替换深拷贝配置的 runtimeProbe;其它授权记录保持不变。"""
|
||
|
||
authorization = config.get("executionAuthorization")
|
||
if not isinstance(authorization, Mapping):
|
||
raise ProbeRefreshError("PROBE_CONFIG_INVALID", "配置缺少 executionAuthorization")
|
||
refreshed = copy.deepcopy(dict(config))
|
||
refreshed["executionAuthorization"] = copy.deepcopy(dict(authorization))
|
||
refreshed["executionAuthorization"]["runtimeProbe"] = copy.deepcopy(dict(probe))
|
||
return refreshed
|
||
|
||
|
||
def _audit_summary(
|
||
profile: RoleExecutionProfile,
|
||
probe: Mapping[str, Any],
|
||
*,
|
||
dry_run: bool,
|
||
) -> dict[str, Any]:
|
||
"""生成不含 prompt、response 和 raw 的审计摘要。"""
|
||
|
||
return {
|
||
"tool": "refresh_runtime_probe",
|
||
"dryRun": dry_run,
|
||
"status": probe.get("status"),
|
||
"runtimeAdapterVersion": RUNTIME_ADAPTER_VERSION,
|
||
"modelPolicyVersion": profile.model_policy_version,
|
||
"executionProfileSha256": probe.get("executionProfileSha256"),
|
||
"structuredOutputSha256": probe.get("structuredOutputSha256"),
|
||
"executionReceiptSha256": probe.get("executionReceiptSha256"),
|
||
"probeReceiptSha256": probe.get("receiptSha256"),
|
||
"totalCostUsd": probe.get("totalCostUsd"),
|
||
"modelAlias": profile.model_alias,
|
||
"actualModelId": probe.get("actualModelId"),
|
||
"checkedAt": probe.get("checkedAt"),
|
||
}
|
||
|
||
|
||
def refresh_runtime_probe(
|
||
config: Mapping[str, Any],
|
||
*,
|
||
invoker: ProbeInvoker,
|
||
checked_at: str,
|
||
dry_run: bool = False,
|
||
) -> RefreshResult:
|
||
"""重建 profile、调用角色、验证结果、重签探针并返回新配置。"""
|
||
|
||
profile = build_writer_profile(config)
|
||
result = invoker(profile, PROBE_BUSINESS_INPUT)
|
||
verify_probe_result(profile, result)
|
||
probe = build_probe_record(profile, result, checked_at=checked_at)
|
||
if dry_run:
|
||
# dry-run 只证明本地构建/重签链可运行,不能冒充真实模型能力证明。
|
||
probe["status"] = "dry_run"
|
||
probe["receiptSha256"] = canonical_sha256(
|
||
{key: value for key, value in probe.items() if key != "receiptSha256"}
|
||
)
|
||
refreshed = apply_probe(config, probe)
|
||
return RefreshResult(
|
||
refreshed_config=refreshed,
|
||
probe=probe,
|
||
summary=_audit_summary(profile, probe, dry_run=dry_run),
|
||
)
|
||
|
||
|
||
def make_role_invoker(*, run_id: str) -> ProbeInvoker:
|
||
"""构造真实角色 invoker,并把探针调用纳入统一运行证据。"""
|
||
|
||
def _invoke(
|
||
profile: RoleExecutionProfile,
|
||
business_input: Mapping[str, Any],
|
||
) -> RoleInvocationResult:
|
||
return run_role(
|
||
profile,
|
||
business_input,
|
||
run_id=run_id,
|
||
caller="refresh-runtime-probe",
|
||
persist_call=persist_llm_event,
|
||
)
|
||
|
||
return _invoke
|
||
|
||
|
||
def make_dry_run_invoker() -> ProbeInvoker:
|
||
"""构造不访问模型的固定 invoker,仅用于验证配置与重签链。"""
|
||
|
||
def _dry_run_invoker(
|
||
profile: RoleExecutionProfile,
|
||
business_input: Mapping[str, Any],
|
||
) -> RoleInvocationResult:
|
||
structured_output = {"candidateBody": DRY_RUN_CANDIDATE_BODY}
|
||
receipt = RoleExecutionReceipt(
|
||
adapter_role=profile.adapter_role,
|
||
invocation_id="dry-run-probe",
|
||
execution_profile_sha256=profile.execution_profile_sha256,
|
||
requested_model_id=profile.model_alias,
|
||
actual_model_id=profile.resolved_model_id,
|
||
model_match=True,
|
||
effort="governed",
|
||
max_budget_usd_per_call=format(profile.max_budget_usd_per_call, "f"),
|
||
total_cost_usd="0.001000",
|
||
usage={"input_tokens": 1, "output_tokens": 1},
|
||
model_usage={profile.resolved_model_id: {"costUSD": "0.001000"}},
|
||
stop_reason="stop",
|
||
terminal_reason="completed",
|
||
is_error=False,
|
||
api_error_status=None,
|
||
exit_code=None,
|
||
duration_ms=1,
|
||
input_sha256=sha256_json(business_input),
|
||
structured_output_sha256=sha256_json(structured_output),
|
||
json_schema_sha256=profile.json_schema_sha256,
|
||
)
|
||
return RoleInvocationResult(structured_output=structured_output, receipt=receipt)
|
||
|
||
return _dry_run_invoker
|
||
|
||
|
||
def _now_iso() -> str:
|
||
return datetime.now(timezone.utc).isoformat(timespec="seconds")
|
||
|
||
|
||
def _write_json(path: Path, value: Mapping[str, Any]) -> None:
|
||
path.write_text(
|
||
json.dumps(value, ensure_ascii=False, indent=2) + "\n",
|
||
encoding="utf-8",
|
||
)
|
||
|
||
|
||
def main(argv: list[str] | None = None) -> int:
|
||
"""读取 base 配置并把刷新结果写到新文件。"""
|
||
|
||
parser = argparse.ArgumentParser(
|
||
description="按当前 writer prompt 合同重测角色能力探针并刷新配置授权。"
|
||
)
|
||
parser.add_argument("--config", required=True, help="base 配置路径(只读)")
|
||
parser.add_argument("--output", required=True, help="刷新后配置的新文件路径")
|
||
parser.add_argument(
|
||
"--dry-run",
|
||
action="store_true",
|
||
help="用固定假产出验证 profile、探针和写文件链,不调用模型",
|
||
)
|
||
parser.add_argument("--checked-at", default=None, help="显式注入 checkedAt")
|
||
args = parser.parse_args(argv)
|
||
|
||
config_path = Path(args.config)
|
||
output_path = Path(args.output)
|
||
if output_path.resolve() == config_path.resolve():
|
||
print(json.dumps({
|
||
"tool": "refresh_runtime_probe",
|
||
"error": "PROBE_OUTPUT_SAME_AS_CONFIG",
|
||
"message": "--output 必须是不同于 --config 的新文件",
|
||
}, ensure_ascii=False))
|
||
return 2
|
||
try:
|
||
config = json.loads(config_path.read_text(encoding="utf-8"))
|
||
except (OSError, json.JSONDecodeError):
|
||
print(json.dumps({
|
||
"tool": "refresh_runtime_probe",
|
||
"error": "PROBE_CONFIG_UNREADABLE",
|
||
"message": "base 配置读取失败",
|
||
}, ensure_ascii=False))
|
||
return 2
|
||
|
||
checked_at = args.checked_at or _now_iso()
|
||
invoker: ProbeInvoker = (
|
||
make_dry_run_invoker()
|
||
if args.dry_run
|
||
else make_role_invoker(run_id=f"runtime-probe:{checked_at}")
|
||
)
|
||
try:
|
||
result = refresh_runtime_probe(
|
||
config,
|
||
invoker=invoker,
|
||
checked_at=checked_at,
|
||
dry_run=args.dry_run,
|
||
)
|
||
except ProbeRefreshError as exc:
|
||
print(json.dumps({
|
||
"tool": "refresh_runtime_probe",
|
||
"error": exc.code,
|
||
"message": exc.message,
|
||
"dryRun": args.dry_run,
|
||
"status": "failed_closed",
|
||
}, ensure_ascii=False))
|
||
return 1
|
||
except RoleRuntimeError as exc:
|
||
print(json.dumps({
|
||
"tool": "refresh_runtime_probe",
|
||
"error": exc.code,
|
||
"message": "角色调用失败关闭",
|
||
"dryRun": args.dry_run,
|
||
"status": "failed_closed",
|
||
}, ensure_ascii=False))
|
||
return 1
|
||
|
||
try:
|
||
_write_json(output_path, result.refreshed_config)
|
||
except OSError:
|
||
print(json.dumps({
|
||
"tool": "refresh_runtime_probe",
|
||
"error": "PROBE_OUTPUT_UNWRITABLE",
|
||
"message": "刷新配置写出失败",
|
||
"dryRun": args.dry_run,
|
||
"status": "failed_closed",
|
||
}, ensure_ascii=False))
|
||
return 1
|
||
summary = dict(result.summary)
|
||
summary["outputFile"] = str(output_path)
|
||
print(json.dumps(summary, ensure_ascii=False, sort_keys=True))
|
||
return 0
|
||
|
||
|
||
__all__ = [
|
||
"DRY_RUN_CANDIDATE_BODY",
|
||
"PROBE_BUSINESS_INPUT",
|
||
"PROBE_SCHEMA_VERSION",
|
||
"ProbeInvoker",
|
||
"ProbeRefreshError",
|
||
"RefreshResult",
|
||
"apply_probe",
|
||
"build_probe_record",
|
||
"build_writer_profile",
|
||
"make_dry_run_invoker",
|
||
"make_role_invoker",
|
||
"main",
|
||
"refresh_runtime_probe",
|
||
"verify_probe_result",
|
||
]
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|