Skip to content
Merged
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
2 changes: 2 additions & 0 deletions docs/implementation-checkpoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 5 additions & 1 deletion docs/research-automation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down
6 changes: 6 additions & 0 deletions secopsai/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
20 changes: 16 additions & 4 deletions secopsai/codex_bridge_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import os
import platform
import plistlib
import re
import shlex
import subprocess
import sys
Expand All @@ -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(
Expand All @@ -25,17 +27,21 @@ 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()
run = runner or _run
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}
Expand Down Expand Up @@ -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)
Expand All @@ -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"),
Expand Down Expand Up @@ -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)
Expand All @@ -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",
Expand Down Expand Up @@ -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",
}


Expand Down
14 changes: 14 additions & 0 deletions tests/test_intelligence.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)

Expand All @@ -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")
Expand Down
Loading