"""Local credential protocol checks only; no real credentials or network.""" import os import secrets import shlex import stat import subprocess import tempfile import unittest from pathlib import Path ROOT = Path(__file__).resolve().parents[3] POLICY = ROOT / "harness/operations/repository-cache.md" REWORK = ROOT / "harness/agents/impl/skills/gitea-issue-rework/SKILL.md" HEADING = "### Non-file Git credential helper" class GitCredentialRecipeTests(unittest.TestCase): def recipe(self): policy = POLICY.read_text(encoding="utf-8") self.assertTrue(HEADING in policy, "canonical non-file credential recipe is missing") section = policy.split(HEADING, 1)[1].split("\n## ", 1)[0] block = section.split("```sh\n", 1)[1].split("```", 1)[0] return section, shlex.split(block.replace("\\\n", "")) def test_canonical_nonfile_recipe_preserves_permission_and_delivery_contract(self): section, command = self.recipe() self.assertEqual(command[0], "git") self.assertEqual(command[1:3], ["-c", "credential.helper="]) helper = command[4] self.assertTrue(helper.startswith("credential.helper=!")) self.assertIn("GIT_ROLE_USERNAME", helper) self.assertIn("GIT_ROLE_TOKEN", helper) self.assertIn("credential.interactive=false", command) self.assertIn("remote.origin.mirror=false", command) self.assertEqual(command[-4:], ["push", "--dry-run", "$VERIFIED_REPOSITORY_URL", "$VERIFIED_REFSPEC"]) for required in ("0600", "GIT_TERMINAL_PROMPT=0", "GIT_ASKPASS=/bin/false", "SSH_ASKPASS=/bin/false", "same-UID", "store", "erase", "stdout", "readback"): self.assertIn(required, section) policy = POLICY.read_text(encoding="utf-8") self.assertIn("Lock 파일과 임시 credential helper는 실행 role 소유와 mode `0600`을 요구한다", policy) delivery = REWORK.read_text(encoding="utf-8").split("### 7. 기존 PR branch 전달", 1)[1].split("###", 1)[0] self.assertIn("#non-file-git-credential-helper", delivery) self.assertNotIn("필요한 temporary askpass helper", delivery) def test_nonfile_get_missing_variables_and_store_erase_do_not_persist_credentials(self): _, documented = self.recipe() options = documented[:documented.index("push")] with tempfile.TemporaryDirectory(dir=os.environ.get("TMPDIR", "/opt/data/cache/scratch")) as d: root = Path(d) token = secrets.token_hex(24) env = {"PATH": os.environ["PATH"], "HOME": str(root), "GIT_CONFIG_NOSYSTEM": "1", "GIT_CONFIG_GLOBAL": os.devnull, "GIT_TERMINAL_PROMPT": "0", "GIT_ASKPASS": "/bin/false", "SSH_ASKPASS": "/bin/false", "GIT_ROLE_USERNAME": "gitea-impl-agent", "GIT_ROLE_TOKEN": token} initialized = subprocess.run(["git", "init", "-q"], cwd=root, env=env, capture_output=True, timeout=10) self.assertEqual(initialized.returncode, 0) config = root / ".git/config" before, mode = config.read_bytes(), stat.S_IMODE(config.stat().st_mode) request = "protocol=https\nhost=example.invalid\n\n" def invoke(action, packet=request, child_env=env): # Values travel only in the private child env/protocol, never argv. self.assertFalse(any(token in arg for arg in options)) result = subprocess.run([*options, "credential", action], cwd=root, env=child_env, input=packet, text=True, capture_output=True, timeout=10) self.assertFalse(token in result.stderr, "credential exposed outside private protocol") return result valid = invoke("fill") self.assertEqual(valid.returncode, 0) fields = dict(line.split("=", 1) for line in valid.stdout.splitlines() if "=" in line) self.assertTrue(fields.get("username") == env["GIT_ROLE_USERNAME"], "username binding mismatch") self.assertTrue(fields.get("password") == token, "synthetic credential binding mismatch") for missing in ("GIT_ROLE_TOKEN", "GIT_ROLE_USERNAME"): with self.subTest(missing=missing): incomplete = dict(env) incomplete.pop(missing) rejected = invoke("fill", child_env=incomplete) self.assertNotEqual(rejected.returncode, 0) self.assertTrue(rejected.stdout == "", "failed credential lookup emitted data") packet = ("protocol=https\nhost=example.invalid\nusername=" + env["GIT_ROLE_USERNAME"] + "\npassword=" + token + "\n\n") for action in ("approve", "reject"): with self.subTest(action=action): result = invoke(action, packet=packet) self.assertEqual(result.returncode, 0) self.assertTrue(result.stdout == "", "store/erase emitted credential data") self.assertTrue(config.read_bytes() == before, "Git config changed") self.assertEqual(stat.S_IMODE(config.stat().st_mode), mode) self.assertFalse(token.encode() in config.read_bytes(), "credential persisted") self.assertEqual(sorted(path.name for path in root.iterdir()), [".git"]) def test_mode0600_direct_askpass_fails_without_permission_relaxation(self): with tempfile.TemporaryDirectory(dir=os.environ.get("TMPDIR", "/opt/data/cache/scratch")) as d: root = Path(d) askpass = root / "askpass.sh" askpass.write_text("#!/bin/sh\nexit 0\n") askpass.chmod(0o600) env = {"PATH": os.environ["PATH"], "HOME": str(root), "GIT_CONFIG_NOSYSTEM": "1", "GIT_CONFIG_GLOBAL": os.devnull, "GIT_TERMINAL_PROMPT": "0", "GIT_ASKPASS": str(askpass)} result = subprocess.run(["git", "-c", "credential.helper=", "credential", "fill"], cwd=root, env=env, input="protocol=https\nhost=example.invalid\n\n", text=True, capture_output=True, timeout=10) self.assertEqual(result.returncode, 128) self.assertIn("Permission denied", result.stderr) self.assertTrue(result.stdout == "", "failed credential lookup emitted data") self.assertEqual(stat.S_IMODE(askpass.stat().st_mode), 0o600)