diff --git a/docs/implementation-checkpoints.md b/docs/implementation-checkpoints.md index a83b1d6..9953bb0 100644 --- a/docs/implementation-checkpoints.md +++ b/docs/implementation-checkpoints.md @@ -16,6 +16,8 @@ Operator surfaces: The remaining human gates are external sandbox submission, external disclosure delivery, and final publication approval. +Follow-up: bridge service installation now persists the selected OpenCodex provider/model identifier in the fixed service command. This prevents an autonomous background worker from silently reverting to a provider default after the browser closes or the workstation restarts. The service file still contains no provider credentials. + ## 107 Hermes Agent v1.0.0 Integration Status: complete in production. diff --git a/docs/research-automation.md b/docs/research-automation.md index b72c983..9479092 100644 --- a/docs/research-automation.md +++ b/docs/research-automation.md @@ -27,7 +27,9 @@ Agent-review mode is the recommended high-automation setting for a local researc Install or update the background bridge in agent-review mode: ```bash -secopsai intelligence bridge service install --autonomy-mode agent_review +secopsai intelligence bridge service install \ + --autonomy-mode agent_review \ + --model kimi/kimi-k2.7-code-highspeed ``` Complete an already-waiting pipeline from the CLI: @@ -45,6 +47,8 @@ Mission Control exposes the same operation as **Complete Agent Review**. The act - reruns publication safety; - does not upload an artifact, contact a third party, or publish content. +The model identifier is stored in the user service definition, not only in the browser session. Provider credentials remain owned by OpenCodex or Codex and are never copied into the service file. + Use supervised mode when policy requires proposal-by-proposal human acceptance: ```bash diff --git a/secopsai/cli.py b/secopsai/cli.py index a2ed90b..dc29182 100644 --- a/secopsai/cli.py +++ b/secopsai/cli.py @@ -1205,6 +1205,11 @@ def parse_args(argv: Optional[List[str]] = None) -> argparse.Namespace: default="supervised", help="Automatically complete bounded research review when set to agent_review", ) + intelligence_bridge_service.add_argument( + "--model", + default="", + help="Persist this OpenCodex provider/model for the background bridge", + ) research = sub.add_parser("research", help="Generate source-backed research reports and preflight checks") research_sub = research.add_subparsers(dest="research_cmd", required=True) @@ -3258,6 +3263,7 @@ def main(argv: Optional[List[str]] = None) -> int: db_path=args.db_path, start=not args.no_start, autonomy_mode=args.autonomy_mode, + model=args.model, ) else: payload = codex_bridge_service_action(args.action, tail=args.tail) diff --git a/secopsai/codex_bridge_service.py b/secopsai/codex_bridge_service.py index 9b56208..f83d632 100644 --- a/secopsai/codex_bridge_service.py +++ b/secopsai/codex_bridge_service.py @@ -3,6 +3,7 @@ import os import platform import plistlib +import re import shlex import subprocess import sys @@ -15,6 +16,7 @@ LABEL = "ai.secopsai.codex-bridge" SYSTEMD_UNIT = "secopsai-codex-bridge.service" RunCommand = Callable[[Sequence[str]], subprocess.CompletedProcess[str]] +MODEL_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/-]{0,199}$") def install_service( @@ -25,6 +27,7 @@ def install_service( platform_name: str | None = None, runner: RunCommand | None = None, autonomy_mode: str = "supervised", + model: str = "", ) -> dict[str, Any]: resolved_home = (home or Path.home()).expanduser().resolve() system = (platform_name or platform.system()).lower() @@ -32,10 +35,13 @@ def install_service( autonomy_mode = str(autonomy_mode or "supervised").strip().lower() if autonomy_mode not in {"supervised", "agent_review"}: raise ValueError("autonomy mode must be supervised or agent_review") + model = str(model or "").strip() + if model and not MODEL_ID_RE.fullmatch(model): + raise ValueError("bridge model id contains unsupported characters") if system == "darwin": - result = _install_launchd(resolved_home, db_path, run, start, autonomy_mode) + result = _install_launchd(resolved_home, db_path, run, start, autonomy_mode, model) elif system == "linux": - result = _install_systemd(resolved_home, db_path, run, start, autonomy_mode) + result = _install_systemd(resolved_home, db_path, run, start, autonomy_mode, model) else: raise ValueError("automatic Codex bridge service installation supports macOS and Linux") return {"status": "installed", "service": LABEL, **result} @@ -64,7 +70,7 @@ def service_action( raise ValueError("Codex bridge service controls support macOS and Linux") -def _install_launchd(home: Path, db_path: str | None, run: RunCommand, start: bool, autonomy_mode: str) -> dict[str, Any]: +def _install_launchd(home: Path, db_path: str | None, run: RunCommand, start: bool, autonomy_mode: str, model: str) -> dict[str, Any]: launch_agents = home / "Library" / "LaunchAgents" logs = home / "Library" / "Logs" / "SecOpsAI" launch_agents.mkdir(parents=True, exist_ok=True) @@ -81,6 +87,8 @@ def _install_launchd(home: Path, db_path: str | None, run: RunCommand, start: bo "--db-path", str(Path(db_path or soc_store.default_db_path()).expanduser().resolve()), ] + if model: + args.extend(["--model", model]) environment = { "HOME": str(home), "PATH": os.environ.get("PATH", "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"), @@ -115,10 +123,11 @@ def _install_launchd(home: Path, db_path: str | None, run: RunCommand, start: bo "logs": [str(logs / "codex-bridge.out.log"), str(logs / "codex-bridge.err.log")], "credentials_persisted": False, "autonomy_mode": autonomy_mode, + "model": model or "provider default", } -def _install_systemd(home: Path, db_path: str | None, run: RunCommand, start: bool, autonomy_mode: str) -> dict[str, Any]: +def _install_systemd(home: Path, db_path: str | None, run: RunCommand, start: bool, autonomy_mode: str, model: str) -> dict[str, Any]: unit_dir = home / ".config" / "systemd" / "user" logs = home / ".local" / "state" / "secopsai" unit_dir.mkdir(parents=True, exist_ok=True) @@ -135,6 +144,8 @@ def _install_systemd(home: Path, db_path: str | None, run: RunCommand, start: bo "--db-path", str(Path(db_path or soc_store.default_db_path()).expanduser().resolve()), ] + if model: + command.extend(["--model", model]) lines = [ "[Unit]", "Description=SecOpsAI local Codex intelligence bridge", @@ -168,6 +179,7 @@ def _install_systemd(home: Path, db_path: str | None, run: RunCommand, start: bo "logs": [f"journalctl --user -u {SYSTEMD_UNIT}"], "credentials_persisted": False, "autonomy_mode": autonomy_mode, + "model": model or "provider default", } diff --git a/tests/test_intelligence.py b/tests/test_intelligence.py index 1382d7d..825cc27 100644 --- a/tests/test_intelligence.py +++ b/tests/test_intelligence.py @@ -218,6 +218,7 @@ def runner(command): platform_name="darwin", runner=runner, autonomy_mode="agent_review", + model="kimi/kimi-k2.7-code-highspeed", ) plist_path = Path(result["path"]) with plist_path.open("rb") as handle: @@ -227,7 +228,9 @@ def runner(command): assert "SECOPSAI_CORE_READ_TOKEN" not in encoded assert "OPENAI_API_KEY" not in encoded assert payload["EnvironmentVariables"]["SECOPSAI_RESEARCH_AUTONOMY_MODE"] == "agent_review" + assert payload["ProgramArguments"][-2:] == ["--model", "kimi/kimi-k2.7-code-highspeed"] assert result["autonomy_mode"] == "agent_review" + assert result["model"] == "kimi/kimi-k2.7-code-highspeed" assert result["credentials_persisted"] is False assert any(command[1] == "bootstrap" for command in calls) @@ -243,6 +246,17 @@ def test_bridge_service_rejects_unknown_autonomy_mode(tmp_path: Path): ) +def test_bridge_service_rejects_unsafe_model_id(tmp_path: Path): + with pytest.raises(ValueError, match="unsupported characters"): + install_service( + db_path=str(tmp_path / "core.db"), + start=False, + home=tmp_path, + platform_name="darwin", + model="kimi/model;curl example.invalid", + ) + + def test_bridge_lists_opencodex_models(monkeypatch): monkeypatch.setattr("secopsai.codex_bridge.shutil.which", lambda value: f"/usr/local/bin/{value}") monkeypatch.setenv("SECOPSAI_BRIDGE_LIVE_MODELS", "true")