fix(opencode): the auth check is the binary, and create takes the adapter's verdict (Issue #60, Linux item 3)
opencode 2.x needs no login: a fresh HOME runs a free default model and a local provider has no key, while 2.x keeps no auth.json. auth_ok is now "opencode on PATH or under <HOME>/.opencode/bin". `python -m lib_py.agents auth-ok <agent>` exits with an adapter's verdict, and create's opencode branch uses it (same 7 lines) instead of its own auth.json/key test and `opencode --version`, which wrote a line to the real opencode log. test_a4's opencode block now pins the binary rule (decision D1, required by the brief's item 3 and by both reviewers). Other agents are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7803d47026
commit
d17cd80bd9
5 files changed
+100
-17
No files matched your search
@@ -117,6 +117,15 @@ def main():
|
|||||||
print(adapter.resume_spec(binary, uuid, materialized=mat))
|
print(adapter.resume_spec(binary, uuid, materialized=mat))
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
|
elif cmd == 'auth-ok':
|
||||||
|
# auth-ok <agent>: exit 0 when the adapter's auth_ok() holds, else 1.
|
||||||
|
agent_name = sys.argv[2] if len(sys.argv) > 2 else ''
|
||||||
|
adapter = get_adapter(agent_name)
|
||||||
|
if not adapter:
|
||||||
|
print(f"ERROR: unknown agent {agent_name!r}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
sys.exit(0 if adapter.auth_ok() else 1)
|
||||||
|
|
||||||
elif cmd == 'exit-key':
|
elif cmd == 'exit-key':
|
||||||
agent_name = sys.argv[2] if len(sys.argv) > 2 else ''
|
agent_name = sys.argv[2] if len(sys.argv) > 2 else ''
|
||||||
adapter = get_adapter(agent_name)
|
adapter = get_adapter(agent_name)
|
||||||
|
|||||||
@@ -39,13 +39,6 @@ def _ro_connect(db: str):
|
|||||||
# (binary, HOME, XDG_DATA_HOME) -> the db `debug paths` printed for them.
|
# (binary, HOME, XDG_DATA_HOME) -> the db `debug paths` printed for them.
|
||||||
_DB_PATHS = {}
|
_DB_PATHS = {}
|
||||||
|
|
||||||
_PROVIDER_ENV_VARS = (
|
|
||||||
'ANTHROPIC_API_KEY',
|
|
||||||
'OPENAI_API_KEY',
|
|
||||||
'GEMINI_API_KEY',
|
|
||||||
'GROQ_API_KEY'
|
|
||||||
)
|
|
||||||
|
|
||||||
class OpenCodeAgentAdapter(BaseAgentAdapter):
|
class OpenCodeAgentAdapter(BaseAgentAdapter):
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
@@ -322,11 +315,10 @@ class OpenCodeAgentAdapter(BaseAgentAdapter):
|
|||||||
return f"{binary} --auto --standalone"
|
return f"{binary} --auto --standalone"
|
||||||
|
|
||||||
def auth_ok(self, run_cmd: Optional[Any] = None) -> bool:
|
def auth_ok(self, run_cmd: Optional[Any] = None) -> bool:
|
||||||
if any(os.environ.get(v) for v in _PROVIDER_ENV_VARS):
|
# opencode 2.x needs no login: a fresh HOME runs a free default model
|
||||||
return True
|
# and local providers have no key, so the binary is the whole check.
|
||||||
home = os.environ.get("HOME_DIR") or os.environ.get("HOME") or os.path.expanduser("~")
|
home = os.environ.get("HOME_DIR") or os.environ.get("HOME") or os.path.expanduser("~")
|
||||||
auth_file = f"{home}/.local/share/opencode/auth.json"
|
return bool(shutil.which("opencode")) or os.path.exists(f"{home}/.opencode/bin/opencode")
|
||||||
return os.path.exists(auth_file)
|
|
||||||
|
|
||||||
def discover(self, ctx: DiscoveryContext) -> list:
|
def discover(self, ctx: DiscoveryContext) -> list:
|
||||||
db = self._resolve_db(ctx)
|
db = self._resolve_db(ctx)
|
||||||
|
|||||||
@@ -143,9 +143,9 @@ elif [ "$AGENT" = "grok" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
elif [ "$AGENT" = "opencode" ]; then
|
elif [ "$AGENT" = "opencode" ]; then
|
||||||
if [ -f "$HOME/.local/share/opencode/auth.json" ] || [ -n "${ANTHROPIC_API_KEY:-}" ] || [ -n "${OPENAI_API_KEY:-}" ] || [ -n "${GEMINI_API_KEY:-}" ] || [ -n "${GROQ_API_KEY:-}" ]; then
|
# opencode needs no login (2.x: a free default model, keyless local
|
||||||
true
|
# providers); the adapter's auth_ok is the one verdict.
|
||||||
elif ! opencode --version >/dev/null 2>&1; then
|
if ! "$(_delegate_py_bin)" -m lib_py.agents auth-ok opencode >/dev/null 2>&1; then
|
||||||
echo "ERROR: opencode is not functional or configured." >&2
|
echo "ERROR: opencode is not functional or configured." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -311,16 +311,24 @@ def test_adapter_auth_ok(tmp_path, monkeypatch):
|
|||||||
# Hermes always True
|
# Hermes always True
|
||||||
assert get_adapter('hermes').auth_ok() is True
|
assert get_adapter('hermes').auth_ok() is True
|
||||||
|
|
||||||
# OpenCode auth check (provider env var or auth.json file)
|
# OpenCode auth check: the binary alone (2.x needs no login, Issue #60);
|
||||||
|
# keys and auth.json do not count
|
||||||
opencode = get_adapter('opencode')
|
opencode = get_adapter('opencode')
|
||||||
|
path = os.environ["PATH"]
|
||||||
|
monkeypatch.setenv("PATH", str(tmp_path / "empty-bin"))
|
||||||
assert opencode.auth_ok() is False
|
assert opencode.auth_ok() is False
|
||||||
monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key")
|
monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key")
|
||||||
assert opencode.auth_ok() is True
|
|
||||||
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
|
|
||||||
opencode_auth = tmp_path / ".local" / "share" / "opencode" / "auth.json"
|
opencode_auth = tmp_path / ".local" / "share" / "opencode" / "auth.json"
|
||||||
opencode_auth.parent.mkdir(parents=True, exist_ok=True)
|
opencode_auth.parent.mkdir(parents=True, exist_ok=True)
|
||||||
opencode_auth.write_text("{}")
|
opencode_auth.write_text("{}")
|
||||||
|
assert opencode.auth_ok() is False
|
||||||
|
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
|
||||||
|
opencode_bin = tmp_path / ".opencode" / "bin" / "opencode"
|
||||||
|
opencode_bin.parent.mkdir(parents=True)
|
||||||
|
opencode_bin.write_text("#!/bin/sh\n")
|
||||||
|
opencode_bin.chmod(0o755)
|
||||||
assert opencode.auth_ok() is True
|
assert opencode.auth_ok() is True
|
||||||
|
monkeypatch.setenv("PATH", path)
|
||||||
|
|
||||||
# Codex auth check (env var, auth.json file, or CLI)
|
# Codex auth check (env var, auth.json file, or CLI)
|
||||||
codex = get_adapter('codex')
|
codex = get_adapter('codex')
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
(binary, HOME, XDG_DATA_HOME) in a process.
|
(binary, HOME, XDG_DATA_HOME) in a process.
|
||||||
2. A graceful stop whose `/exit` worked says so: the agent exits instead of
|
2. A graceful stop whose `/exit` worked says so: the agent exits instead of
|
||||||
turning `working`, so the delivery is unconfirmed, and no kill step ran.
|
turning `working`, so the delivery is unconfirmed, and no kill step ran.
|
||||||
|
3. opencode's auth check is the binary alone (2.x needs no login), and create
|
||||||
|
takes the adapter's verdict instead of running `opencode --version`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -279,3 +281,75 @@ def test_the_graceful_stop_reports_what_happened(mam_sandbox, mock_herdr, mock_a
|
|||||||
else:
|
else:
|
||||||
# The kill step runs, so it is named.
|
# The kill step runs, so it is named.
|
||||||
assert "still alive → kill-session" in out, out
|
assert "still alive → kill-session" in out, out
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 3. opencode's auth check is the binary
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def _executable(path: Path) -> Path:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text("#!/bin/sh\n")
|
||||||
|
path.chmod(0o755)
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def bare_home(tmp_path, monkeypatch):
|
||||||
|
"""An empty HOME_DIR and a PATH with no opencode."""
|
||||||
|
monkeypatch.setenv("HOME_DIR", str(tmp_path / "home"))
|
||||||
|
monkeypatch.setenv("PATH", str(tmp_path / "empty-bin"))
|
||||||
|
for var in ("ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY", "GROQ_API_KEY"):
|
||||||
|
monkeypatch.delenv(var, raising=False)
|
||||||
|
return tmp_path
|
||||||
|
|
||||||
|
|
||||||
|
def test_keys_and_auth_json_without_the_binary_are_not_enough(bare_home, monkeypatch):
|
||||||
|
opencode = get_adapter("opencode")
|
||||||
|
assert opencode.auth_ok() is False
|
||||||
|
monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key")
|
||||||
|
assert opencode.auth_ok() is False
|
||||||
|
auth = bare_home / "home" / ".local" / "share" / "opencode" / "auth.json"
|
||||||
|
auth.parent.mkdir(parents=True)
|
||||||
|
auth.write_text("{}")
|
||||||
|
assert opencode.auth_ok() is False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("where", ["path", "home"])
|
||||||
|
def test_the_binary_alone_is_enough(bare_home, monkeypatch, where):
|
||||||
|
if where == "path":
|
||||||
|
_executable(bare_home / "bin" / "opencode")
|
||||||
|
monkeypatch.setenv("PATH", str(bare_home / "bin"))
|
||||||
|
else:
|
||||||
|
_executable(bare_home / "home" / ".opencode" / "bin" / "opencode")
|
||||||
|
assert get_adapter("opencode").auth_ok() is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_auth_ok_command_exits_with_the_verdict(bare_home):
|
||||||
|
def rc(*args):
|
||||||
|
env = dict(os.environ, PYTHONPATH=str(SKILLS))
|
||||||
|
return subprocess.run([sys.executable, "-m", "lib_py.agents", "auth-ok", *args],
|
||||||
|
capture_output=True, text=True, env=env).returncode
|
||||||
|
|
||||||
|
assert rc("opencode") == 1
|
||||||
|
_executable(bare_home / "home" / ".opencode" / "bin" / "opencode")
|
||||||
|
assert rc("opencode") == 0
|
||||||
|
assert rc("no-such-agent") == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_decides_opencode_auth_without_running_it(mam_sandbox, mock_herdr, mock_agents, monkeypatch):
|
||||||
|
# Every opencode call create makes is logged; --version was the old check.
|
||||||
|
log = mam_sandbox / "opencode-calls.log"
|
||||||
|
wrap = mam_sandbox / "logbin" / "opencode"
|
||||||
|
wrap.parent.mkdir()
|
||||||
|
wrap.write_text(f'#!/bin/sh\necho "$*" >> "{log}"\nexec "{shutil.which("opencode")}" "$@"\n')
|
||||||
|
wrap.chmod(0o755)
|
||||||
|
monkeypatch.setenv("PATH", f"{wrap.parent}:{os.environ['PATH']}")
|
||||||
|
for var in ("ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY", "GROQ_API_KEY"):
|
||||||
|
monkeypatch.delenv(var, raising=False)
|
||||||
|
create = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-create" / "scripts" / "create_session.sh"
|
||||||
|
res = subprocess.run(["bash", str(create), "--workspace", str(mam_sandbox), "--agent", "opencode",
|
||||||
|
"--role", "creator", "--no-onboard"],
|
||||||
|
capture_output=True, text=True, cwd=str(mam_sandbox), timeout=300)
|
||||||
|
assert res.returncode == 0, res.stdout + res.stderr
|
||||||
|
calls = log.read_text().splitlines() if log.exists() else []
|
||||||
|
assert "--version" not in calls, calls
|
||||||
Reference in new issue
Block a user