Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ WaveBench 从指定路径或默认的 `wavebench.toml` 加载本地实验台配
| 类别 | 表 |
| --- | --- |
| 必需 | `[connection]`、`[scope]` |
| 可选 | `[autoscale]`、`[waveform]`、`[output]`、`[quality]`、`[safety_limits]`、`[tui]`、`[source]`、`[rf_source]`、`[power]`、`[dmm]` |
| 可选 | `[autoscale]`、`[waveform]`、`[output]`、`[quality]`、`[safety_limits]`、`[tui]`、`[source]`、`[rf_source]`、`[power]`、`[dmm]`、`[advisor]` |

字段、默认值和跨字段约束由 config model 与 parser 定义。修改 plan 或配置前,先核对[示例配置](https://github.com/Scaxlibur/wavebench/blob/master/wavebench.example.toml)、当前 CLI help 和相关 Reference。

Expand Down Expand Up @@ -37,6 +37,14 @@ access = "read_only"

配置中的 `access` 不能替代真实接线、操作系统权限或仪器自身保护。

## Advisor 外发边界

`[advisor]` 控制 advisor(外部判断模型)能否把状态发出本机。默认 `enabled = false`,关闭时不会构造任何外发请求;开启必须同时给出 `endpoint_hosts` 与 `allowed_state_fields` 白名单,否则配置校验直接失败。`accept`/`review` 是「概率 → 动作」的阈值,由 Core 拥有,必须满足 `0 <= review <= accept <= 1`。

外发还需通过同意门:发送前产出完整预览(内容、字节数、sha256、逐条不可信来源),预览不联网;同意默认按次,可登记为同一 run 内有效,并绑定 endpoint 集合与允许字段集,任一变化即失效;非交互场景(run plan、CI、MCP)一律拒绝。结论写入对应 run 目录的 `decisions/`(schema `wavebench.decision.v1`),附加式,不影响 `run.json`、质量门或 `auto_recover`。

实现状态与分期见 [advisor 插件类别 RFC(PR #20)](https://github.com/Scaxlibur/WaveBench/pull/20)(`Draft`)。

## 安全限制

`[safety_limits]` 用于在打开 transport 前限制 Source/Power 写入。Source V2 的端口电压下界和上界必须成对出现,并且下界小于上界。限制不应为了通过一次实验而放宽;应先确认实际端接、量程和实验要求。
Expand Down
62 changes: 62 additions & 0 deletions src/wavebench/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from .errors import ConfigError
from .services.access_policy import AccessMode, normalize_access_mode
from .services.advisor_consent import normalize_names

WAVEFORM_POINTS_ALIASES = {
"def": "DEF",
Expand Down Expand Up @@ -144,6 +145,34 @@ class DmmConfig:
dsrdtr: bool = False
access: AccessMode = "read_write"

@dataclass(frozen=True)
class AdvisorConfig:
"""advisor(外部判断模型)配置。

默认关闭;开启必须同时给出 endpoint 与字段白名单,避免"打开开关即放宽外发范围"。
概率阈值由 Core 拥有,插件不得自带。
"""

enabled: bool = False
endpoint_hosts: tuple[str, ...] = ()
allowed_state_fields: tuple[str, ...] = ()
accept: float = 0.60
review: float = 0.35

def __post_init__(self) -> None:
if isinstance(self.accept, bool) or isinstance(self.review, bool):
raise ConfigError("advisor thresholds must be numbers")
if not 0.0 <= float(self.review) <= float(self.accept) <= 1.0:
raise ConfigError("advisor thresholds must satisfy 0 <= review <= accept <= 1")
object.__setattr__(
self, "endpoint_hosts", normalize_names(self.endpoint_hosts, label="advisor.endpoint_hosts")
)
object.__setattr__(
self,
"allowed_state_fields",
normalize_names(self.allowed_state_fields, label="advisor.allowed_state_fields"),
)

@dataclass(frozen=True)
class OutputConfig:
directory: Path
Expand Down Expand Up @@ -364,6 +393,37 @@ def _instrument_options(raw: dict, section: str) -> dict[str, object]:
raise ConfigError(f"{section}.options must be a TOML table")
return dict(options)


def _name_list(raw: dict, key: str, *, path: str) -> tuple[str, ...]:
values = raw.get(key, [])
if isinstance(values, (str, bytes)) or not isinstance(values, (list, tuple)):
raise ConfigError(f"{path}.{key} must be an array of strings")
for value in values:
if not isinstance(value, str) or not value or value.strip() != value:
raise ConfigError(f"{path}.{key} must contain non-empty, trimmed strings")
return tuple(dict.fromkeys(values))


def _advisor_config(raw: object) -> AdvisorConfig:
if not isinstance(raw, dict):
raise ConfigError("advisor must be a TOML table")
enabled = _strict_bool(raw, "enabled", False, path="advisor")
endpoints = _name_list(raw, "endpoint_hosts", path="advisor")
fields = _name_list(raw, "allowed_state_fields", path="advisor")
if enabled and not fields:
raise ConfigError(
"advisor.enabled requires a non-empty advisor.allowed_state_fields allowlist"
)
if enabled and not endpoints:
raise ConfigError("advisor.enabled requires a non-empty advisor.endpoint_hosts allowlist")
return AdvisorConfig(
enabled=enabled,
endpoint_hosts=endpoints,
allowed_state_fields=fields,
accept=_finite_number(raw.get("accept", 0.60), path="advisor.accept"),
review=_finite_number(raw.get("review", 0.35), path="advisor.review"),
)

@dataclass(frozen=True)
class WaveBenchConfig:
connection: ConnectionConfig
Expand All @@ -380,6 +440,7 @@ class WaveBenchConfig:
tui: TuiConfig = TuiConfig()
# Append-only: preserve the public positional layout of existing config fields.
rf_source: RfSourceConfig | None = None
advisor: AdvisorConfig = AdvisorConfig()

def with_connection_timeout_ms(self, timeout_ms: int) -> "WaveBenchConfig":
if timeout_ms <= 0:
Expand Down Expand Up @@ -666,6 +727,7 @@ def load_config(path: str | Path = "wavebench.toml") -> WaveBenchConfig:
log_keep_lines_after_trim=int(tui_raw.get("log_keep_lines_after_trim", 1_000)),
),
rf_source=rf_source,
advisor=_advisor_config(raw.get("advisor", {})),
)
except KeyError as exc:
raise ConfigError(f"missing required config key: {exc}") from exc
Expand Down
Loading
Loading