|
@@ -24,15 +24,21 @@ def emit(kind, **values):
|
|
|
WIRE.write(json.dumps({"nonce": NONCE, "type": kind, **values}, ensure_ascii=False) + "\n")
|
|
WIRE.write(json.dumps({"nonce": NONCE, "type": kind, **values}, ensure_ascii=False) + "\n")
|
|
|
WIRE.flush()
|
|
WIRE.flush()
|
|
|
|
|
|
|
|
-def resolve_model():
|
|
|
|
|
|
|
+class ConfigurationError(Exception):
|
|
|
|
|
+ def __init__(self, code):
|
|
|
|
|
+ self.code = code
|
|
|
|
|
+ super().__init__(code)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def load_profile():
|
|
|
source = Path(BOOT["hermesSource"]).resolve()
|
|
source = Path(BOOT["hermesSource"]).resolve()
|
|
|
root = Path(BOOT["hermesHome"]).resolve()
|
|
root = Path(BOOT["hermesHome"]).resolve()
|
|
|
profile = BOOT["profile"]
|
|
profile = BOOT["profile"]
|
|
|
if not re.fullmatch(r"[a-z0-9][a-z0-9_-]{0,63}", profile):
|
|
if not re.fullmatch(r"[a-z0-9][a-z0-9_-]{0,63}", profile):
|
|
|
- raise ValueError("invalid profile")
|
|
|
|
|
|
|
+ raise ConfigurationError("computer_profile_invalid")
|
|
|
home = root if profile == "default" else root / "profiles" / profile
|
|
home = root if profile == "default" else root / "profiles" / profile
|
|
|
if not (home / "config.yaml").is_file():
|
|
if not (home / "config.yaml").is_file():
|
|
|
- raise ValueError("profile config missing")
|
|
|
|
|
|
|
+ raise ConfigurationError("computer_profile_missing")
|
|
|
os.environ["HERMES_HOME"] = str(home)
|
|
os.environ["HERMES_HOME"] = str(home)
|
|
|
sys.path.insert(0, str(source))
|
|
sys.path.insert(0, str(source))
|
|
|
from dotenv import dotenv_values
|
|
from dotenv import dotenv_values
|
|
@@ -42,21 +48,46 @@ def resolve_model():
|
|
|
from hermes_constants import set_hermes_home_override
|
|
from hermes_constants import set_hermes_home_override
|
|
|
set_hermes_home_override(str(home))
|
|
set_hermes_home_override(str(home))
|
|
|
from hermes_cli.config import load_config_readonly
|
|
from hermes_cli.config import load_config_readonly
|
|
|
- from hermes_cli.runtime_provider import resolve_runtime_provider
|
|
|
|
|
- config = load_config_readonly()
|
|
|
|
|
|
|
+ try:
|
|
|
|
|
+ return load_config_readonly()
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ raise ConfigurationError("computer_profile_config_invalid") from None
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def resolve_workspace(config):
|
|
|
|
|
+ terminal = config.get("terminal", {})
|
|
|
|
|
+ if not isinstance(terminal, dict):
|
|
|
|
|
+ raise ConfigurationError("computer_cwd_invalid")
|
|
|
|
|
+ raw = terminal.get("cwd")
|
|
|
|
|
+ if raw is None or raw == "":
|
|
|
|
|
+ raw = "."
|
|
|
|
|
+ if not isinstance(raw, str) or not raw.strip() or len(raw) > 4096 or re.search(r"[\x00-\x1f\x7f]", raw):
|
|
|
|
|
+ raise ConfigurationError("computer_cwd_invalid")
|
|
|
|
|
+ configured = raw.strip()
|
|
|
|
|
+ # Placeholders refer to the assigned Linux workspace, never the Web/App
|
|
|
|
|
+ # release directory inherited by this host-side configuration reader.
|
|
|
|
|
+ base = BOOT.get("defaultCwd", "/home/cua/workspace")
|
|
|
|
|
+ path = "." if configured in (".", "auto", "cwd") else os.path.expanduser(configured)
|
|
|
|
|
+ cwd = os.path.normpath(path if os.path.isabs(path) else os.path.join(base, path))
|
|
|
|
|
+ if not os.path.isabs(path) and os.path.commonpath([base, cwd]) != base:
|
|
|
|
|
+ raise ConfigurationError("computer_cwd_invalid")
|
|
|
|
|
+ return {"cwd": cwd, "configuredCwd": configured}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def resolve_model():
|
|
|
|
|
+ config = load_profile()
|
|
|
|
|
+ workspace = resolve_workspace(config)
|
|
|
model = config.get("model", {})
|
|
model = config.get("model", {})
|
|
|
if not isinstance(model, dict) or not model.get("default"):
|
|
if not isinstance(model, dict) or not model.get("default"):
|
|
|
- raise ValueError("model config missing")
|
|
|
|
|
- runtime = resolve_runtime_provider(requested=model.get("provider"), explicit_api_key=model.get("api_key"), explicit_base_url=model.get("base_url"), target_model=model["default"])
|
|
|
|
|
|
|
+ raise ConfigurationError("computer_model_missing")
|
|
|
|
|
+ try:
|
|
|
|
|
+ from hermes_cli.runtime_provider import resolve_runtime_provider
|
|
|
|
|
+ runtime = resolve_runtime_provider(requested=model.get("provider"), explicit_api_key=model.get("api_key"), explicit_base_url=model.get("base_url"), target_model=model["default"])
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ raise ConfigurationError("computer_model_unavailable") from None
|
|
|
if runtime.get("api_mode") not in ("chat_completions", "anthropic_messages", "codex_responses"):
|
|
if runtime.get("api_mode") not in ("chat_completions", "anthropic_messages", "codex_responses"):
|
|
|
- raise ValueError("provider cannot run with closed tool dispatch")
|
|
|
|
|
- cwd = config.get("terminal", {}).get("cwd")
|
|
|
|
|
- if not isinstance(cwd, str) or not cwd.strip():
|
|
|
|
|
- raise ValueError("terminal.cwd required")
|
|
|
|
|
- cwd = os.path.normpath(os.path.expanduser(cwd.strip()))
|
|
|
|
|
- if not os.path.isabs(cwd):
|
|
|
|
|
- raise ValueError("absolute terminal.cwd required")
|
|
|
|
|
- emit("resolved", model={key: runtime[key] for key in ("provider", "api_mode", "base_url", "api_key") if key in runtime} | {"model": model["default"]}, cwd=cwd, configuredCwd=config["terminal"]["cwd"])
|
|
|
|
|
|
|
+ raise ConfigurationError("computer_model_unsupported")
|
|
|
|
|
+ emit("resolved", model={key: runtime[key] for key in ("provider", "api_mode", "base_url", "api_key") if key in runtime} | {"model": model["default"]}, **workspace)
|
|
|
|
|
|
|
|
|
|
|
|
|
def run_model():
|
|
def run_model():
|
|
@@ -161,7 +192,9 @@ def run_model():
|
|
|
|
|
|
|
|
try:
|
|
try:
|
|
|
with contextlib.redirect_stdout(sys.stderr):
|
|
with contextlib.redirect_stdout(sys.stderr):
|
|
|
- if BOOT.get("mode") == "resolve":
|
|
|
|
|
|
|
+ if BOOT.get("mode") == "resolve-workspace":
|
|
|
|
|
+ emit("resolved", **resolve_workspace(load_profile()))
|
|
|
|
|
+ elif BOOT.get("mode") == "resolve":
|
|
|
resolve_model()
|
|
resolve_model()
|
|
|
else:
|
|
else:
|
|
|
run_model()
|
|
run_model()
|
|
@@ -170,5 +203,5 @@ except BaseException as error:
|
|
|
import traceback
|
|
import traceback
|
|
|
traceback.print_exc(file=sys.stderr)
|
|
traceback.print_exc(file=sys.stderr)
|
|
|
# Never serialize SDK exception strings, request headers, or credentials.
|
|
# Never serialize SDK exception strings, request headers, or credentials.
|
|
|
- emit("failed", message="Hermes Worker 执行失败,请核对基础 Profile 与模型连接")
|
|
|
|
|
|
|
+ emit("failed", code=error.code if isinstance(error, ConfigurationError) else "computer_worker_failed")
|
|
|
sys.exit(1)
|
|
sys.exit(1)
|