"""Local credential protocol checks only; no real credentials or network.""" from harness.tests._support.paths import ROOT as SOURCE_ROOT import os import secrets import shlex import stat import subprocess import tempfile import unittest from pathlib import Path from harness.tests._support.contracts import read_contract ROOT = SOURCE_ROOT 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', 'store', 'erase'): self.assertIn(required, section) policy = POLICY.read_text(encoding='utf-8') self.assertIn('mode `0600`', policy) delivery = read_contract(REWORK).split('### 7. 기존 PR branch 전달', 1)[1].split('###', 1)[0] self.assertIn('#non-file-git-credential-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)