From 4acc28563c1796ed435d83f8d4adea33509d82ee Mon Sep 17 00:00:00 2001 From: gitea-spec-agent Date: Mon, 12 Oct 2026 06:01:42 +0000 Subject: [PATCH] [verified] Expose canonical broker roles in CLI and docs --- harness/lib/managed_lock_broker.py | 2 +- harness/operations/repository-cache.md | 4 +- .../tests/common/test_managed_lock_broker.py | 57 +++++++++++++++++++ 3 files changed, 61 insertions(+), 2 deletions(-) diff --git a/harness/lib/managed_lock_broker.py b/harness/lib/managed_lock_broker.py index d602ca7..dfd8ae1 100644 --- a/harness/lib/managed_lock_broker.py +++ b/harness/lib/managed_lock_broker.py @@ -473,7 +473,7 @@ def parser() -> argparse.ArgumentParser: p = argparse.ArgumentParser() sub = p.add_subparsers(dest="command", required=True) s = sub.add_parser("serve") - s.add_argument("--root", required=True); s.add_argument("--owner", required=True); s.add_argument("--repository", required=True); s.add_argument("--issue", required=True, type=int); s.add_argument("--role", required=True); s.add_argument("--run-id", required=True); s.add_argument("--handle"); s.add_argument("--socket"); s.add_argument("--ttl-seconds", required=True, type=float); s.add_argument("--acquire-timeout", type=float, default=0.0) + s.add_argument("--root", required=True); s.add_argument("--owner", required=True); s.add_argument("--repository", required=True); s.add_argument("--issue", required=True, type=int); s.add_argument("--role", required=True, choices=sorted(ROLES)); s.add_argument("--run-id", required=True); s.add_argument("--handle"); s.add_argument("--socket"); s.add_argument("--ttl-seconds", required=True, type=float); s.add_argument("--acquire-timeout", type=float, default=0.0) r = sub.add_parser("request") r.add_argument("--handle", required=True); r.add_argument("--action", required=True, choices=["status", "renew", "acquire_repository", "release_repository", "release"]); r.add_argument("--ttl-seconds", type=float) check = sub.add_parser("probe") diff --git a/harness/operations/repository-cache.md b/harness/operations/repository-cache.md index 41d0aa5..0da1666 100644 --- a/harness/operations/repository-cache.md +++ b/harness/operations/repository-cache.md @@ -90,12 +90,14 @@ Broker 응답은 action별 계약이다. 다른 action의 필드가 있다고 ### Canonical command templates +`serve --role`은 기존 `ROLES`의 정확한 값 `gitea-spec-agent`, `gitea-impl-agent`, `gitea-review-agent`만 허용한다. `spec`/`impl`/`review` 축약, 대소문자·공백 변형 및 다른 role은 허용하지 않는다. `serve --help`와 invalid-choice 진단에서 같은 목록을 표시하며 잘못된 값은 runtime 생성 전에 exit 2로 거절한다. 아래는 spec 실행 예제이며 impl/review 호출자는 자신의 정확한 canonical role을 사용한다. Role 문자열은 같은 UID 내 workflow metadata이며 authorization 또는 추가 권한을 부여하지 않는다. 기존 filesystem UID 보안 경계와 constructor 검증은 그대로 유지한다. + ```sh # Runtime paths come only from broker ready JSON; never create them or read the token. python3 /harness/lib/managed_lock_broker.py serve \ --root \ --owner --repository --issue \ - --role --run-id \ + --role gitea-spec-agent --run-id \ --ttl-seconds # Parse only public paths from ready JSON; do not print token-bearing fields. HANDLE= diff --git a/harness/tests/common/test_managed_lock_broker.py b/harness/tests/common/test_managed_lock_broker.py index 09a6aec..92205c0 100644 --- a/harness/tests/common/test_managed_lock_broker.py +++ b/harness/tests/common/test_managed_lock_broker.py @@ -147,6 +147,63 @@ class ManagedLockBrokerTests(unittest.TestCase): self.assertFalse(sock.exists()) self.assertTrue(probe_free(root / "repositories/alice/ledger/locks/issue-7.lock")) + def test_serve_parser_accepts_only_canonical_roles(self): + import contextlib + import io + roles = {"gitea-spec-agent", "gitea-impl-agent", "gitea-review-agent"} + self.assertEqual(broker.ROLES, roles) + with tempfile.TemporaryDirectory() as d: + root = Path(d) / "not-created" + args = ["serve", "--root", str(root), "--owner", "alice", "--repository", "ledger", + "--issue", "7", "--run-id", "role-probe", "--ttl-seconds", "30"] + for role in sorted(roles): + with self.subTest(role=role): + self.assertEqual(broker.parser().parse_args([*args, "--role", role]).role, role) + for role in ("spec", "impl", "review", "gitea-release-agent", "Gitea-spec-agent", " gitea-spec-agent", "gitea-spec-agent "): + with self.subTest(role=role): + stderr = io.StringIO() + with contextlib.redirect_stderr(stderr), self.assertRaises(SystemExit) as rejected: + broker.parser().parse_args([*args, "--role", role]) + self.assertEqual(rejected.exception.code, 2) + self.assertIn("invalid choice", stderr.getvalue()) + for allowed in roles: + self.assertIn(allowed, stderr.getvalue()) + help_text = io.StringIO() + with contextlib.redirect_stdout(help_text), self.assertRaises(SystemExit) as helped: + broker.parser().parse_args(["serve", "--help"]) + self.assertEqual(helped.exception.code, 0) + for role in roles: + self.assertIn(role, help_text.getvalue()) + self.assertFalse(root.exists()) + # The direct Python constructor retains its independent role guard. + with self.assertRaisesRegex(ValueError, "invalid role"): + broker.Broker(Namespace(role="spec")) + + def test_invalid_serve_role_cli_reports_choices_before_runtime_creation(self): + with tempfile.TemporaryDirectory() as d: + root = Path(d) / "not-created" + result = subprocess.run( + [sys.executable, str(BROKER), "serve", "--root", str(root), "--owner", "alice", + "--repository", "ledger", "--issue", "7", "--role", "spec", "--run-id", "role-probe", + "--ttl-seconds", "30"], capture_output=True, text=True, timeout=10) + self.assertEqual(result.returncode, 2) + self.assertEqual(result.stdout, "") + self.assertFalse(root.exists()) + self.assertIn("invalid choice", result.stderr) + for role in ("gitea-spec-agent", "gitea-impl-agent", "gitea-review-agent"): + self.assertIn(role, result.stderr) + self.assertNotIn("error: ValueError", result.stderr) + self.assertNotIn("Traceback", result.stderr) + + def test_canonical_role_values_and_example_are_documented(self): + doc = (Path(__file__).parents[2] / "operations/repository-cache.md").read_text() + section = doc.split("### Canonical command templates", 1)[1].split("## Lock 규칙", 1)[0] + for role in ("gitea-spec-agent", "gitea-impl-agent", "gitea-review-agent"): + self.assertIn(f"`{role}`", section) + self.assertIn("--role gitea-spec-agent --run-id", section) + self.assertIn("spec`/`impl`/`review", section) + self.assertIn("authorization", section) + def test_constructor_rejects_lexical_root_and_handle_aliases(self): with tempfile.TemporaryDirectory() as d: root = Path(d); ensure_cache(root)