feat(providers): implement Packet S4 agent-harness-setup CLI wrapper and provider handoff

This commit is contained in:
rob
2026-09-11 22:31:08 -03:00
parent 1e698996b2
commit 60cd44bcfb
3 changed files with 442 additions and 128 deletions
+2
View File
@@ -190,6 +190,8 @@ def main():
# providers install # providers install
p_prov_install = providers_sub.add_parser("install", help="Interactive guide to install AI providers") p_prov_install = providers_sub.add_parser("install", help="Interactive guide to install AI providers")
p_prov_install.add_argument("--harness", help="Specific harness ID to install")
p_prov_install.add_argument("--dry-run", action="store_true", help="Preview installation without executing")
p_prov_install.set_defaults(func=cmd_providers) p_prov_install.set_defaults(func=cmd_providers)
# providers discover # providers discover
+221 -125
View File
@@ -1,12 +1,15 @@
"""Provider management commands.""" import json
import os import os
import shlex import shlex
import shutil import shutil
import subprocess import subprocess
import sys
import tempfile
import uuid
from pathlib import Path from pathlib import Path
from ..providers import ( from ..providers import (
KNOWN_PROVIDER_CLIS,
PRESET_CHAINS, PRESET_CHAINS,
Provider, Provider,
add_provider, add_provider,
@@ -121,142 +124,235 @@ def cmd_providers(args):
return 0 return 0
def _cmd_providers_install(args): def _find_setup_helper() -> list[str] | None:
"""Interactive guide to install AI providers.""" """Locate the agent-harness-setup CLI executable or python module."""
print("=" * 60) # 1. System/user PATH
print("CmdForge Provider Installation Guide") path_bin = shutil.which("agent-harness-setup")
print("=" * 60) if path_bin:
print() return [path_bin]
# Check what's already installed # 2. Current environment virtualenv bin
providers = load_providers() venv_bin = Path(sys.prefix) / "bin" / "agent-harness-setup"
installed_groups = set() if venv_bin.is_file() and os.access(venv_bin, os.X_OK):
for p in providers: return [str(venv_bin)]
if p.name.lower() == "mock" or p.type == "api":
continue
available, _ = _provider_status(p)
if available:
# Find which group this belongs to
for group, info in PROVIDER_INSTALL_INFO.items():
if p.name in info.get("variants", []):
installed_groups.add(group)
# Show available provider groups # 3. User local bin
print("Available AI Provider Groups:\n") local_bin = Path.home() / ".local" / "bin" / "agent-harness-setup"
options = [] if local_bin.is_file() and os.access(local_bin, os.X_OK):
for i, (key, info) in enumerate(PROVIDER_INSTALL_INFO.items(), 1): return [str(local_bin)]
status = "[INSTALLED]" if key in installed_groups else ""
print(f" {i}. {info['group']} {status}")
print(f" Cost: {info['cost']}")
print(f" Models: {', '.join(info['variants']) if info['variants'] else 'Custom'}")
print()
options.append(key)
print(" 0. Cancel") # 4. Workspace sibling repository
print() workspace_candidates = [
Path(__file__).resolve().parents[4] / "agent-harness-setup" / ".venv" / "bin" / "agent-harness-setup",
try: Path("/home/rob/PycharmProjects/agent-harness-setup/.venv/bin/agent-harness-setup"),
choice = input("Select a provider to install (1-{}, or 0 to cancel): ".format(len(options)))
choice = int(choice)
except (ValueError, EOFError):
print("Cancelled.")
return 0
if choice == 0 or choice > len(options):
print("Cancelled.")
return 0
selected = options[choice - 1]
info = PROVIDER_INSTALL_INFO[selected]
print()
print("=" * 60)
print(f"Installing: {info['group']}")
print("=" * 60)
print()
print(f"Requirements: {info['requires']}")
if info["install_cmd"]:
print(f"Install command: {info['install_cmd']}")
else:
print("Install method: Manual download")
print(f"Post-install: {info['setup']}")
print()
if not info["install_cmd"]:
print("CmdForge cannot install this provider automatically.")
print(info["setup"])
return 0
try:
confirm = input("Run installation command? (y/N): ").strip().lower()
except EOFError:
confirm = "n"
if confirm == "y":
print()
print(f"Running: {info['install_cmd']}")
print("-" * 40)
result = subprocess.run(info['install_cmd'], shell=True)
print("-" * 40)
if result.returncode == 0:
# Refresh PATH to pick up newly installed tools
import os
new_paths = []
# Common install locations that might have been added
potential_paths = [
Path.home() / ".opencode" / "bin", # OpenCode
Path.home() / ".local" / "bin", # pip/pipx installs
Path("/usr/local/bin"), # Ollama, system installs
] ]
for cand in workspace_candidates:
if cand.is_file() and os.access(cand, os.X_OK):
return [str(cand)]
# Also try to get npm global bin path # 5. Check if agent_harness_setup is importable via python
try: try:
npm_result = subprocess.run( import agent_harness_setup # type: ignore # noqa: F401
["npm", "bin", "-g"], return [sys.executable, "-m", "agent_harness_setup"]
capture_output=True, text=True, timeout=5 except ImportError:
)
if npm_result.returncode == 0:
npm_bin = npm_result.stdout.strip()
if npm_bin:
potential_paths.append(Path(npm_bin))
except (subprocess.TimeoutExpired, FileNotFoundError):
pass pass
current_path = os.environ.get("PATH", "") return None
for p in potential_paths:
if p.exists() and str(p) not in current_path:
new_paths.append(str(p))
if new_paths:
os.environ["PATH"] = ":".join(new_paths) + ":" + current_path def _get_provider_defaults_for_harness(harness_id: str, executable_path: str | None = None) -> dict:
"""Derive CmdForge provider configuration defaults for an installed harness."""
harness_map = {
"claude-code": "claude",
"claude": "claude",
"openai-codex": "codex",
"codex": "codex",
"agy": "agy",
"gemini-cli": "agy",
"opencode": "opencode",
"crush": "crush",
"ollama": "ollama",
}
cli_key = harness_map.get(harness_id, harness_id)
if cli_key in KNOWN_PROVIDER_CLIS:
base = dict(KNOWN_PROVIDER_CLIS[cli_key])
if executable_path:
old_cmd = base.get("command", "")
parts = shlex.split(old_cmd)
if parts:
base["command"] = " ".join([executable_path] + parts[1:])
else:
base["command"] = executable_path
return base
cmd = executable_path if executable_path else harness_id
return {
"name": harness_id,
"command": cmd,
"description": f"{harness_id} CLI provider",
"tags": ["cli"],
"locality": "remote",
"capabilities": ["text"],
"data_policy": "public",
}
def _cmd_providers_install(args):
"""Interactive guide to install AI harnesses and configure providers."""
helper_argv = _find_setup_helper()
if not helper_argv:
print("=" * 60)
print("CmdForge Provider Installation")
print("=" * 60)
print()
print("Notice: Agent Harness Setup Helper ('agent-harness-setup') was not")
print("found on your PATH or in active virtual environments.")
print()
print("To install the setup helper, run:")
print(" pip install -e /home/rob/PycharmProjects/agent-harness-setup")
print()
print("Or run the helper directly:")
print(" PYTHONPATH=/home/rob/PycharmProjects/agent-harness-setup/src python3 -m agent_harness_setup")
print()
print("Manual installation instructions are available in documentation:")
print(" https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview")
return 0
# Prepare secure temporary result file and correlation request_id
temp_dir = tempfile.mkdtemp(prefix="cmdforge-harness-setup-")
try:
os.chmod(temp_dir, 0o700)
except OSError:
pass
result_file = Path(temp_dir) / "result.json"
req_id = str(uuid.uuid4())
cmd = list(helper_argv) + ["--result-file", str(result_file), "--request-id", req_id]
harness_arg = getattr(args, "harness", None)
if harness_arg:
cmd.extend(["--harness", harness_arg])
if getattr(args, "dry_run", False):
cmd.append("--dry-run")
try:
proc = subprocess.run(cmd)
helper_exit_code = proc.returncode
except Exception as e:
print(f"\nError executing agent-harness-setup: {e}")
shutil.rmtree(temp_dir, ignore_errors=True)
return 1
# Read and parse completion result
result_data = None
if result_file.is_file():
try:
result_data = json.loads(result_file.read_text(encoding="utf-8"))
except Exception:
result_data = None
# Clean up temporary directory
shutil.rmtree(temp_dir, ignore_errors=True)
# Validate correlation request ID
if result_data is not None and result_data.get("request_id") != req_id:
print("\nWarning: Correlation request ID mismatch in setup result. Rejecting result.")
return 1
# Incoherence handling (Contract §C.5)
if result_data is not None:
status = result_data.get("status")
if helper_exit_code != 0 and status == "success":
print("\nWarning: Helper exited with non-zero code but result reported success. Treating as failure.")
return helper_exit_code
if helper_exit_code == 0 and status in ("failed", "cancelled"):
print("\nWarning: Helper exited with code 0 but result reported failure/cancellation. Treating as failure.")
return 1
# Dry-run completion
if getattr(args, "dry_run", False) or (result_data and result_data.get("status") == "dry_run"):
return 0
# Cancellation
if helper_exit_code in (10, 130, 143) or (result_data and result_data.get("status") == "cancelled"):
print("\nSetup cancelled. No provider configuration was changed.")
return 0
# Failure
if helper_exit_code != 0 or (result_data and result_data.get("status") == "failed"):
diag = result_data.get("error_diagnostics") if result_data else None
if diag:
print(f"\nSetup failed: {diag}")
return helper_exit_code if helper_exit_code != 0 else 1
# Verified Success
if helper_exit_code == 0 and result_data and result_data.get("status") == "success":
harness_id = result_data.get("harness_id") or "unknown"
verification = result_data.get("verification") or {}
executable_path = verification.get("executable_path")
path_updates = result_data.get("path_updates") or []
# Advisory PATH updates
if path_updates:
cur_path = os.environ.get("PATH", "")
new_dirs = [
str(Path(os.path.expanduser(p)))
for p in path_updates
if Path(os.path.expanduser(p)).is_dir() and str(Path(os.path.expanduser(p))) not in cur_path
]
if new_dirs:
os.environ["PATH"] = ":".join(new_dirs) + ":" + cur_path
provider_defaults = _get_provider_defaults_for_harness(harness_id, executable_path)
provider_name = provider_defaults["name"]
print() print()
print("Installation completed!") print("=" * 60)
print(f"Harness Setup Verified: {harness_id}")
print("=" * 60)
if executable_path:
print(f"Verified executable: {executable_path}")
if result_data.get("manual_steps_remaining"):
print("\nRemaining setup steps:")
for step in result_data["manual_steps_remaining"]:
print(f" * {step}")
existing = get_provider(provider_name)
if existing:
print(f"\nProvider '{provider_name}' is already configured in ~/.cmdforge/providers.yaml.")
print(f"Command: {existing.command}")
print(f"\nTest with: cmdforge providers test {provider_name}")
return 0
# Interactive confirmation prompt for provider configuration
print() print()
print("IMPORTANT: Refresh your shell PATH before continuing:") try:
print(" source ~/.bashrc") prompt_resp = input(
print() f"Would you like to register '{provider_name}' as a model provider in ~/.cmdforge/providers.yaml? (Y/n): "
print(f"Next steps:") ).strip().lower()
print(f" 1. source ~/.bashrc (required!)") except (EOFError, KeyboardInterrupt):
print(f" 2. {info['setup']}") prompt_resp = "n"
if info.get('post_install_note'):
print(f" 3. {info['post_install_note']}") if prompt_resp in ("", "y", "yes"):
print(f" 4. Test with: cmdforge providers test {selected}") new_prov = Provider(
name=provider_name,
command=provider_defaults["command"],
description=provider_defaults.get("description", ""),
tags=provider_defaults.get("tags", []),
locality=provider_defaults.get("locality", "remote"),
capabilities=provider_defaults.get("capabilities", ["text"]),
data_policy=provider_defaults.get("data_policy", "public"),
)
add_provider(new_prov)
print(f"\nProvider '{provider_name}' successfully added to ~/.cmdforge/providers.yaml.")
print(f"Command: {new_prov.command}")
print(f"\nTest with: cmdforge providers test {provider_name}")
else: else:
print(f" 3. Test with: cmdforge providers test {info['variants'][0] if info['variants'] else selected}") print("\nSkipped provider registration. You can add it later with:")
else: print(f" cmdforge providers add {provider_name} '{provider_defaults['command']}'")
print()
print(f"Installation failed (exit code {result.returncode})") return 0
print("Try running the command manually:")
print(f" {info['install_cmd']}")
else:
print()
print("To install manually, run:")
print(f" {info['install_cmd']}")
print()
print(f"Then: {info['setup']}")
return 0 return 0
+216
View File
@@ -0,0 +1,216 @@
"""Tests for Packet S4: CmdForge CLI integration with agent-harness-setup."""
import json
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
import pytest
from cmdforge.cli import main
from cmdforge.cli.provider_commands import _find_setup_helper, _get_provider_defaults_for_harness
from cmdforge.providers import Provider, get_provider, load_providers, save_providers
@pytest.fixture
def temp_providers_file(tmp_path):
providers_file = tmp_path / ".cmdforge" / "providers.yaml"
with patch("cmdforge.providers.PROVIDERS_FILE", providers_file):
save_providers([])
yield providers_file
def _mock_helper_run(status: str, harness_id: str = "claude-code", exit_code: int = 0, diagnostics: str | None = None, executable_path: str = "/usr/local/bin/claude"):
"""Create a mock subprocess.run that writes a valid completion result to --result-file."""
def _run(cmd, *args, **kwargs):
# Extract --result-file and --request-id from cmd
res_idx = cmd.index("--result-file")
result_file_path = Path(cmd[res_idx + 1])
req_idx = cmd.index("--request-id")
request_id = cmd[req_idx + 1]
data = {
"schema_version": 1,
"helper_version": "0.1.0",
"request_id": request_id,
"timestamp": "2026-09-11T22:00:00Z",
"status": status,
"harness_id": harness_id,
"recipe_id": harness_id,
"recipe_version": "2026-09-10",
"install_method": "argv",
"installer_exit_code": 0 if status == "success" else None,
"verification": {
"verified": True,
"executable_path": executable_path,
"version_output": f"{harness_id} 1.0.0",
} if status == "success" else None,
"path_updates": ["/usr/local/bin"],
"auth_status": "setup_required",
"manual_steps_remaining": ["Run 'claude' to complete browser sign-in."],
"partial_changes": {
"observed_created": [executable_path] if status == "success" else [],
"observed_modified": [],
"unknown_changes": True,
},
"error_diagnostics": diagnostics,
}
result_file_path.write_text(json.dumps(data), encoding="utf-8")
return SimpleNamespace(returncode=exit_code)
return _run
def test_helper_discovery_finds_executable():
"""Verify _find_setup_helper locates agent-harness-setup."""
with patch("shutil.which", return_value="/usr/local/bin/agent-harness-setup"):
helper = _find_setup_helper()
assert helper == ["/usr/local/bin/agent-harness-setup"]
def test_get_provider_defaults_for_claude():
"""Verify provider defaults derived for claude-code."""
defaults = _get_provider_defaults_for_harness("claude-code", "/custom/bin/claude")
assert defaults["name"] == "claude"
assert defaults["command"] == "/custom/bin/claude -p"
assert "Claude" in defaults["description"]
def test_providers_install_verified_success_registers_provider(temp_providers_file, capsys):
"""Test verified installation prompts and registers provider in providers.yaml."""
with (
patch("cmdforge.cli.provider_commands._find_setup_helper", return_value=["agent-harness-setup"]),
patch("subprocess.run", side_effect=_mock_helper_run("success", "claude-code", 0)) as mock_run,
patch("builtins.input", return_value="y"),
patch("sys.argv", ["cmdforge", "providers", "install", "--harness", "claude-code"]),
):
exit_code = main()
assert exit_code == 0
captured = capsys.readouterr()
assert "Harness Setup Verified: claude-code" in captured.out
assert "Provider 'claude' successfully added" in captured.out
# Verify provider stored in providers.yaml
provider = get_provider("claude")
assert provider is not None
assert provider.command == "/usr/local/bin/claude -p"
assert "code" in provider.tags
# Verify subprocess invocation arguments
call_args = mock_run.call_args[0][0]
assert call_args[0] == "agent-harness-setup"
assert "--result-file" in call_args
assert "--request-id" in call_args
assert "--harness" in call_args
assert "claude-code" in call_args
def test_providers_install_declined_registration(temp_providers_file, capsys):
"""Test declining the provider registration prompt leaves providers.yaml unchanged."""
with (
patch("cmdforge.cli.provider_commands._find_setup_helper", return_value=["agent-harness-setup"]),
patch("subprocess.run", side_effect=_mock_helper_run("success", "claude-code", 0)),
patch("builtins.input", return_value="n"),
patch("sys.argv", ["cmdforge", "providers", "install"]),
):
exit_code = main()
assert exit_code == 0
captured = capsys.readouterr()
assert "Harness Setup Verified: claude-code" in captured.out
assert "Skipped provider registration" in captured.out
assert get_provider("claude") is None
def test_providers_install_already_configured(temp_providers_file, capsys):
"""Test that existing provider is recognized and not re-registered."""
save_providers([Provider("claude", "existing-claude -p")])
with (
patch("cmdforge.cli.provider_commands._find_setup_helper", return_value=["agent-harness-setup"]),
patch("subprocess.run", side_effect=_mock_helper_run("success", "claude-code", 0)),
patch("sys.argv", ["cmdforge", "providers", "install"]),
):
exit_code = main()
assert exit_code == 0
captured = capsys.readouterr()
assert "already configured in ~/.cmdforge/providers.yaml" in captured.out
assert get_provider("claude").command == "existing-claude -p"
def test_providers_install_cancelled(temp_providers_file, capsys):
"""Test cancellation exits cleanly without changing providers."""
with (
patch("cmdforge.cli.provider_commands._find_setup_helper", return_value=["agent-harness-setup"]),
patch("subprocess.run", side_effect=_mock_helper_run("cancelled", "claude-code", 10)),
patch("sys.argv", ["cmdforge", "providers", "install"]),
):
exit_code = main()
assert exit_code == 0
captured = capsys.readouterr()
assert "Setup cancelled. No provider configuration was changed." in captured.out
assert get_provider("claude") is None
def test_providers_install_execution_failure(temp_providers_file, capsys):
"""Test helper failure reports error diagnostics and does not register provider."""
with (
patch("cmdforge.cli.provider_commands._find_setup_helper", return_value=["agent-harness-setup"]),
patch("subprocess.run", side_effect=_mock_helper_run("failed", "claude-code", 1, "Missing node.js")),
patch("sys.argv", ["cmdforge", "providers", "install"]),
):
exit_code = main()
assert exit_code == 1
captured = capsys.readouterr()
assert "Setup failed: Missing node.js" in captured.out
assert get_provider("claude") is None
def test_providers_install_incoherent_result_rejected(temp_providers_file, capsys):
"""Test exit code 1 with status 'success' is rejected as incoherent."""
with (
patch("cmdforge.cli.provider_commands._find_setup_helper", return_value=["agent-harness-setup"]),
patch("subprocess.run", side_effect=_mock_helper_run("success", "claude-code", 1)),
patch("sys.argv", ["cmdforge", "providers", "install"]),
):
exit_code = main()
assert exit_code == 1
captured = capsys.readouterr()
assert "Treating as failure" in captured.out
assert get_provider("claude") is None
def test_providers_install_helper_not_found(temp_providers_file, capsys):
"""Test that missing helper prints actionable instructions without crashing."""
with (
patch("cmdforge.cli.provider_commands._find_setup_helper", return_value=None),
patch("sys.argv", ["cmdforge", "providers", "install"]),
):
exit_code = main()
assert exit_code == 0
captured = capsys.readouterr()
assert "agent-harness-setup" in captured.out
assert "pip install" in captured.out
assert "PYTHONPATH=" in captured.out
def test_providers_install_dry_run(temp_providers_file, capsys):
"""Test --dry-run passes flag to helper and makes no provider changes."""
with (
patch("cmdforge.cli.provider_commands._find_setup_helper", return_value=["agent-harness-setup"]),
patch("subprocess.run", side_effect=_mock_helper_run("dry_run", "claude-code", 0)) as mock_run,
patch("sys.argv", ["cmdforge", "providers", "install", "--dry-run"]),
):
exit_code = main()
assert exit_code == 0
call_args = mock_run.call_args[0][0]
assert "--dry-run" in call_args
assert get_provider("claude") is None