Files
handler/tests/test_credentials_hosts.py
Claude 71a7550f48 feat: git servers own credentials, one-line project adds with auto-clone, and scheduled agents
Git servers (forge_hosts) become full credential owners:
- an encrypted forge token (Fernet, HANDLER_SECRET_KEY) stored per server and
  never returned by the API (has_token flag only); used automatically by every
  project on that host and addressable as db:host:<hostname> — the reserved
  db: credential scheme is now live
- a per-server ed25519 SSH deploy key: generated server-side, public half
  shown in the dashboard to paste into the forge, private half encrypted at
  rest and materialized 0600 only in the control container (GIT_SSH_COMMAND /
  core.sshCommand)

Project registration gets a git-server mode: pick a registered server, type
owner/name, and the API derives the remote (ssh when the server has a deploy
key, https otherwise), computes root_dir under PROJECTS_ROOT, and enqueues a
new 'sync' command the worker executes (clone, or ff-only pull). Spawn always
pulls first, so runs start from the remote's latest state; POST /projects/:p/sync
and 'handler sync' re-pull on demand.

Schedules: recurring agent spawns (prefix, prompt, interval, role). The worker
fires due schedules as ordinary queued spawn commands with timestamped agent
names, so runs are fresh stateless agents and appear in the Activity audit
trail; missed intervals collapse into one catch-up run.

Dashboard: Git Servers pane shows the SSH public key (copy button) and takes a
write-only token; Repositories gains the server-first add form and a Pull now
button; new Schedules pane. Rebuilt static export. Also restores the missing
frontend/lib (api client + format helpers) the components import.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XY1tEhQZXHZ5wci7dLc7rM
2026-07-10 19:17:12 +00:00

54 lines
2.1 KiB
Python

"""Host-aware credential resolution: the forge_hosts registry overrides the built-in map
when a connection is supplied, and behaviour is unchanged when it isn't (regression)."""
from __future__ import annotations
from handler.control import credentials
from handler.db import repository as repo
from handler.db.engine import get_engine
def test_registry_host_overrides_builtin_env_var(env):
with get_engine().begin() as conn:
repo.create_host(conn, "github.com", "github", token_env_var="CORP_GH_TOKEN")
e = credentials.credential_env("tok", "https://github.com/me/repo.git", conn)
# Registry wins over the built-in GITHUB_TOKEN mapping.
assert e["CORP_GH_TOKEN"] == "tok"
assert e["FORGE_TOKEN"] == "tok"
assert "GITHUB_TOKEN" not in e
def test_registry_enables_self_hosted_host(env):
with get_engine().begin() as conn:
repo.create_host(conn, "git.corp.internal", "gitea", token_env_var="CORP_TOKEN")
e = credentials.credential_env("tok", "https://git.corp.internal/me/repo.git", conn)
assert e["CORP_TOKEN"] == "tok"
def test_fallback_to_builtin_when_no_row(env):
with get_engine().begin() as conn:
e = credentials.credential_env("tok", "https://github.com/me/repo.git", conn)
# No forge_hosts row -> built-in map still applies.
assert e["GITHUB_TOKEN"] == "tok"
def test_no_conn_behaviour_is_unchanged():
# The 2-arg form (no registry) must match the pre-existing built-in behaviour.
e = credentials.credential_env("tok", "https://github.com/me/repo.git")
assert e == {"FORGE_TOKEN": "tok", "GITHUB_TOKEN": "tok"}
def test_credential_config_uses_registry_base_url(env):
with get_engine().begin() as conn:
repo.create_host(conn, "git.corp", "gitea", base_url="https://git.corp:8443")
key, value = credentials.git_credential_config("https://git.corp/me/repo.git", conn)
assert key == "credential.https://git.corp:8443.helper"
assert "$FORGE_TOKEN" in value
def test_db_scheme_requires_host_form():
import pytest
with pytest.raises(credentials.CredentialError, match="db:host:<hostname>"):
credentials.resolve("db:42")