mirror of
https://github.com/0xWheatyz/handler.git
synced 2026-08-30 18:56:26 +00:00
6fb26115ce
Phase 2 configures forge for the agents (operator only sets a credential_ref + optional version pin) and lets them drive a junior→senior→deploy workflow: - Credential resolution/injection (control/credentials.py): credential_ref pointers (env:/file:/cmd:) resolved only at spawn, injected as FORGE_TOKEN + host var, with a forge-host-scoped git credential helper reading the token from env (never on disk / in the DB). Resolution is a fail-fast spawn gate. - Role-based forge skills committed into the managed repo (control/skills_gen.py, `handler forge-init`): forge-junior/senior/deploy + a workflow overview. - Hard approval gate (hooks/gate.py, approvals table, migration 0002): merge/deploy — and direct pushes to protected branches — are denied unless a DIFFERENT agent has an `approved` record for the branch, pinned to the reviewed commit (approved_sha). Senior records verdicts via `handler approve`/`reject`. - forge/git seams (control/forge.py, control/gitops.py) matching the Phase 1 seam pattern. - CI status poller (control/poller.py, `handler poll-ci [--watch]`) backfilling ci_status/ci_checked_at via `forge ci list`. - Fix: migrations/env.py commits explicitly after run_migrations — pysqlite on Py 3.12+ was rolling back the final migration's DDL + alembic_version stamp (latent in Phase 1). Reviewed via a separate code-reviewer pass; gate-bypass and credential-scoping findings addressed. 106 tests, ruff clean, verified end-to-end against real git + migrations.
105 lines
3.5 KiB
Python
105 lines
3.5 KiB
Python
"""Credential resolution + env/helper derivation (README 3.7)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from handler.control import credentials
|
|
|
|
|
|
def test_resolve_none_returns_none():
|
|
assert credentials.resolve(None) is None
|
|
assert credentials.resolve("") is None
|
|
|
|
|
|
def test_resolve_env(monkeypatch):
|
|
monkeypatch.setenv("MY_TOKEN", "secret-value")
|
|
assert credentials.resolve("env:MY_TOKEN") == "secret-value"
|
|
|
|
|
|
def test_resolve_env_missing_raises(monkeypatch):
|
|
monkeypatch.delenv("NOPE", raising=False)
|
|
with pytest.raises(credentials.CredentialError, match="not set"):
|
|
credentials.resolve("env:NOPE")
|
|
|
|
|
|
def test_resolve_file(tmp_path):
|
|
f = tmp_path / "tok"
|
|
f.write_text(" file-secret\n")
|
|
assert credentials.resolve(f"file:{f}") == "file-secret"
|
|
|
|
|
|
def test_resolve_file_missing_raises(tmp_path):
|
|
with pytest.raises(credentials.CredentialError, match="unreadable"):
|
|
credentials.resolve(f"file:{tmp_path / 'absent'}")
|
|
|
|
|
|
def test_resolve_cmd():
|
|
assert credentials.resolve("cmd:printf hunter2") == "hunter2"
|
|
|
|
|
|
def test_resolve_cmd_failure_raises():
|
|
with pytest.raises(credentials.CredentialError, match="exited"):
|
|
credentials.resolve("cmd:false")
|
|
|
|
|
|
def test_resolve_unknown_scheme_raises():
|
|
with pytest.raises(credentials.CredentialError, match="unknown scheme"):
|
|
credentials.resolve("vault:secret/x")
|
|
|
|
|
|
def test_resolve_empty_value_raises():
|
|
with pytest.raises(credentials.CredentialError, match="no value"):
|
|
credentials.resolve("env:")
|
|
|
|
|
|
def test_credential_env_always_sets_forge_token():
|
|
env = credentials.credential_env("tok", None)
|
|
assert env == {"FORGE_TOKEN": "tok"}
|
|
|
|
|
|
def test_credential_env_adds_host_specific_var():
|
|
gh = credentials.credential_env("tok", "https://github.com/me/repo.git")
|
|
assert gh["GITHUB_TOKEN"] == "tok" and gh["FORGE_TOKEN"] == "tok"
|
|
gitea = credentials.credential_env("tok", "https://gitea.example.com/me/repo.git")
|
|
assert gitea["GITEA_TOKEN"] == "tok"
|
|
|
|
|
|
def test_credential_env_empty_when_no_token():
|
|
assert credentials.credential_env(None, "https://github.com/x") == {}
|
|
|
|
|
|
def test_git_credential_helper_reads_from_env():
|
|
helper = credentials.git_credential_helper_value()
|
|
# Inline helper hands back the token from $FORGE_TOKEN, never a value on disk.
|
|
assert "$FORGE_TOKEN" in helper
|
|
assert helper.startswith("!")
|
|
|
|
|
|
def test_remote_host_parses_https_and_ssh():
|
|
assert credentials.remote_host("https://github.com/me/repo.git") == "github.com"
|
|
assert credentials.remote_host("git@gitea.example.com:me/repo.git") == "gitea.example.com"
|
|
assert credentials.remote_host(None) is None
|
|
|
|
|
|
def test_host_token_env_not_fooled_by_repo_name():
|
|
# A GitHub repo merely *named* 'gitea' must not be mapped to GITEA_TOKEN.
|
|
env = credentials.credential_env("tok", "https://github.com/me/gitea-mirror.git")
|
|
assert "GITEA_TOKEN" not in env and env["GITHUB_TOKEN"] == "tok"
|
|
|
|
|
|
def test_host_token_env_self_hosted_hint():
|
|
env = credentials.credential_env("tok", "https://gitea.mycorp.internal/me/repo.git")
|
|
assert env["GITEA_TOKEN"] == "tok"
|
|
|
|
|
|
def test_git_credential_config_scoped_to_host():
|
|
key, value = credentials.git_credential_config("https://github.com/me/repo.git")
|
|
assert key == "credential.https://github.com.helper"
|
|
assert "$FORGE_TOKEN" in value
|
|
|
|
|
|
def test_git_credential_config_none_for_ssh():
|
|
assert credentials.git_credential_config("git@github.com:me/repo.git") is None
|
|
assert credentials.git_credential_config(None) is None
|