feat: OpenMesh 基础平台与 MD/PDF 转换技能
- 后端: coworker 智能体框架, WS API, 文件上传, 附件处理 - 前端: Open WebUI, 文件全量走 upload API (含 MD/TXT/JSON 等文本类) - 技能: md-to-office (pandoc + wkhtmltopdf) - 修复: 上传文件路径丢失, Agent 搜索浪费, 输出文件跑到 uploads/ - 打包: PyInstaller one-dir, 预打包 pandoc/wkhtmltopdf/chromium
This commit is contained in:
157
coworker/config.py
Normal file
157
coworker/config.py
Normal file
@@ -0,0 +1,157 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user