Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/prompts/dogfood-github-integration.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
# Dogfood: GitHub integration boundary

Validate the repository publication path without moving credentials into worker state.

## Acceptance criteria

1. The worker and arbiter remain provider-neutral.
2. GitHub authentication is supplied only by the trusted parent environment or `gh` credential store.
3. `GitHubClient` does not store, serialize, or write credentials to task state, worker results, prompts, or repository files.
4. CI proves `GH_TOKEN`/`GITHUB_TOKEN` remains available to the `gh` subprocess.
5. Pull-request creation, inspection, and squash merge remain explicit operations.
6. Merge authorization remains outside the worker/arbiter decision itself.

## Dogfood sequence

`Task -> Worker -> hard gates -> Arbiter -> accepted diff -> trusted GitHub publication`

A live publication run must use a disposable branch and an explicitly authorized GitHub identity. Tests must use mocked `gh` subprocesses and must never require a real token.
20 changes: 12 additions & 8 deletions personal_agent/github.py
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
"""GitHub CLI integration for guarded repository publication.

The adapter deliberately uses the authenticated ``gh`` CLI instead of storing
GitHub credentials in the agent process. It is an integration boundary, not a
second Git implementation. All mutating operations are explicit methods so
callers can map them to the autonomous-engineering authority levels.
GitHub credentials in agent state. It is an integration boundary, not a second
Git implementation. All mutating operations are explicit methods so callers
can map them to the autonomous-engineering authority levels.
"""

from __future__ import annotations
Expand All@@ -29,7 +29,12 @@ class PullRequest:


class GitHubClient:
"""Small, credential-free GitHub adapter backed by ``gh``."""
"""Small GitHub adapter backed by ``gh``.

Authentication remains owned by the trusted parent environment or gh's
credential store. The client does not persist, serialize, or place tokens
in worker results, task state, prompts, or repository files.
"""

def __init__(self, repository: str | None = None, *, timeout: int = 30) -> None:
self.repository = repository
Expand All@@ -39,11 +44,10 @@ def _run(self, args: Sequence[str], *, check: bool = True) -> str:
command = ["gh", *args]
if self.repository and "--repo" not in command:
command.extend(["--repo", self.repository])
# Preserve gh's authentication environment. These credentials are
# consumed only by the trusted GitHub adapter and are never copied into
# worker state. In CI, GH_TOKEN/GITHUB_TOKEN is the normal gh contract.
env = dict(os.environ)
env.pop("GITHUB_TOKEN", None)
env.pop("GH_TOKEN", None)
# gh authentication is intentionally delegated to gh's configured
# credential store / host auth. Do not copy tokens into worker state.
try:
result = subprocess.run(
command,
Expand Down
15 changes: 14 additions & 1 deletion tests/test_github.py
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import json
import os
import subprocess

import pytest
Expand All@@ -13,14 +14,17 @@ def test_create_pull_request_parses_machine_output(monkeypatch: pytest.MonkeyPat

def fake_run(command, **kwargs):
calls.append(command)
assert "GH_TOKEN" not in kwargs["env"]
assert kwargs["env"]["GH_TOKEN"] == "test-token"
assert kwargs["env"]["GITHUB_TOKEN"] == "test-token-2"
return subprocess.CompletedProcess(
command,
0,
stdout=json.dumps({"number": 42, "url": "https://github.com/FlossWare/agent/pull/42", "title": "Fix"}),
stderr="",
)

monkeypatch.setenv("GH_TOKEN", "test-token")
monkeypatch.setenv("GITHUB_TOKEN", "test-token-2")
monkeypatch.setattr(subprocess, "run", fake_run)
pr = GitHubClient("FlossWare/agent").create_pull_request(
title="Fix",
Expand All@@ -34,6 +38,15 @@ def fake_run(command, **kwargs):
assert "--repo" in calls[0]


def test_github_client_does_not_persist_credentials(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("GH_TOKEN", "secret")
client = GitHubClient("FlossWare/agent")
assert client.repository == "FlossWare/agent"
assert client.timeout == 30
assert not hasattr(client, "token")
assert not hasattr(client, "credential")


def test_missing_gh_is_reported(monkeypatch: pytest.MonkeyPatch) -> None:
def fake_run(*args, **kwargs):
raise FileNotFoundError
Expand Down
Loading