From 1659086bef00113ca4fd75317f7e13faf63a4822 Mon Sep 17 00:00:00 2001 From: Aymen Hammouda Date: Thu, 1 Oct 2026 23:53:55 +0200 Subject: [PATCH 1/4] feat(vision): enable guarded temporary GitHub authentication --- AGENT-EXECUTION-PIPELINE.md | 12 +++++++ ops/vision/README.md | 22 ++++++++++-- ops/vision/configure_apps.py | 30 ++++++++++++++++- ops/vision/control.py | 41 +++++++++++++++++++++-- ops/vision/install.py | 4 +++ tests/test_vision_control.py | 65 ++++++++++++++++++++++++++++++++++++ 6 files changed, 168 insertions(+), 6 deletions(-) diff --git a/AGENT-EXECUTION-PIPELINE.md b/AGENT-EXECUTION-PIPELINE.md index 567e239..217d7b4 100644 --- a/AGENT-EXECUTION-PIPELINE.md +++ b/AGENT-EXECUTION-PIPELINE.md @@ -408,3 +408,15 @@ Until activation, authenticated project operations fail closed. propose policy improvements through a reviewed PR; it cannot replace its own installed trust boundary. Paid model benchmarking stays disabled without a separately specified budget. + +## Amendment 2026-10-01 — Authorized temporary credential + +Aymen authorized using Vision's existing GitHub credential until the two Apps +are configured. Only the root-owned fixed-repository broker may hold it; no +worker receives the token. Independent prepublication review remains mandatory. +For this temporary mode, merge authorization uses a root-owned verification +receipt bound to the exact PR head, current main and installed broker policy. +Public evidence comments cannot replace that receipt. Required GitHub CI/security +checks, resolved conversations and SHA-matched merging remain enforced. Releases +remain disabled until App activation. Successful App activation removes the +temporary credential and enables the separate verifier identity/check requirement. diff --git a/ops/vision/README.md b/ops/vision/README.md index 1490bd7..99c3793 100644 --- a/ops/vision/README.md +++ b/ops/vision/README.md @@ -6,9 +6,25 @@ at **08:17 and 20:17 Europe/Paris**. Account ownership stays with Aymen. ## Finish GitHub identity setup later -The host preparation does not activate authenticated bot actions. No personal -GitHub token is handed to a project worker. The user explicitly deferred this -one-time browser step. +The user authorized temporary use of Vision's existing GitHub credential while +deferring the two Apps. The operator enables it with: + +```sh +ssh -t ahammouda@vision \ + 'sudo /opt/python-docs-vision/configure_apps.py enable-temporary' +``` + +The broker privately captures the existing host credential into a root-only file; +workers never receive it. Issues, publication, review and merges work through the +same fixed-repository allowlist. Independent reviews produce root-owned receipts +bound to the current PR head, main and installed policy, plus public evidence +comments. Comments and account-issued statuses cannot authorize merges. The eleven +required CI/security checks remain unchanged. The credential itself retains its +existing account scope; this temporary mode does not provide separate App identity. +Releases stay disabled until App activation, which removes the temporary copy. +Removing `/etc/python-docs-vision/activated` stops either authentication mode. + +To switch to the Apps: 1. Open an operator terminal and keep this SSH tunnel running: diff --git a/ops/vision/configure_apps.py b/ops/vision/configure_apps.py index edc246f..83a3f74 100644 --- a/ops/vision/configure_apps.py +++ b/ops/vision/configure_apps.py @@ -258,6 +258,7 @@ def activate(): operator_api( "environments/pypi/deployment-branch-policies", "POST", {"name": "v*", "type": "tag"} ) + (CONFIG / "temporary-token").unlink(missing_ok=True) (CONFIG / "activated").write_text(str(int(time.time()))) print( "Both Apps are scoped to one repository. Independent verification is required. " @@ -265,16 +266,43 @@ def activate(): ) +def enable_temporary(): + if any((CONFIG / f"{role}.json").exists() for role in PERMISSIONS): + raise ValueError("Apps already registered; complete activation instead of downgrading") + (CONFIG / "activated").unlink(missing_ok=True) + result = subprocess.run( + ["/usr/sbin/runuser", "-u", "ahammouda", "--", + "/home/linuxbrew/.linuxbrew/bin/gh", "auth", "token", "--hostname", "github.com"], + env=CONTROL["ENV"], capture_output=True, text=True, check=True, + ) + credential = result.stdout.strip() + account = CONTROL["request"]("https://api.github.com/user", credential) + repository = CONTROL["request"](f"https://api.github.com/repos/{REPO}", credential) + if account["login"] != REPO.split("/")[0] or not repository["permissions"]["push"]: + raise ValueError("Expected Vision's existing repository-owner credential with write access") + fd = os.open( + CONFIG / "temporary-token", os.O_WRONLY | os.O_CREAT | os.O_TRUNC | os.O_NOFOLLOW, 0o600 + ) + with os.fdopen(fd, "w") as output: + os.fchmod(output.fileno(), 0o600) + output.write(credential) + (CONFIG / "activated").write_text(str(int(time.time()))) + print("Temporary broker authentication enabled. Independent review and CI remain required. " + "Releases still require Apps; workers never receive the credential.") + + def main(): if os.geteuid() != 0 or os.environ.get("SUDO_USER") != "ahammouda": raise PermissionError("Operator sudo session required") os.umask(0o077) parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("operation", choices=["serve", "activate"]) + parser.add_argument("operation", choices=["serve", "activate", "enable-temporary"]) parser.add_argument("--port", type=int, default=8766) args = parser.parse_args() if args.operation == "activate": activate() + elif args.operation == "enable-temporary": + enable_temporary() else: print( f"Operator-only setup: http://127.0.0.1:{args.port}/?key={SETUP_KEY}", diff --git a/ops/vision/control.py b/ops/vision/control.py index 7cdd3df..2561054 100644 --- a/ops/vision/control.py +++ b/ops/vision/control.py @@ -86,6 +86,10 @@ def app_jwt(role: str) -> str: @functools.lru_cache def token(role: str) -> str: + if role not in {"owner", "verifier"}: + raise ValueError("Unknown identity") + if temporary_auth(): + return (CONFIG / "temporary-token").read_text().strip() settings = json.loads((CONFIG / f"{role}.json").read_text()) jwt = app_jwt(role) permissions = ( @@ -109,6 +113,10 @@ def token(role: str) -> str: return response["token"] +def temporary_auth() -> bool: + return (CONFIG / "temporary-token").is_file() + + def api(path: str, method="GET", data=None, role="owner"): return request(f"https://api.github.com/repos/{REPO}/{path}", token(role), method, data) @@ -255,6 +263,18 @@ def evidence(data: dict) -> None: def require_verified(pr: dict, head: str, base: str) -> dict: + if temporary_auth(): + checkpoint = STATE / f"verify-{pr['number']}-{base}-{head}.json" + result = json.loads(checkpoint.read_text()) if checkpoint.exists() else {} + if ( + pr["head"]["sha"] != head + or pr["state"] != "open" + or pr["base"]["ref"] != "main" + or result.get("status") != "success" + or result.get("policy") != hashlib.sha256(Path(__file__).read_bytes()).hexdigest() + ): + raise ValueError("Missing/stale trusted verification; reverify current head and main") + return result app_id = json.loads((CONFIG / "verifier.json").read_text())["app_id"] checks = api(f"commits/{head}/check-runs?per_page=100")["check_runs"] checks = [ @@ -387,9 +407,11 @@ def verify(number: int, head: str, decision: dict): status="in_progress", updated_at=int(time.time()), decision=decision, + policy=hashlib.sha256(Path(__file__).read_bytes()).hexdigest(), ) checkpoint.write_text(json.dumps(prior)) - check = api( + temporary = temporary_auth() + check = None if temporary else api( "check-runs", "POST", { @@ -413,6 +435,17 @@ def verify(number: int, head: str, decision: dict): verdict = {"approved": False, "summary": f"{type(exc).__name__}: {str(exc)[:500]}"} prior.update(verdict=verdict, updated_at=int(time.time())) checkpoint.write_text(json.dumps(prior)) + if temporary: + # OAuth cannot issue App check-runs. Only this root-owned receipt permits merge; + # the public comment is evidence, never an authorization input. + comment = api( + f"issues/{number}/comments", "POST", + {"body": "Vision — automated project maintainer\n\n" + f"Independent verification (temporary credential): **{prior['status']}**\n\n" + f"Head: `{head}`; main: `{base}`.\n\n" + "```json\n" + json.dumps(verdict, indent=2)[:60000] + "\n```"}, + ) + return {"status": prior["status"], "comment_url": comment["html_url"]} return api( f"check-runs/{check['id']}", "PATCH", @@ -476,6 +509,8 @@ def resolve_thread(number: int, head: str, thread_id: str, reason: str): def release(sha: str, tag: str): + if temporary_auth(): + raise ValueError("Releases require GitHub App activation; temporary token cannot release") if not SHA.fullmatch(sha) or not re.fullmatch(r"v\d+\.\d+\.\d+(?:(?:a|b|rc)\d+)?", tag): raise ValueError("Exact commit and semantic version tag required") app_id = json.loads((CONFIG / "verifier.json").read_text())["app_id"] @@ -501,6 +536,8 @@ def dispatch(data: dict): return { "repository": REPO, "ready": (CONFIG / "activated").exists(), + "authentication": "temporary-token" if temporary_auth() else "github-apps", + "releases_enabled": (CONFIG / "activated").exists() and not temporary_auth(), "blocked_verifications": [ json.loads(p.read_text()) for p in STATE.glob("verify-*.json") @@ -508,7 +545,7 @@ def dispatch(data: dict): ], } if not (CONFIG / "activated").exists(): - raise ValueError("Scoped GitHub Apps are not activated; project writes are disabled") + raise ValueError("GitHub credentials are not activated; project writes are disabled") if operation == "api": validate_api(data["method"], data["path"], data.get("body")) return api(data["path"], data["method"], data.get("body")) diff --git a/ops/vision/install.py b/ops/vision/install.py index 1ee4bae..3971103 100644 --- a/ops/vision/install.py +++ b/ops/vision/install.py @@ -53,6 +53,10 @@ Obtain independent verification through pdctl verify, never by self-assertion. pdctl merge performs a SHA-matched merge only after the verifier succeeds. Use pdctl release COMMIT vX.Y.Z only after successful main CI; tags are immutable. +When status.authentication is temporary-token, the operator authorized the existing +credential inside the broker only. Continue normal issue/PR/merge work. Independent +verification uses a root-owned receipt and public evidence comment until Apps exist; +comments cannot authorize merges. Releases remain blocked until releases_enabled. Feature decisions must include a user problem, dated primary-source research, baseline, target, acceptance, non-goals, outcome review date and revisit condition. Maintenance must not indefinitely displace relevant product development. diff --git a/tests/test_vision_control.py b/tests/test_vision_control.py index 50e0049..e827566 100644 --- a/tests/test_vision_control.py +++ b/tests/test_vision_control.py @@ -1,6 +1,8 @@ """Negative cases for the fixed-repository privileged broker boundary.""" import base64 +import hashlib +import json import runpy from pathlib import Path @@ -202,3 +204,66 @@ def malformed(*args): ) assert calls[-1]["status"] == "completed" assert calls[-1]["conclusion"] == "failure" + + +def test_temporary_auth_uses_only_the_broker_and_keeps_releases_disabled(tmp_path, monkeypatch): + module = CONTROL["dispatch"].__globals__ + monkeypatch.setitem(module, "CONFIG", tmp_path) + monkeypatch.setitem(module, "STATE", tmp_path) + (tmp_path / "temporary-token").write_text("test-credential") + (tmp_path / "activated").write_text("1") + assert CONTROL["token"].__wrapped__("owner") == "test-credential" + with pytest.raises(ValueError, match="Unknown identity"): + CONTROL["token"].__wrapped__("arbitrary") + status = CONTROL["dispatch"]({"operation": "status"}) + assert status["ready"] is True + assert status["authentication"] == "temporary-token" + assert status["releases_enabled"] is False + assert "test-credential" not in json.dumps(status) + with pytest.raises(ValueError, match="Releases require GitHub App"): + CONTROL["release"]("a" * 40, "v1.0.0") + (tmp_path / "activated").unlink() + with pytest.raises(ValueError, match="not activated"): + CONTROL["dispatch"]({"operation": "merge", "pr": 1, "head_sha": "a" * 40}) + + +def test_temporary_verification_requires_current_private_receipt(tmp_path, monkeypatch): + module = CONTROL["verify"].__globals__ + monkeypatch.setitem(module, "CONFIG", tmp_path) + monkeypatch.setitem(module, "STATE", tmp_path) + (tmp_path / "temporary-token").write_text("test-credential") + head, base = "a" * 40, "b" * 40 + pr = {"number": 1, "head": {"sha": head}, "base": {"ref": "main"}, "state": "open"} + verdict = {"approved": True, "head_sha": head, "base_sha": base, "blockers": []} + calls = [] + + def api(path, method="GET", data=None, role="owner"): + calls.append((path, method)) + if path == "pulls/1": + return pr + if path == "branches/main": + return {"commit": {"sha": base}} + assert path == "issues/1/comments" and method == "POST" + return {"html_url": "https://github.com/example/evidence"} + + monkeypatch.setitem(module, "api", api) + monkeypatch.setitem(module, "review", lambda *args: verdict) + with pytest.raises(ValueError, match="Missing/stale"): + CONTROL["require_verified"](pr, head, base) + result = CONTROL["verify"]( + 1, head, {"kind": "maintenance", "rationale": "Repair", "acceptance": "Tests"} + ) + assert result["status"] == "success" + assert CONTROL["require_verified"](pr, head, base)["verdict"] == verdict + with pytest.raises(ValueError, match="Missing/stale"): + CONTROL["require_verified"](pr, head, "c" * 40) + with pytest.raises(ValueError, match="Missing/stale"): + CONTROL["require_verified"]({**pr, "head": {"sha": "c" * 40}}, head, base) + path = tmp_path / f"verify-1-{base}-{head}.json" + receipt = json.loads(path.read_text()) + assert receipt["policy"] == hashlib.sha256(Path(module["__file__"]).read_bytes()).hexdigest() + for invalid in [{**receipt, "policy": "outdated"}, {**receipt, "status": "failure"}]: + path.write_text(json.dumps(invalid)) + with pytest.raises(ValueError, match="Missing/stale"): + CONTROL["require_verified"](pr, head, base) + assert all(not p.startswith("check-runs") for p, _ in calls) From 5265d1cfaecce3ed8a8f3c7d1c352893486f76ff Mon Sep 17 00:00:00 2001 From: Aymen Hammouda Date: Fri, 2 Oct 2026 00:02:44 +0200 Subject: [PATCH 2/4] fix(vision): isolate broker tests and surface review blockers --- ops/vision/control.py | 5 ++++- ops/vision/install.py | 3 +++ tests/test_vision_control.py | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/ops/vision/control.py b/ops/vision/control.py index 2561054..ad61104 100644 --- a/ops/vision/control.py +++ b/ops/vision/control.py @@ -381,7 +381,10 @@ def review(head: str, base: str, decision: dict): or any(not any(check in command for command in recorded) for check in mandatory) or any(c.get("exit_code") != 0 for c in commands) ): - raise ValueError("Independent reviewer did not approve complete current-head evidence") + raise ValueError( + "Independent review rejected: " + + str(verdict.get("blockers") or "incomplete check evidence") + ) checkpoint.write_text(json.dumps(verdict)) return verdict except Exception: diff --git a/ops/vision/install.py b/ops/vision/install.py index 3971103..e16d372 100644 --- a/ops/vision/install.py +++ b/ops/vision/install.py @@ -93,6 +93,9 @@ GitHub enforce hosted checks separately at merge. Never treat skipped CodeRabbit or a mocked benchmark as substantive verification. Report blockers if access, tests, review or product evidence is incomplete. +Host MCP connectors are intentionally excluded, not an acceptance requirement. +Their absence belongs in limitations, not blockers, when the required repository +checks and sandbox tools are available. Never skip a required check on that basis. Return only the JSON verdict schema in the trusted task. Every command must have its real exit_code. No completion without fresh evidence. """, diff --git a/tests/test_vision_control.py b/tests/test_vision_control.py index e827566..78365a7 100644 --- a/tests/test_vision_control.py +++ b/tests/test_vision_control.py @@ -183,6 +183,7 @@ def test_thread_resolution_rejects_a_thread_from_another_pr(monkeypatch): def test_malformed_verifier_result_completes_check_as_failure(tmp_path, monkeypatch): module = CONTROL["verify"].__globals__ + monkeypatch.setitem(module, "CONFIG", tmp_path) monkeypatch.setitem(module, "STATE", tmp_path) calls = [] From e740e5d47a8a162124131e001bc473536020876a Mon Sep 17 00:00:00 2001 From: Aymen Hammouda Date: Fri, 2 Oct 2026 00:12:17 +0200 Subject: [PATCH 3/4] fix(vision): attest operator mode and verify credential lifecycle --- ops/vision/control.py | 6 +++ tests/test_vision_control.py | 78 ++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/ops/vision/control.py b/ops/vision/control.py index ad61104..2aab741 100644 --- a/ops/vision/control.py +++ b/ops/vision/control.py @@ -325,6 +325,12 @@ def review(head: str, base: str, decision: dict): # The reviewer is a separate OpenClaw agent with an SSH sandbox and no GitHub identity. prompt = ( f"Independently review {REPO}. Exact head: {head}; base: {base}. " + "Trusted installed authentication mode: " + f"{'temporary-token' if temporary_auth() else 'github-apps'}. " + "When temporary mode is active, the host operator has authorized " + "the existing account credential inside the broker plus private review " + "receipts for merges; releases remain disabled. This mode is supplied " + "from root-owned configuration, not the following owner rationale. " "This may be unpublished: fetch its exact SHA from the public repository. " "Use the trusted verifier instructions. Clone/fetch the public repository, " "inspect the full diff and run the canonical checks yourself. Do not execute " diff --git a/tests/test_vision_control.py b/tests/test_vision_control.py index 78365a7..32252bd 100644 --- a/tests/test_vision_control.py +++ b/tests/test_vision_control.py @@ -268,3 +268,81 @@ def api(path, method="GET", data=None, role="owner"): with pytest.raises(ValueError, match="Missing/stale"): CONTROL["require_verified"](pr, head, base) assert all(not p.startswith("check-runs") for p, _ in calls) + + +def test_temporary_capture_and_app_activation_lifecycle(tmp_path, monkeypatch, capsys): + import copy + from types import SimpleNamespace + + loader = runpy.run_path + monkeypatch.setattr(runpy, "run_path", lambda _: CONTROL) + setup = loader(str(Path(__file__).parents[1] / "ops/vision/configure_apps.py")) + module = setup["activate"].__globals__ + monkeypatch.setitem(module, "CONFIG", tmp_path) + credential = "private-test-credential" + rules = { + "name": "main", "target": "branch", "enforcement": "active", + "bypass_actors": [], "conditions": {}, + "rules": [{"type": "required_status_checks", "parameters": { + "required_status_checks": [{"context": "Existing CI", "integration_id": 15368}] + }}], + } + + def run(command, **kwargs): + assert command[-4:] == ["auth", "token", "--hostname", "github.com"] + assert kwargs["capture_output"] is True + return SimpleNamespace(stdout=credential) + + def request(url, token, method="GET", data=None): + if url.endswith("/user"): + assert token == credential + return {"login": "ayhammouda"} + if url.endswith("/repos/" + CONTROL["REPO"]): + return {"permissions": {"push": True}} + if url.endswith("/app"): + return {"owner": {"login": "ayhammouda"}, + "permissions": setup["PERMISSIONS"][token]} + if url.endswith("/app/installations"): + return [{"id": 1, "account": {"login": "ayhammouda"}, + "repository_selection": "selected"}] + if url.endswith("/access_tokens"): + return {"token": "scoped-installation-test"} + assert url.endswith("/installation/repositories") + return {"total_count": 1, "repositories": [{"full_name": CONTROL["REPO"]}]} + + def operator_api(path, method="GET", body=None): + if path == "rulesets/15269598": + if method == "PUT": + rules.update(copy.deepcopy(body)) + return copy.deepcopy(rules) + if path == "actions/variables" and method == "GET": + return {"variables": []} + if path == "rulesets?per_page=100": + return [] + if path == "environments/pypi/deployment-branch-policies" and method == "GET": + return {"branch_policies": []} + return {} + + monkeypatch.setattr(module["subprocess"], "run", run) + monkeypatch.setitem(module, "CONTROL", {**CONTROL, "request": request, "app_jwt": lambda r: r}) + monkeypatch.setitem(module, "operator_api", operator_api) + setup["enable_temporary"]() + token_path = tmp_path / "temporary-token" + assert token_path.read_text() == credential + assert token_path.stat().st_mode & 0o777 == 0o600 + assert (tmp_path / "activated").exists() + assert credential not in capsys.readouterr().out + with pytest.raises(FileNotFoundError): + setup["activate"]() + assert not (tmp_path / "activated").exists() + assert token_path.exists() # Retained privately, but no automatic reactivation. + for role, app_id in [("owner", 41), ("verifier", 42)]: + (tmp_path / f"{role}.json").write_text(json.dumps({"app_id": app_id})) + with pytest.raises(ValueError, match="instead of downgrading"): + setup["enable_temporary"]() + setup["activate"]() + assert not token_path.exists() + assert (tmp_path / "activated").exists() + required = rules["rules"][0]["parameters"]["required_status_checks"] + assert {"context": "Existing CI", "integration_id": 15368} in required + assert {"context": "Independent verification", "integration_id": 42} in required From 2111a2d4f87b90296a0aae50a2779a3cd36e70c1 Mon Sep 17 00:00:00 2001 From: Aymen Hammouda Date: Fri, 2 Oct 2026 00:30:12 +0200 Subject: [PATCH 4/4] fix(vision): scope host MCP servers before project tool discovery --- ops/vision/README.md | 5 ++++ ops/vision/install.py | 14 +++++++++- tests/test_vision_control.py | 53 ++++++++++++++++++++++++++++++------ 3 files changed, 62 insertions(+), 10 deletions(-) diff --git a/ops/vision/README.md b/ops/vision/README.md index 99c3793..07b7b86 100644 --- a/ops/vision/README.md +++ b/ops/vision/README.md @@ -151,6 +151,11 @@ project accounts/agents. Root helpers in `/opt/python-docs-vision` cannot be updated by Vision, even if a PR changes their source. Apply reviewed updates as the operator. The installer does not change the existing job or activate Apps. +Host MCP servers use native `codex.agents` scoping to exclude project agents before +connection, avoiding false incomplete-MCP blockers. Existing agent access and +explicit restrictions are preserved. Add future host agents to those allowlists +when granting them connector access. + Check native agent execution, credential unreadability, root/SSH write denial, worker isolation, refused publication without independent review, wrong-App or stale-head checks, failed CI and release eligibility. Run the repository tests diff --git a/ops/vision/install.py b/ops/vision/install.py index e16d372..cf3c1e7 100644 --- a/ops/vision/install.py +++ b/ops/vision/install.py @@ -121,6 +121,17 @@ def owned(path, text, uid, gid, mode=0o600): output.write(text) +def scope_host_mcp(config): + project_agents = {f"pd-{role}" for role in ROLES} + for server in config.get("mcp", {}).get("servers", {}).values(): + codex = server.setdefault("codex", {}) + codex["agents"] = [ + agent + for agent in codex.get("agents", config["agents"]["entries"]) + if agent.strip().lower() not in project_agents + ] + + def main(): if os.geteuid() != 0 or os.environ.get("SUDO_USER") != HOST_USER: raise PermissionError("Run through the trusted operator's sudo session") @@ -166,6 +177,7 @@ def main(): entries = config["agents"]["entries"] if not isinstance(entries, dict): raise ValueError("Unexpected agent configuration shape") + scope_host_mcp(config) for role in ROLES: user = f"pd-{role}" try: @@ -311,7 +323,7 @@ def main(): { "installed": True, "backup": str(backup), - "github_writes": "pending App setup", + "github_writes": "unchanged; run pdctl status", "scheduler_changed": False, } ) diff --git a/tests/test_vision_control.py b/tests/test_vision_control.py index 32252bd..0540bc5 100644 --- a/tests/test_vision_control.py +++ b/tests/test_vision_control.py @@ -159,6 +159,33 @@ def test_installer_rejects_worker_symlinks_before_ownership_changes(tmp_path): assert secret.read_text() == "unchanged" +def test_host_mcp_scoping_preserves_other_agents_and_existing_restrictions(): + scope = runpy.run_path(str(Path(__file__).parents[1] / "ops/vision/install.py"))[ + "scope_host_mcp" + ] + config = { + "agents": {"entries": {"main": {}, "piquetbot": {}, "pd-owner": {}}}, + "mcp": { + "servers": { + "unscoped": {"url": "https://example.test/mcp"}, + "restricted": {"codex": {"agents": ["main", "future", "PD-Verifier"]}}, + "disabled": {"codex": {"agents": []}}, + } + }, + } + scope(config) + servers = config["mcp"]["servers"] + assert servers["unscoped"] == { + "url": "https://example.test/mcp", + "codex": {"agents": ["main", "piquetbot"]}, + } + assert servers["restricted"]["codex"]["agents"] == ["main", "future"] + assert servers["disabled"]["codex"]["agents"] == [] + before = json.dumps(config) + scope(config) + assert json.dumps(config) == before + + def test_thread_resolution_rejects_a_thread_from_another_pr(monkeypatch): module = CONTROL["resolve_thread"].__globals__ monkeypatch.setitem(module, "api", lambda *args: {"commit": {"sha": "b" * 40}}) @@ -281,11 +308,19 @@ def test_temporary_capture_and_app_activation_lifecycle(tmp_path, monkeypatch, c monkeypatch.setitem(module, "CONFIG", tmp_path) credential = "private-test-credential" rules = { - "name": "main", "target": "branch", "enforcement": "active", - "bypass_actors": [], "conditions": {}, - "rules": [{"type": "required_status_checks", "parameters": { - "required_status_checks": [{"context": "Existing CI", "integration_id": 15368}] - }}], + "name": "main", + "target": "branch", + "enforcement": "active", + "bypass_actors": [], + "conditions": {}, + "rules": [ + { + "type": "required_status_checks", + "parameters": { + "required_status_checks": [{"context": "Existing CI", "integration_id": 15368}] + }, + } + ], } def run(command, **kwargs): @@ -300,11 +335,11 @@ def request(url, token, method="GET", data=None): if url.endswith("/repos/" + CONTROL["REPO"]): return {"permissions": {"push": True}} if url.endswith("/app"): - return {"owner": {"login": "ayhammouda"}, - "permissions": setup["PERMISSIONS"][token]} + return {"owner": {"login": "ayhammouda"}, "permissions": setup["PERMISSIONS"][token]} if url.endswith("/app/installations"): - return [{"id": 1, "account": {"login": "ayhammouda"}, - "repository_selection": "selected"}] + return [ + {"id": 1, "account": {"login": "ayhammouda"}, "repository_selection": "selected"} + ] if url.endswith("/access_tokens"): return {"token": "scoped-installation-test"} assert url.endswith("/installation/repositories")