mirror of
https://github.com/andrewyng/openworker.git
synced 2026-09-11 06:30:25 +00:00
Spec Part 6 step 3. The reviewer runs on every approval card and records what it WOULD have decided, while the human decides everything. This is how the ship gates get measured on real sessions before the flag ever defaults on. Nothing about a decision changes. - config.py: auto_approve_shadow flag, off by default, _GLOBAL_ONLY (a cloned repo can't turn it on). agent.py attaches the reviewer when either auto_approve OR the shadow flag is set; reviewer_shadow gates only the recording path. - engine.py: _spawn_shadow_review fires the reviewer fire-and-forget from the needs_user branch and audits stage="reviewer_shadow" joined to the human's approval_resolved row by call_id. The card is never delayed; a shadow failure never surfaces. Skipped when the live path already consulted the reviewer this card (no double spend). approval_requested / approval_resolved rows gained call_id for the join. Eval harness (scripts/eval_reviewer.py, spec 7.5): - Runs the reviewer against three JSONL corpora and scores the ship gates: benign allow-rate >= 30% (prompt-reduction proxy), zero false-allows on dangerous and injection. Exit 1 on any gate failure. - Corpora seeded: benign (20), dangerous (15), injection (13), each with a ~20% holdout and per-row answer keys, in the spec's 7.5.1 format. Known world is reconstructed folders-and-remotes-only, matching the engine. - --stub runs with no network (canned verdicts) for plumbing/CI; real runs use ProviderRouter and cost money, so this is on-demand, not a pytest. tests/test_shadow_eval.py (18): shadow records but never decides; shadow off records nothing; live allow/unsure never double-recorded; shadow errors swallowed; corpora well-formed; scoring/gate maths; stub passes all gates.
158 lines
6.3 KiB
Python
158 lines
6.3 KiB
Python
"""Configuration — layered TOML: built-in defaults < global < per-workspace.
|
|
|
|
Global: <state-dir>/config.toml (see `secrets.state_dir`; platform-native)
|
|
Workspace: <workspace>/.coworker/config.toml (overrides global)
|
|
|
|
Workspace command allowances apply only after the user trusts that exact canonical
|
|
workspace path. Other permission grants remain global-only.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
try:
|
|
import tomllib # stdlib since 3.11
|
|
except ModuleNotFoundError: # 3.10, the floor requires-python declares
|
|
import tomli as tomllib # type: ignore[no-redef]
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
from typing import Any, Optional
|
|
|
|
from .secrets import state_dir
|
|
|
|
# Commands auto-run WITHOUT an approval prompt. There is no generally safe executable:
|
|
# nominally read-only programs can read secrets outside the workspace, expand environment
|
|
# variables, load project-controlled config/plugins, or execute helpers (for example
|
|
# `find -exec` and pytest collection). Keep the built-in list empty. A user may explicitly
|
|
# opt into command prefixes in their user-owned global config, accepting that authority.
|
|
DEFAULT_ALLOWED_COMMANDS: list[str] = []
|
|
|
|
|
|
@dataclass
|
|
class Config:
|
|
model: str = "gpt-5.6-sol"
|
|
mode: str = "interactive"
|
|
max_iterations: int = 150
|
|
allowed_commands: list[str] = field(
|
|
default_factory=lambda: list(DEFAULT_ALLOWED_COMMANDS)
|
|
)
|
|
# In "custom" permission mode, these tools are auto-approved (e.g. file edits)
|
|
# while everything else still asks.
|
|
auto_allow: list[str] = field(default_factory=list)
|
|
# Egress destinations `web_fetch` may reach WITHOUT an approval prompt (exact host or
|
|
# subdomain). Empty by default — the first fetch to any host asks. A power-user opt-in,
|
|
# like `allowed_commands`; user-global only, so a repo can't widen the agent's network reach.
|
|
allowed_domains: list[str] = field(default_factory=list)
|
|
# Auto-Approve mode's feature flag (spec §1.5): when true, sessions get an LLM reviewer
|
|
# that judges would-be approval cards in Mode.AUTO_APPROVE. Off by default; user-global
|
|
# only — a cloned repo must not be able to hand itself a looser reviewer.
|
|
auto_approve: bool = False
|
|
# Shadow evaluation (spec Part 6 step 3): the reviewer records what it WOULD have
|
|
# decided on every approval card while the human still decides. Verdicts land in the
|
|
# audit log next to the human's outcome and nothing else changes — this is how the ship
|
|
# gates (zero false-allows; ≥30% fewer prompts) get measured on real sessions. Costs
|
|
# one model call per card while on. Off by default; user-global only.
|
|
auto_approve_shadow: bool = False
|
|
host: str = "127.0.0.1"
|
|
port: int = 8765
|
|
# Web search provider: "duckduckgo" (keyless default) | "tavily" | "brave" (need a key).
|
|
web_search_provider: str = "duckduckgo"
|
|
# OpenWorker Cloud (sign-in + managed connectors). Config, never constants:
|
|
# dev/staging/BYO-VPC deployments point these at their own instances.
|
|
cloud_base_url: str = "https://api.openworker.com"
|
|
# Auth0 tenant + API audience are registered identifiers, not branding: the
|
|
# tenant name can never be renamed, and the audience must match the API
|
|
# identifier registered in Auth0 — both keep the legacy value on purpose.
|
|
cloud_auth_domain: str = "opencoworker.us.auth0.com"
|
|
cloud_client_id: str = "g1l4Q1lhYWmyS03qPSf4KEJGrgq02Qam"
|
|
cloud_audience: str = "https://api.opencoworker.app"
|
|
# Managed relay WebSocket endpoint (Slack/GitHub inbound). Defaults to the
|
|
# PRODUCTION relay so a fresh install relays out of the box — an empty
|
|
# default shipped once as "connected but relay OFF" on every machine
|
|
# without a hand-edited config.toml. Empty override ⇒ relay disabled
|
|
# (manual Socket Mode still works); dev/BYO deployments point elsewhere.
|
|
cloud_relay_ws_url: str = (
|
|
"wss://l4z1paxb83.execute-api.us-east-1.amazonaws.com/ocw-connect"
|
|
)
|
|
|
|
|
|
_FIELDS = {
|
|
"model",
|
|
"mode",
|
|
"max_iterations",
|
|
"allowed_commands",
|
|
"auto_allow",
|
|
"allowed_domains",
|
|
"auto_approve",
|
|
"auto_approve_shadow",
|
|
"host",
|
|
"port",
|
|
"web_search_provider",
|
|
"cloud_base_url",
|
|
"cloud_auth_domain",
|
|
"cloud_client_id",
|
|
"cloud_audience",
|
|
"cloud_relay_ws_url",
|
|
}
|
|
|
|
# These fields change what consequential actions can run without a prompt, so the normal
|
|
# workspace override pass never applies them. `allowed_commands` is added separately only
|
|
# for a canonically trusted workspace; `auto_allow` and `allowed_domains` remain user-global
|
|
# only (a repo must not be able to widen the agent's command or network reach).
|
|
_GLOBAL_ONLY_FIELDS = {
|
|
"allowed_commands",
|
|
"auto_allow",
|
|
"allowed_domains",
|
|
"auto_approve",
|
|
"auto_approve_shadow",
|
|
}
|
|
_WORKSPACE_FIELDS = _FIELDS - _GLOBAL_ONLY_FIELDS
|
|
|
|
|
|
def global_config_path() -> Path:
|
|
return state_dir() / "config.toml"
|
|
|
|
|
|
def _read(path: Path) -> dict[str, Any]:
|
|
try:
|
|
with open(path, "rb") as f:
|
|
return tomllib.load(f)
|
|
except (OSError, tomllib.TOMLDecodeError):
|
|
return {}
|
|
|
|
|
|
def workspace_allowed_commands(workspace: str | Path) -> list[str]:
|
|
"""Command prefixes requested by repository config; advisory until workspace trust."""
|
|
path = Path(workspace).expanduser() / ".coworker" / "config.toml"
|
|
value = _read(path).get("allowed_commands", [])
|
|
if not isinstance(value, list):
|
|
return []
|
|
return list(dict.fromkeys(v.strip() for v in value if isinstance(v, str) and v.strip()))
|
|
|
|
|
|
def load_config(
|
|
workspace: Optional[str | Path] = None,
|
|
*,
|
|
global_path: Optional[Path] = None,
|
|
workspace_trusted: bool = False,
|
|
) -> Config:
|
|
cfg = Config()
|
|
|
|
g = Path(global_path) if global_path is not None else global_config_path()
|
|
if g.is_file():
|
|
for key, value in _read(g).items():
|
|
if key in _FIELDS:
|
|
setattr(cfg, key, value)
|
|
if workspace:
|
|
w = Path(workspace).expanduser() / ".coworker" / "config.toml"
|
|
if w.is_file():
|
|
for key, value in _read(w).items():
|
|
if key in _WORKSPACE_FIELDS:
|
|
setattr(cfg, key, value)
|
|
if workspace_trusted:
|
|
cfg.allowed_commands = list(
|
|
dict.fromkeys(
|
|
[*cfg.allowed_commands, *workspace_allowed_commands(workspace)]
|
|
)
|
|
)
|
|
return cfg
|