【评测合同与提示词】 - writer/detector/judge 评测提示词去支架化:删评测身份/输出格式叮嘱/盲化反向叮嘱/机器字段 (改由运行时 --json-schema/--tools ""/--strict-mcp-config/输入设计强制),装回各角色本业纪律 - writer 加写作纪律:戏剧化节拍禁抄细纲概述句、具体压倒抽象、每场戏三件套、篇幅用场景写够不注水不缩水 - 篇幅自纠环修订指令区分偏短(加场景)/偏长(删冗余);机械门要求清单(章末钩子/硬事件/角色/伏笔)注入写手硬约束 - 引文校验从「必须恰好1次」放宽为「0次失败关闭、≥1次绑首次」(检测+盲评同口径) - llm 立 chat_governed 为主入口(chat 降为仅调试),clean_detect 改用 chat_governed 弃自带降级链 - rewrite/expansion/polish 输出合同对齐 writer.md(返回文本、写手不读写工作区、主会话落工作区) - 修复 confirm/replay-eval 探针两组坏自测(夹具适配现行合同、探针测试自包含不硬编码漂移哈希);补 clean_detect 离线测试 - AGENTS.md 新增 §10「Agent 提示词与 Skill 审查标准」 【skill 三层重组:单向依赖 底座→能力→编排,断两环+修生产倒挂】 - Phase 0: 新建 runtime 底座(claude_runtime/file_cas/raw_vault),断环 C1、修 continuation 生产倒挂 - Phase 1: 评分尺+门判(writer_rubric/fine_outline_rubric/writer_gate/gate_input_builder)收进 quality-gate,断环 C2、名实相符 - Phase 2: 升格管线从 parse-book 独立成 upgrade skill(备份审计契约键名稳定、仅改路径定位) - Phase 3: replay-eval 瘦成纯编排 - read-context 共用簇(build_snapshot/audit_leakage/check_snapshot/load_reference_work)下沉到新 snapshot skill,消除能力层向上引用 - Phase 4: run_writer_replay.py(130KB)拆成包(_common/budget/authorization/sample/blind/execute),__init__ 全量 re-export 测试零改动 【base 配置】三角色 effort 提 high;提示词更新;探针重测绑定 writer 合同;预算 writer 45 次/总 450 美元(含篇幅修订) 全量离线测试 36 个文件全绿;函数逻辑零改动(仅搬位置/改 import/改文档,capture_code_identity 仅改路径定位)。
465 lines
21 KiB
Python
465 lines
21 KiB
Python
#!/usr/bin/env python3
|
||
"""运行探针刷新工具:按当前 writer 合同重测结构化输出能力探针,刷新配置授权后写到新文件。
|
||
|
||
WHY(为什么需要这个工具)
|
||
正文回放的正式执行门(run_writer_replay._validate_execute_authorization)已加固:
|
||
runtimeProbe.executionProfileSha256 必须等于当前 writer profile 的身份哈希
|
||
(ExecutionProfile.execution_profile_sha256,计算时已把 jsonSchemaSha256 +
|
||
systemPromptSha256 + 模型 + CLI + 预算一并卷入)。writer 合同(schema / prompt / 模型 /
|
||
CLI / 预算)一旦升级,旧探针的身份哈希必然失配,门会以 EXECUTE_PROBE_CONTRACT_MISMATCH
|
||
失败关闭。仓里此前没有重测探针的工具,本工具补上这一环:用当前 writer 合同实跑一次极小的
|
||
合成能力探针,产出绑定新合同的 runtimeProbe,替换旧探针并重签授权自哈希,写到 --output
|
||
指定的新文件,供主代理 review diff 后再替换正式配置。
|
||
|
||
红线(对齐 replay-eval 合同)
|
||
- 探针输入是固定、极小、全合成的能力探针 prompt,绝不使用原书全文 / 真实正文 / raw 物料。
|
||
- Claude 调用经【可注入 invoker 接口】发起。默认 invoker = claude_runtime.run_claude
|
||
(真实调用层是薄薄一层,run_claude 内部已联合校验 schema / 预算 cap / deadline / 模型 /
|
||
回执);离线测试与 --dry-run 注入假 invoker,工具主体完全确定、可离线测。
|
||
- 失败关闭:调用失败 / 结构化输出 schema 不过 / 超单次预算 cap / 超 deadline / 回执不可信
|
||
→ 不写 successful 探针、不产出刷新配置、退出非零;原配置保持不动。
|
||
- 审计日志只记录做了什么(探针身份哈希、结构化输出哈希、成本、是否成功、输出文件),
|
||
绝不打印完整 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, Callable, Mapping, Protocol
|
||
|
||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||
RUNTIME_DIR = SCRIPT_DIR.parents[1] / "runtime" / "scripts"
|
||
QUALITY_GATE_DIR = SCRIPT_DIR.parents[1] / "quality-gate" / "scripts"
|
||
if str(SCRIPT_DIR) not in sys.path:
|
||
sys.path.insert(0, str(SCRIPT_DIR))
|
||
if str(RUNTIME_DIR) not in sys.path:
|
||
sys.path.insert(0, str(RUNTIME_DIR))
|
||
if str(QUALITY_GATE_DIR) not in sys.path:
|
||
sys.path.insert(0, str(QUALITY_GATE_DIR))
|
||
|
||
from claude_runtime import ( # noqa: E402
|
||
HASH_PATTERN,
|
||
ClaudeInvocationResult,
|
||
ClaudeRuntimeError,
|
||
ExecutionProfile,
|
||
ExecutionReceipt,
|
||
run_claude,
|
||
sha256_json,
|
||
validate_json_schema,
|
||
)
|
||
# WHY: 重签必须与门校验逐字节同源。run_writer_replay 的授权门用 gate_input_builder.canonical_sha256
|
||
# 复核 probe/budget/raw 各自的 receiptSha256,这里复用同一函数,保证重签结果与门校验完全一致。
|
||
from gate_input_builder import canonical_sha256 # noqa: E402
|
||
# WHY: 复用门自己用来从配置重建 writer profile 的函数,保证本工具算出的 execution_profile_sha256
|
||
# 与门算出的逐字节相同;它只读 executionProfiles.writer,不碰需要 oracleTruthPacks 的整适配路径。
|
||
from run_writer_replay import _profile_from_mapping # noqa: E402
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 探针输入:固定、极小、全合成。绝不引用真实作品、真实正文或 raw 物料。
|
||
# WHY: 探针只证明「当前 writer 合同能产出一小段合规 candidateBody」,输入内容本身不参与任何
|
||
# 身份哈希,故用一段自包含的合成微任务即可;固定常量保证刷新过程确定、可复跑、可审计。
|
||
# 形状对齐 WriterCreativeInput v2(writer 系统提示词只接收该形状),让真实调用能正常产出。
|
||
# ---------------------------------------------------------------------------
|
||
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 使用的固定合成产出(同样不含任何真实作品内容)。
|
||
DRY_RUN_CANDIDATE_BODY = (
|
||
"黎明前最暗的一刻,守塔人划亮火柴,灯芯燃起一小团光,海面被照出一线金边。"
|
||
)
|
||
|
||
|
||
class ProbeRefreshError(RuntimeError):
|
||
"""探针刷新失败关闭:携带稳定错误码,绝不携带正文 / prompt / 原始响应。"""
|
||
|
||
def __init__(self, code: str, message: str) -> None:
|
||
super().__init__(f"{code}: {message}")
|
||
self.code = code
|
||
self.message = message
|
||
|
||
|
||
class ProbeInvoker(Protocol):
|
||
"""可注入的模型调用接口:真实层与确定层之间的唯一 seam。
|
||
|
||
真实实现 = claude_runtime.run_claude(默认);离线测试 / --dry-run 注入返回预设
|
||
ClaudeInvocationResult 的假实现。签名与 run_claude 的前两个位置参数对齐。
|
||
"""
|
||
|
||
def __call__(
|
||
self, profile: ExecutionProfile, business_input: Mapping[str, Any]
|
||
) -> ClaudeInvocationResult: ...
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class RefreshResult:
|
||
"""一次成功刷新的确定性产物:刷新后配置 + 新探针 + 审计安全摘要。"""
|
||
|
||
refreshed_config: dict[str, Any]
|
||
probe: dict[str, Any]
|
||
summary: dict[str, Any]
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 确定层:构建 profile、校验回执、构建探针、重签授权。全程无模型调用、无文件 IO、无墙钟。
|
||
# ---------------------------------------------------------------------------
|
||
def build_writer_profile(config: Mapping[str, Any]) -> ExecutionProfile:
|
||
"""从 base 配置的 executionProfiles.writer 构建当前 writer 冻结 profile。
|
||
|
||
WHY: 只读 writer profile(探针只用 writer 合同),复用门同款 _profile_from_mapping,
|
||
保证 execution_profile_sha256 与门逐字节一致;不读 semantic / judge,不读 oracleTruthPacks。
|
||
"""
|
||
|
||
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 verify_probe_result(profile: ExecutionProfile, result: ClaudeInvocationResult) -> None:
|
||
"""失败关闭地复核 invoker 产出:任一异常都抛 ProbeRefreshError,绝不放行可疑回执。
|
||
|
||
WHY: run_claude 内部已联合校验 schema / 预算 / deadline / 模型 / 回执;这里再做一层同口径
|
||
的独立复核,既是对真实调用的纵深防御,也让「假 invoker 返回坏产出」在离线测试里可被精确触发。
|
||
所有错误码只描述失败类别,不携带正文 / prompt / 原始响应。
|
||
"""
|
||
|
||
receipt = result.receipt
|
||
|
||
# 1. 结构化输出必须符合当前 writer schema(candidateBody 非空字符串、无额外字段)。
|
||
try:
|
||
validate_json_schema(result.structured_output, profile.json_schema)
|
||
except Exception as exc: # noqa: BLE001 - 任何 schema 异常统一失败关闭。
|
||
raise ProbeRefreshError("PROBE_SCHEMA_INVALID", "结构化输出不符合 writer schema") from exc
|
||
|
||
# 2. 回执必须绑定到当前 profile 身份,证明它确实是针对当前合同跑的。
|
||
if receipt.execution_profile_sha256 != profile.execution_profile_sha256:
|
||
raise ProbeRefreshError(
|
||
"PROBE_PROFILE_BINDING_MISMATCH", "回执身份哈希与当前 writer profile 不一致"
|
||
)
|
||
|
||
# 3. 结构化输出哈希必须是合法 SHA-256,且等于实际产出的规范哈希,防止挂空壳。
|
||
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", "结构化输出哈希缺失、非法或与产出不一致"
|
||
)
|
||
|
||
# 4. 模型必须匹配:model_match 为真且实际模型等于冻结的完整模型 ID。
|
||
if receipt.model_match is not True or receipt.actual_model_id != profile.resolved_model_id:
|
||
raise ProbeRefreshError("PROBE_MODEL_MISMATCH", "实际模型与冻结模型不一致")
|
||
|
||
# 5. 回执不得标记错误。
|
||
if receipt.is_error is not False:
|
||
raise ProbeRefreshError("PROBE_RECEIPT_ERROR", "回执标记为错误")
|
||
|
||
# 6. 进程必须正常退出。
|
||
if receipt.exit_code != 0:
|
||
raise ProbeRefreshError("PROBE_NONZERO_EXIT", "Claude CLI 非零退出")
|
||
|
||
# 7. 终止原因必须落在冻结的正常终止集合内(writer 为 completed)。
|
||
if receipt.terminal_reason not in profile.normal_terminal_reasons:
|
||
raise ProbeRefreshError("PROBE_TERMINAL_REASON_INVALID", "终止原因不在正常终止集合内")
|
||
|
||
# 8. 不得携带 API 错误状态。
|
||
if receipt.api_error_status is not None:
|
||
raise ProbeRefreshError("PROBE_API_ERROR", "回执携带 API 错误状态")
|
||
|
||
# 9. 单次成本必须可信且不超过冻结的单次预算 cap。
|
||
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: ExecutionProfile, result: ClaudeInvocationResult, *, checked_at: str
|
||
) -> dict[str, Any]:
|
||
"""用调用结果构建新的 runtimeProbe 记录并重签其 receiptSha256 自哈希。
|
||
|
||
字段集与现有 runtimeProbe 完全一致(不缺不多):身份 / 运行时绑定来自 writer profile,
|
||
能力证据(身份哈希、输出哈希、成本、终止原因、模型匹配、退出码、API 状态)来自回执,
|
||
executionReceiptSha256 绑定完整回执,receiptSha256 是记录自身(去掉 receiptSha256)的规范哈希。
|
||
"""
|
||
|
||
receipt = result.receipt
|
||
# WHY: 先构建不含 receiptSha256 的记录,对它取规范哈希作为自哈希,再回填——这与门校验
|
||
# `item.receiptSha256 == canonical_sha256({k: v for k, v in item.items() if k != "receiptSha256"})`
|
||
# 完全等价,保证重签后自检逐字节通过。
|
||
record: dict[str, Any] = {
|
||
"status": "successful",
|
||
"checkedAt": checked_at,
|
||
"claudeExecutablePath": profile.claude_executable_path,
|
||
"claudeExecutableSha256": profile.claude_executable_sha256,
|
||
"claudeCliVersion": profile.claude_cli_version,
|
||
"modelAlias": profile.model_alias,
|
||
"resolvedModelId": profile.resolved_model_id,
|
||
"executionProfileSha256": receipt.execution_profile_sha256,
|
||
"executionReceiptSha256": canonical_sha256(receipt.as_dict()),
|
||
"structuredOutputSha256": receipt.structured_output_sha256,
|
||
"terminalReason": receipt.terminal_reason,
|
||
"totalCostUsd": receipt.total_cost_usd,
|
||
"modelMatch": receipt.model_match,
|
||
"exitCode": receipt.exit_code,
|
||
"apiErrorStatus": receipt.api_error_status,
|
||
}
|
||
record["receiptSha256"] = canonical_sha256(record)
|
||
return record
|
||
|
||
|
||
def apply_probe(config: Mapping[str, Any], probe: Mapping[str, Any]) -> dict[str, Any]:
|
||
"""把新探针写入 executionAuthorization.runtimeProbe 的深拷贝,返回刷新后的完整配置。
|
||
|
||
WHY: 深拷贝保证原配置对象不被就地改动;只替换 runtimeProbe,budget / rawRetention /
|
||
profileSha256 原样保留(它们的自哈希不受探针刷新影响,门会各自复核)。
|
||
"""
|
||
|
||
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: ExecutionProfile, 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"),
|
||
"executionProfileSha256": probe.get("executionProfileSha256"),
|
||
"structuredOutputSha256": probe.get("structuredOutputSha256"),
|
||
"executionReceiptSha256": probe.get("executionReceiptSha256"),
|
||
"probeReceiptSha256": probe.get("receiptSha256"),
|
||
"totalCostUsd": probe.get("totalCostUsd"),
|
||
"modelAlias": profile.model_alias,
|
||
"resolvedModelId": profile.resolved_model_id,
|
||
"claudeCliVersion": profile.claude_cli_version,
|
||
"checkedAt": probe.get("checkedAt"),
|
||
}
|
||
|
||
|
||
def refresh_runtime_probe(
|
||
config: Mapping[str, Any],
|
||
*,
|
||
invoker: ProbeInvoker,
|
||
checked_at: str,
|
||
dry_run: bool = False,
|
||
) -> RefreshResult:
|
||
"""工具主体的确定链路:构建 profile → 调用 → 失败关闭复核 → 构建探针 → 重签 → 刷新配置。
|
||
|
||
无文件 IO、无墙钟(checked_at 由调用方注入)、真实调用只发生在传入的 invoker 内部。
|
||
任一步失败抛 ProbeRefreshError / ClaudeRuntimeError,调用方据此退出非零、不产出刷新配置。
|
||
"""
|
||
|
||
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)
|
||
refreshed = apply_probe(config, probe)
|
||
summary = _audit_summary(profile, probe, dry_run=dry_run)
|
||
return RefreshResult(refreshed_config=refreshed, probe=probe, summary=summary)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 真实调用层(薄薄一层)与 dry-run 假调用层。
|
||
# ---------------------------------------------------------------------------
|
||
def make_dry_run_invoker() -> ProbeInvoker:
|
||
"""返回固定合成产出的假 invoker,供 --dry-run 冒烟「构建+重签+写文件」链路,绝不发起真实调用。"""
|
||
|
||
def _dry_run_invoker(
|
||
profile: ExecutionProfile, business_input: Mapping[str, Any]
|
||
) -> ClaudeInvocationResult:
|
||
structured_output = {"candidateBody": DRY_RUN_CANDIDATE_BODY}
|
||
receipt = ExecutionReceipt(
|
||
adapter_role=profile.adapter_role,
|
||
invocation_id="dry-run-probe",
|
||
execution_profile_sha256=profile.execution_profile_sha256,
|
||
requested_model_id=profile.resolved_model_id,
|
||
actual_model_id=profile.resolved_model_id,
|
||
model_match=True,
|
||
effort=profile.effort,
|
||
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="end_turn",
|
||
terminal_reason=profile.normal_terminal_reasons[0],
|
||
is_error=False,
|
||
api_error_status=None,
|
||
exit_code=0,
|
||
duration_ms=1,
|
||
input_sha256=sha256_json(business_input),
|
||
structured_output_sha256=sha256_json(structured_output),
|
||
json_schema_sha256=profile.json_schema_sha256,
|
||
)
|
||
return ClaudeInvocationResult(structured_output=structured_output, receipt=receipt)
|
||
|
||
return _dry_run_invoker
|
||
|
||
|
||
def _now_iso() -> str:
|
||
"""当前 UTC 时间的 ISO-8601 字符串,作为 checkedAt 默认值。"""
|
||
|
||
return datetime.now(timezone.utc).isoformat(timespec="seconds")
|
||
|
||
|
||
def _write_json(path: Path, value: Mapping[str, Any]) -> None:
|
||
"""以稳定格式写出配置:UTF-8、保留中文、两空格缩进、结尾换行。"""
|
||
|
||
path.write_text(
|
||
json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
|
||
)
|
||
|
||
|
||
def main(argv: list[str] | None = None) -> int:
|
||
"""CLI 入口:读 base 配置 → 刷新探针 → 写到 --output 新文件;失败关闭返回非零。"""
|
||
|
||
parser = argparse.ArgumentParser(
|
||
description="按当前 writer 合同重测能力探针并刷新配置授权(写到新文件,不就地覆盖)。"
|
||
)
|
||
parser.add_argument("--config", required=True, help="base 配置路径(只读)")
|
||
parser.add_argument("--output", required=True, help="刷新后配置的写出路径(新文件)")
|
||
parser.add_argument(
|
||
"--dry-run",
|
||
action="store_true",
|
||
help="不发起真实调用,用固定假产出走通构建+重签+写文件链路(冒烟用)",
|
||
)
|
||
parser.add_argument(
|
||
"--checked-at",
|
||
default=None,
|
||
help="探针 checkedAt 时间戳(默认当前 UTC);离线复跑可显式注入以保证确定",
|
||
)
|
||
args = parser.parse_args(argv)
|
||
|
||
config_path = Path(args.config)
|
||
output_path = Path(args.output)
|
||
# WHY: 强制输出是新文件,绝不就地覆盖原配置,便于主代理 review diff 后再替换。
|
||
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) as exc:
|
||
print(
|
||
json.dumps(
|
||
{"tool": "refresh_runtime_probe", "error": "PROBE_CONFIG_UNREADABLE",
|
||
"message": "base 配置读取失败"},
|
||
ensure_ascii=False,
|
||
)
|
||
)
|
||
return 2
|
||
|
||
invoker: ProbeInvoker = make_dry_run_invoker() if args.dry_run else run_claude
|
||
checked_at = args.checked_at or _now_iso()
|
||
|
||
try:
|
||
result = refresh_runtime_probe(
|
||
config, invoker=invoker, checked_at=checked_at, dry_run=args.dry_run
|
||
)
|
||
except ProbeRefreshError as exc:
|
||
# 审计安全:只报错误码与类别,绝不报正文 / prompt / 原始响应。
|
||
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 ClaudeRuntimeError as exc:
|
||
# WHY: 真实调用层(run_claude)的失败关闭(调用失败 / 超预算 / 超 deadline / schema /
|
||
# 回执不可信)在这里统一兜底:不写探针、不改配置、退出非零。只透传稳定主码 exc.code,
|
||
# 不透传可能携带敏感上下文的 message 正文。
|
||
print(
|
||
json.dumps(
|
||
{"tool": "refresh_runtime_probe", "error": exc.code,
|
||
"message": "Claude 调用失败关闭", "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",
|
||
"ProbeInvoker",
|
||
"ProbeRefreshError",
|
||
"RefreshResult",
|
||
"apply_probe",
|
||
"build_probe_record",
|
||
"build_writer_profile",
|
||
"make_dry_run_invoker",
|
||
"main",
|
||
"refresh_runtime_probe",
|
||
"verify_probe_result",
|
||
]
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|