Skip to content
Closed
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
10 changes: 8 additions & 2 deletions inkbox_claude/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,12 +194,15 @@ def bootstrap(
return {"status": "error", "error": "API key is required"}
actions: list[str] = []
secrets = [api_key.strip()]
step = "start"
try:
previous = _handle(_env("INKBOX_IDENTITY"))
symbols = _load_inkbox_symbols()
step = "resolve_credentials"
scoped_key, identity = _resolve_credentials(api_key.strip(), handle, base_url, symbols, actions)
secrets.append(scoped_key)
client = symbols["Inkbox"](**inkbox_client_kwargs(scoped_key, base_url))
step = "save_configuration"
_save("INKBOX_API_KEY", scoped_key)
_save("INKBOX_IDENTITY", handle)
if base_url:
Expand All @@ -209,16 +212,19 @@ def bootstrap(
_save("INKBOX_ALLOW_ALL_USERS", "true")
actions.append("saved_claude_configuration")
if voice_ai:
step = "configure_voice"
_configure_voice(identity, client, voice_ai_instructions)
actions.append("configured_voice_ai")
step = "configure_signing"
blocker = _configure_signing(identity, client, rotate_signing_key, not previous or previous == handle, actions)
if blocker:
return {"status": "requires_human", "identity": handle, "actions": actions, "human_actions": [blocker]}
running = False
if start_gateway:
step = "start_gateway"
running = _start_gateway(actions)
if not running:
return {"status": "error", "identity": handle, "actions": actions, "error": "Claude Code gateway did not become ready. Check ~/.inkbox-claude/gateway.log."}
return {"status": "error", "identity": handle, "actions": actions, "failed_step": step, "error": "Claude Code gateway did not become ready. Check ~/.inkbox-claude/gateway.log."}
return {"status": "configured", "identity": handle, "actions": actions, "gateway_running": running}
except Exception as exc:
return {"status": "error", "identity": handle, "actions": actions, "error": _redact(exc, secrets)}
return {"status": "error", "identity": handle, "actions": actions, "failed_step": step, "error": _redact(exc, secrets)}
11 changes: 11 additions & 0 deletions inkbox_claude/doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,17 @@ def run_doctor() -> List[Tuple[str, bool, str]]:
claude_bin or "not on PATH — install Claude Code first",
))

# Presence of the binary isn't enough; the gateway can't answer without auth.
claude_authed = bool(os.environ.get("ANTHROPIC_API_KEY")) or os.path.isfile(
os.path.expanduser("~/.claude/.credentials.json")
)
checks.append((
"claude auth",
claude_authed,
"authenticated" if claude_authed
else "not authenticated — set ANTHROPIC_API_KEY or log in with the Claude Code app/CLI",
))

project_dir = cfg.project_dir
checks.append((
"project dir",
Expand Down
9 changes: 9 additions & 0 deletions tests/test_bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,15 @@ def test_bootstrap_configures_voice_signing_and_gateway(monkeypatch):
assert saved["INKBOX_SIGNING_KEY"] == "signing-secret"


def test_bootstrap_reports_failed_step_on_error(monkeypatch):
identity = Identity()
identity.get_hosted_agent_config = lambda: (_ for _ in ()).throw(RuntimeError("boom"))
install(monkeypatch, identity)
result = subject.bootstrap(identity_handle="helper", api_key="agent-secret", voice_ai=True)
assert result["status"] == "error"
assert result["failed_step"] == "configure_voice"


def test_bootstrap_requires_explicit_signing_rotation(monkeypatch):
identity = Identity(signing=True)
install(monkeypatch, identity)
Expand Down
17 changes: 17 additions & 0 deletions tests/test_doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,23 @@ def test_voice_config_probe_failures_do_not_mark_identity_unreachable(
)


def test_doctor_reports_claude_auth_state(monkeypatch, tmp_path):
monkeypatch.setattr(daemon, "_maybe_load_env_file", lambda: None)
monkeypatch.setattr(doctor.shutil, "which", lambda _name: "/usr/bin/claude")
monkeypatch.setattr(doctor, "read_config", lambda: BridgeConfig(project_dir=str(tmp_path)))

# No ANTHROPIC_API_KEY and no credentials file -> not authenticated.
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
monkeypatch.setattr(doctor.os.path, "isfile", lambda _p: False)
by_name = {name: ok for name, ok, _detail in doctor.run_doctor()}
assert by_name["claude auth"] is False

# Env var present -> authenticated.
monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-test")
by_name = {name: ok for name, ok, _detail in doctor.run_doctor()}
assert by_name["claude auth"] is True


def test_doctor_reports_remote_routing_mismatch(monkeypatch, tmp_path):
identity = types.SimpleNamespace(
mailbox=types.SimpleNamespace(email_address="agent@example.com"),
Expand Down