github-agent 0.29.0__tar.gz → 0.31.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. {github_agent-0.29.0/github_agent.egg-info → github_agent-0.31.0}/PKG-INFO +18 -3
  2. github_agent-0.29.0/PKG-INFO → github_agent-0.31.0/README.md +16 -22
  3. {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/agent_server.py +1 -1
  4. github_agent-0.31.0/github_agent/api/api_client_orgs.py +293 -0
  5. github_agent-0.31.0/github_agent/api/api_client_pages.py +234 -0
  6. {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/api_client.py +10 -0
  7. {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/auth.py +10 -0
  8. {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/github_input_models.py +134 -1
  9. {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/github_response_models.py +117 -0
  10. github_agent-0.31.0/github_agent/mcp/mcp_org.py +270 -0
  11. github_agent-0.31.0/github_agent/mcp/mcp_repo.py +287 -0
  12. {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/mcp_server.py +375 -6
  13. github_agent-0.29.0/README.md → github_agent-0.31.0/github_agent.egg-info/PKG-INFO +37 -1
  14. {github_agent-0.29.0 → github_agent-0.31.0}/github_agent.egg-info/SOURCES.txt +3 -0
  15. {github_agent-0.29.0 → github_agent-0.31.0}/github_agent.egg-info/requires.txt +1 -1
  16. {github_agent-0.29.0 → github_agent-0.31.0}/pyproject.toml +2 -2
  17. {github_agent-0.29.0 → github_agent-0.31.0}/tests/test_init_dynamics.py +7 -1
  18. github_agent-0.31.0/tests/test_org_crud.py +587 -0
  19. github_agent-0.31.0/tests/test_pages.py +616 -0
  20. github_agent-0.29.0/github_agent/api/api_client_orgs.py +0 -40
  21. github_agent-0.29.0/github_agent/mcp/mcp_org.py +0 -75
  22. github_agent-0.29.0/github_agent/mcp/mcp_repo.py +0 -113
  23. {github_agent-0.29.0 → github_agent-0.31.0}/LICENSE +0 -0
  24. {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/__init__.py +0 -0
  25. {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/__main__.py +0 -0
  26. {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/api/__init__.py +0 -0
  27. {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/api/api_client_base.py +0 -0
  28. {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/api/api_client_branches.py +0 -0
  29. {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/api/api_client_commits.py +0 -0
  30. {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/api/api_client_contents.py +0 -0
  31. {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/api/api_client_issues.py +0 -0
  32. {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/api/api_client_pulls.py +0 -0
  33. {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/api/api_client_releases.py +0 -0
  34. {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/api/api_client_repos.py +0 -0
  35. {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/api/api_client_search.py +0 -0
  36. {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/api/api_client_workflows.py +0 -0
  37. {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/mcp/__init__.py +0 -0
  38. {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/mcp/mcp_action.py +0 -0
  39. {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/mcp/mcp_branch.py +0 -0
  40. {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/mcp/mcp_collaborator.py +0 -0
  41. {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/mcp/mcp_commit.py +0 -0
  42. {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/mcp/mcp_content.py +0 -0
  43. {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/mcp/mcp_issue.py +0 -0
  44. {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/mcp/mcp_pull.py +0 -0
  45. {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/mcp/mcp_release.py +0 -0
  46. {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/mcp/mcp_search.py +0 -0
  47. {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/mcp_config.json +0 -0
  48. {github_agent-0.29.0 → github_agent-0.31.0}/github_agent.egg-info/dependency_links.txt +0 -0
  49. {github_agent-0.29.0 → github_agent-0.31.0}/github_agent.egg-info/entry_points.txt +0 -0
  50. {github_agent-0.29.0 → github_agent-0.31.0}/github_agent.egg-info/top_level.txt +0 -0
  51. {github_agent-0.29.0 → github_agent-0.31.0}/scripts/security_sanitizer.py +0 -0
  52. {github_agent-0.29.0 → github_agent-0.31.0}/scripts/validate_a2a_agent.py +0 -0
  53. {github_agent-0.29.0 → github_agent-0.31.0}/scripts/verify_api_integration.py +0 -0
  54. {github_agent-0.29.0 → github_agent-0.31.0}/setup.cfg +0 -0
  55. {github_agent-0.29.0 → github_agent-0.31.0}/tests/conftest.py +0 -0
  56. {github_agent-0.29.0 → github_agent-0.31.0}/tests/test_api_client_endpoints.py +0 -0
  57. {github_agent-0.29.0 → github_agent-0.31.0}/tests/test_api_validation.py +0 -0
  58. {github_agent-0.29.0 → github_agent-0.31.0}/tests/test_auth_client_edge_cases.py +0 -0
  59. {github_agent-0.29.0 → github_agent-0.31.0}/tests/test_concept_parity.py +0 -0
  60. {github_agent-0.29.0 → github_agent-0.31.0}/tests/test_github_agent_api_brute_force_coverage.py +0 -0
  61. {github_agent-0.29.0 → github_agent-0.31.0}/tests/test_github_agent_brute_force_coverage.py +0 -0
  62. {github_agent-0.29.0 → github_agent-0.31.0}/tests/test_mcp_coverage.py +0 -0
  63. {github_agent-0.29.0 → github_agent-0.31.0}/tests/test_model_fields.py +0 -0
  64. {github_agent-0.29.0 → github_agent-0.31.0}/tests/test_startup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: github-agent
3
- Version: 0.29.0
3
+ Version: 0.31.0
4
4
  Summary: GitHub Agent for MCP
5
5
  Author-email: Audel Rouhi <knucklessg1@gmail.com>
6
6
  License: MIT
@@ -12,7 +12,7 @@ Classifier: Programming Language :: Python :: 3
12
12
  Requires-Python: <3.15,>=3.11
13
13
  Description-Content-Type: text/markdown
14
14
  License-File: LICENSE
15
- Requires-Dist: agent-utilities[agent,logfire]>=0.47.0
15
+ Requires-Dist: agent-utilities[agent,logfire]>=0.49.0
16
16
  Provides-Extra: test
17
17
  Requires-Dist: pytest-xdist>=3.6.0; extra == "test"
18
18
  Requires-Dist: pytest; extra == "test"
@@ -41,7 +41,7 @@ Dynamic: license-file
41
41
  ![PyPI - Wheel](https://img.shields.io/pypi/wheel/github-agent)
42
42
  ![PyPI - Implementation](https://img.shields.io/pypi/implementation/github-agent)
43
43
 
44
- *Version: 0.29.0*
44
+ *Version: 0.31.0*
45
45
 
46
46
  > **Documentation** — Installation, deployment, and usage across the MCP, API, and
47
47
  > CLI interfaces, including the integrated A2A agent server, are maintained in the
@@ -199,6 +199,21 @@ docker run -d \
199
199
 
200
200
  ---
201
201
 
202
+ <!-- BEGIN GENERATED: additional-deployment-options -->
203
+ ### Additional Deployment Options
204
+
205
+ `github-agent` can also run as a **local container** (Docker / Podman / `uv`) or be
206
+ consumed from a **remote deployment**. The
207
+ [Deployment guide](https://knuckles-team.github.io/github-agent/deployment/) has full, copy-paste
208
+ `mcp_config.json` for all four transports — **stdio**, **streamable-http**,
209
+ **local container / uv**, and **remote URL**:
210
+
211
+ - **Local container / uv** — launch the server from `mcp_config.json` via `uvx`,
212
+ `docker run`, or `podman run`, or point at a local streamable-http container by `url`.
213
+ - **Remote URL** — connect to a server deployed behind Caddy at
214
+ `http://github-mcp.arpa/mcp` using the `"url"` key.
215
+ <!-- END GENERATED: additional-deployment-options -->
216
+
202
217
  ## Agent
203
218
 
204
219
  This repository features a fully integrated Pydantic AI Graph Agent. It communicates over the **Agent Control Protocol (ACP)** and interacts seamlessly with the **Agent Web UI (AG-UI)** and Terminal interface.
@@ -1,24 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: github-agent
3
- Version: 0.29.0
4
- Summary: GitHub Agent for MCP
5
- Author-email: Audel Rouhi <knucklessg1@gmail.com>
6
- License: MIT
7
- Classifier: Development Status :: 5 - Production/Stable
8
- Classifier: License :: Public Domain
9
- Classifier: Environment :: Console
10
- Classifier: Operating System :: POSIX :: Linux
11
- Classifier: Programming Language :: Python :: 3
12
- Requires-Python: <3.15,>=3.11
13
- Description-Content-Type: text/markdown
14
- License-File: LICENSE
15
- Requires-Dist: agent-utilities[agent,logfire]>=0.47.0
16
- Provides-Extra: test
17
- Requires-Dist: pytest-xdist>=3.6.0; extra == "test"
18
- Requires-Dist: pytest; extra == "test"
19
- Requires-Dist: pytest-asyncio; extra == "test"
20
- Dynamic: license-file
21
-
22
1
  # Github Agent
23
2
  ## CLI or API | MCP | Agent
24
3
 
@@ -41,7 +20,7 @@ Dynamic: license-file
41
20
  ![PyPI - Wheel](https://img.shields.io/pypi/wheel/github-agent)
42
21
  ![PyPI - Implementation](https://img.shields.io/pypi/implementation/github-agent)
43
22
 
44
- *Version: 0.29.0*
23
+ *Version: 0.31.0*
45
24
 
46
25
  > **Documentation** — Installation, deployment, and usage across the MCP, API, and
47
26
  > CLI interfaces, including the integrated A2A agent server, are maintained in the
@@ -199,6 +178,21 @@ docker run -d \
199
178
 
200
179
  ---
201
180
 
181
+ <!-- BEGIN GENERATED: additional-deployment-options -->
182
+ ### Additional Deployment Options
183
+
184
+ `github-agent` can also run as a **local container** (Docker / Podman / `uv`) or be
185
+ consumed from a **remote deployment**. The
186
+ [Deployment guide](https://knuckles-team.github.io/github-agent/deployment/) has full, copy-paste
187
+ `mcp_config.json` for all four transports — **stdio**, **streamable-http**,
188
+ **local container / uv**, and **remote URL**:
189
+
190
+ - **Local container / uv** — launch the server from `mcp_config.json` via `uvx`,
191
+ `docker run`, or `podman run`, or point at a local streamable-http container by `url`.
192
+ - **Remote URL** — connect to a server deployed behind Caddy at
193
+ `http://github-mcp.arpa/mcp` using the `"url"` key.
194
+ <!-- END GENERATED: additional-deployment-options -->
195
+
202
196
  ## Agent
203
197
 
204
198
  This repository features a fully integrated Pydantic AI Graph Agent. It communicates over the **Agent Control Protocol (ACP)** and interacts seamlessly with the **Agent Web UI (AG-UI)** and Terminal interface.
@@ -4,7 +4,7 @@ import os
4
4
  import sys
5
5
  import warnings
6
6
 
7
- __version__ = "0.29.0"
7
+ __version__ = "0.31.0"
8
8
 
9
9
  logging.basicConfig(
10
10
  level=logging.INFO,
@@ -0,0 +1,293 @@
1
+ #!/usr/bin/env python
2
+ from urllib.parse import urlparse
3
+
4
+ from agent_utilities.decorators import require_auth
5
+ from agent_utilities.exceptions import (
6
+ ParameterError,
7
+ )
8
+ from pydantic import ValidationError
9
+
10
+ from github_agent.api.api_client_base import BaseApiClient
11
+ from github_agent.github_input_models import (
12
+ OrgListModel,
13
+ OrgMemberModel,
14
+ OrgUpdateModel,
15
+ )
16
+ from github_agent.github_response_models import (
17
+ Organization,
18
+ OrganizationMembership,
19
+ OrganizationSummary,
20
+ Repository,
21
+ Response,
22
+ User,
23
+ )
24
+
25
+
26
+ class OrganizationCreationNotSupportedError(Exception):
27
+ """Raised when organization creation is requested against github.com.
28
+
29
+ github.com does not expose an API to create organizations — they can only
30
+ be created through the web UI. The POST /admin/organizations endpoint
31
+ exists only on GitHub Enterprise Server.
32
+ """
33
+
34
+
35
+ class Api(BaseApiClient):
36
+ @require_auth
37
+ def get_organization(self, org: str) -> Response:
38
+ """Get an organization's full profile.
39
+
40
+ GET /orgs/{org}
41
+ https://docs.github.com/en/rest/orgs/orgs#get-an-organization
42
+ """
43
+ response = self._session.get(
44
+ url=f"{self.url}/orgs/{org}",
45
+ headers=self.headers,
46
+ verify=self.verify,
47
+ proxies=self.proxies,
48
+ )
49
+ response.raise_for_status()
50
+ try:
51
+ parsed_data = Organization(**response.json())
52
+ return Response(response=response, data=parsed_data)
53
+ except ValidationError as e:
54
+ raise ParameterError(f"Invalid parameters: {e.errors()}") from e
55
+
56
+ @require_auth
57
+ def list_organizations(self, **kwargs) -> Response:
58
+ """List organizations.
59
+
60
+ With scope='member' (default): organizations the authenticated user
61
+ belongs to.
62
+ GET /user/orgs
63
+ https://docs.github.com/en/rest/orgs/orgs#list-organizations-for-the-authenticated-user
64
+
65
+ With scope='all': every organization, in ascending ID order. GitHub
66
+ paginates this endpoint with a 'since' ID cursor — pass the highest
67
+ organization ID from the previous page to fetch the next one.
68
+ GET /organizations
69
+ https://docs.github.com/en/rest/orgs/orgs#list-organizations
70
+ """
71
+ model = OrgListModel(**kwargs)
72
+ endpoint = "/user/orgs" if model.scope == "member" else "/organizations"
73
+ response, data = self._fetch_all_pages(endpoint, model)
74
+ try:
75
+ parsed_data = [OrganizationSummary(**item) for item in data]
76
+ return Response(response=response, data=parsed_data)
77
+ except ValidationError as e:
78
+ raise ParameterError(f"Invalid parameters: {e.errors()}") from e
79
+
80
+ @require_auth
81
+ def update_organization(self, org: str, **kwargs) -> Response:
82
+ """Update an organization's profile and member settings.
83
+
84
+ Accepts the documented mutable fields (validated by OrgUpdateModel):
85
+ billing_email, company, email, twitter_username, location, name,
86
+ description, blog, has_organization_projects, has_repository_projects,
87
+ default_repository_permission, members_can_create_repositories,
88
+ members_can_create_public/private/internal_repositories,
89
+ members_can_create_pages, members_can_fork_private_repositories,
90
+ web_commit_signoff_required.
91
+
92
+ PATCH /orgs/{org}
93
+ https://docs.github.com/en/rest/orgs/orgs#update-an-organization
94
+ """
95
+ try:
96
+ model = OrgUpdateModel(**kwargs)
97
+ except ValidationError as e:
98
+ raise ParameterError(f"Invalid parameters: {e.errors()}") from e
99
+ response = self._session.patch(
100
+ url=f"{self.url}/orgs/{org}",
101
+ json=model.model_dump(exclude_none=True),
102
+ headers=self.headers,
103
+ verify=self.verify,
104
+ proxies=self.proxies,
105
+ )
106
+ response.raise_for_status()
107
+ try:
108
+ parsed_data = Organization(**response.json())
109
+ return Response(response=response, data=parsed_data)
110
+ except ValidationError as e:
111
+ raise ParameterError(f"Invalid parameters: {e.errors()}") from e
112
+
113
+ @require_auth
114
+ def delete_organization(self, org: str) -> Response:
115
+ """Schedule an organization for deletion. IRREVERSIBLE.
116
+
117
+ GitHub responds 202 Accepted and deletes the organization, all of its
118
+ repositories, and all of its data asynchronously. The MCP layer gates
119
+ this action behind allow_destructive / GITHUB_ALLOW_DESTRUCTIVE.
120
+
121
+ DELETE /orgs/{org}
122
+ https://docs.github.com/en/rest/orgs/orgs#delete-an-organization
123
+ """
124
+ response = self._session.delete(
125
+ url=f"{self.url}/orgs/{org}",
126
+ headers=self.headers,
127
+ verify=self.verify,
128
+ proxies=self.proxies,
129
+ )
130
+ response.raise_for_status()
131
+ return Response(response=response, data={"status": "deletion_scheduled"})
132
+
133
+ @require_auth
134
+ def create_organization(
135
+ self, login: str, admin: str, profile_name: str | None = None
136
+ ) -> Response:
137
+ """Create an organization — GitHub Enterprise Server ONLY.
138
+
139
+ POST /admin/organizations (site-admin API)
140
+ https://docs.github.com/en/enterprise-server@latest/rest/enterprise-admin/orgs#create-an-organization
141
+
142
+ github.com does NOT expose an API to create organizations; on
143
+ github.com they can only be created through the web UI
144
+ (https://github.com/account/organizations/new). When this client
145
+ points at api.github.com this method raises
146
+ OrganizationCreationNotSupportedError instead of calling the API.
147
+
148
+ Args:
149
+ login: The organization's username.
150
+ admin: The login of the user who will manage this organization.
151
+ profile_name: The organization's display name.
152
+ """
153
+ host = urlparse(self.url).hostname or ""
154
+ if host == "api.github.com":
155
+ raise OrganizationCreationNotSupportedError(
156
+ "github.com organizations cannot be created via the API — "
157
+ "create them in the web UI at "
158
+ "https://github.com/account/organizations/new. "
159
+ "POST /admin/organizations is only available on GitHub "
160
+ "Enterprise Server (point GITHUB_URL at your enterprise "
161
+ "instance's API, e.g. https://ghe.example.com/api/v3)."
162
+ )
163
+ payload: dict = {"login": login, "admin": admin}
164
+ if profile_name is not None:
165
+ payload["profile_name"] = profile_name
166
+ response = self._session.post(
167
+ url=f"{self.url}/admin/organizations",
168
+ json=payload,
169
+ headers=self.headers,
170
+ verify=self.verify,
171
+ proxies=self.proxies,
172
+ )
173
+ response.raise_for_status()
174
+ try:
175
+ parsed_data = OrganizationSummary(**response.json())
176
+ return Response(response=response, data=parsed_data)
177
+ except ValidationError as e:
178
+ raise ParameterError(f"Invalid parameters: {e.errors()}") from e
179
+
180
+ @require_auth
181
+ def create_organization_repository(self, org: str, name: str, **kwargs) -> Response:
182
+ """Create a repository in an organization.
183
+
184
+ Accepts the same payload fields as create_repository (which posts to
185
+ /user/repos): description, homepage, private, visibility, has_issues,
186
+ has_projects, has_wiki, auto_init, gitignore_template,
187
+ license_template, etc.
188
+
189
+ POST /orgs/{org}/repos
190
+ https://docs.github.com/en/rest/repos/repos#create-an-organization-repository
191
+ """
192
+ try:
193
+ payload = {"name": name, **kwargs}
194
+ response = self._session.post(
195
+ url=f"{self.url}/orgs/{org}/repos",
196
+ json=payload,
197
+ headers=self.headers,
198
+ verify=self.verify,
199
+ proxies=self.proxies,
200
+ )
201
+ response.raise_for_status()
202
+ parsed_data = Repository(**response.json())
203
+ return Response(response=response, data=parsed_data)
204
+ except ValidationError as e:
205
+ raise ParameterError(f"Invalid parameters: {e.errors()}") from e
206
+
207
+ @require_auth
208
+ def get_organization_membership(self, org: str, username: str) -> Response:
209
+ """Get a user's organization membership (state and role).
210
+
211
+ GET /orgs/{org}/memberships/{username}
212
+ https://docs.github.com/en/rest/orgs/members#get-organization-membership-for-a-user
213
+ """
214
+ response = self._session.get(
215
+ url=f"{self.url}/orgs/{org}/memberships/{username}",
216
+ headers=self.headers,
217
+ verify=self.verify,
218
+ proxies=self.proxies,
219
+ )
220
+ response.raise_for_status()
221
+ try:
222
+ parsed_data = OrganizationMembership(**response.json())
223
+ return Response(response=response, data=parsed_data)
224
+ except ValidationError as e:
225
+ raise ParameterError(f"Invalid parameters: {e.errors()}") from e
226
+
227
+ @require_auth
228
+ def set_organization_membership(
229
+ self, org: str, username: str, role: str = "member"
230
+ ) -> Response:
231
+ """Add a user to an organization or update their role.
232
+
233
+ Invites the user when they are not yet a member (membership state
234
+ 'pending' until accepted). Role is 'member' or 'admin'.
235
+
236
+ PUT /orgs/{org}/memberships/{username}
237
+ https://docs.github.com/en/rest/orgs/members#set-organization-membership-for-a-user
238
+ """
239
+ response = self._session.put(
240
+ url=f"{self.url}/orgs/{org}/memberships/{username}",
241
+ json={"role": role},
242
+ headers=self.headers,
243
+ verify=self.verify,
244
+ proxies=self.proxies,
245
+ )
246
+ response.raise_for_status()
247
+ try:
248
+ parsed_data = OrganizationMembership(**response.json())
249
+ return Response(response=response, data=parsed_data)
250
+ except ValidationError as e:
251
+ raise ParameterError(f"Invalid parameters: {e.errors()}") from e
252
+
253
+ @require_auth
254
+ def remove_organization_member(self, org: str, username: str) -> Response:
255
+ """Remove a user from an organization (repositories access included).
256
+
257
+ Destructive: the MCP layer gates this action behind
258
+ allow_destructive / GITHUB_ALLOW_DESTRUCTIVE. GitHub responds 204.
259
+
260
+ DELETE /orgs/{org}/members/{username}
261
+ https://docs.github.com/en/rest/orgs/members#remove-an-organization-member
262
+ """
263
+ response = self._session.delete(
264
+ url=f"{self.url}/orgs/{org}/members/{username}",
265
+ headers=self.headers,
266
+ verify=self.verify,
267
+ proxies=self.proxies,
268
+ )
269
+ response.raise_for_status()
270
+ return Response(response=response, data={"status": "removed"})
271
+
272
+ @require_auth
273
+ def get_org_members(self, **kwargs) -> Response:
274
+ """List members for an organization."""
275
+ model = OrgMemberModel(**kwargs)
276
+ response, data = self._fetch_all_pages(f"/orgs/{model.org}/members", model)
277
+ try:
278
+ parsed_data = [User(**item) for item in data]
279
+ return Response(response=response, data=parsed_data)
280
+ except ValidationError as e:
281
+ raise ParameterError(f"Invalid parameters: {e.errors()}") from e
282
+
283
+ @require_auth
284
+ def get_org_teams(self, org: str) -> Response:
285
+ """List teams for an organization."""
286
+ response = self._session.get(
287
+ url=f"{self.url}/orgs/{org}/teams",
288
+ headers=self.headers,
289
+ verify=self.verify,
290
+ proxies=self.proxies,
291
+ )
292
+ response.raise_for_status()
293
+ return Response(response=response, data=response.json())
@@ -0,0 +1,234 @@
1
+ #!/usr/bin/env python
2
+ import requests
3
+ from agent_utilities.decorators import require_auth
4
+ from agent_utilities.exceptions import (
5
+ ParameterError,
6
+ )
7
+ from pydantic import ValidationError
8
+
9
+ from github_agent.api.api_client_base import BaseApiClient
10
+ from github_agent.github_input_models import (
11
+ PagesBuildsModel,
12
+ PagesUpdateModel,
13
+ )
14
+ from github_agent.github_response_models import (
15
+ PagesAlreadyEnabled,
16
+ PagesBuild,
17
+ PagesBuildRequest,
18
+ PagesNotEnabled,
19
+ PagesSite,
20
+ Response,
21
+ )
22
+
23
+
24
+ def _normalize_pages_source(source: str | dict) -> dict:
25
+ """Coerce a Pages publishing source into the API's {'branch', 'path'} shape.
26
+
27
+ Accepts a branch name string or a {'branch': ..., 'path': ...} mapping;
28
+ path defaults to '/'.
29
+ """
30
+ if isinstance(source, str):
31
+ return {"branch": source, "path": "/"}
32
+ if isinstance(source, dict) and "branch" in source:
33
+ return {"branch": source["branch"], "path": source.get("path", "/")}
34
+ raise ParameterError(
35
+ "Invalid 'source': expected a branch name string or a mapping with "
36
+ "'branch' (and an optional 'path' of '/' or '/docs')."
37
+ )
38
+
39
+
40
+ class Api(BaseApiClient):
41
+ @require_auth
42
+ def get_pages(self, owner: str, repo: str) -> Response:
43
+ """Get the GitHub Pages site configuration for a repository.
44
+
45
+ Returns PagesSite data when Pages is enabled. When the repository has
46
+ no Pages site GitHub responds 404 — instead of raising, this returns
47
+ a typed PagesNotEnabled result so callers can branch on it.
48
+
49
+ GET /repos/{owner}/{repo}/pages
50
+ https://docs.github.com/en/rest/pages/pages#get-a-apiname-pages-site
51
+ """
52
+ response = self._session.get(
53
+ url=f"{self.url}/repos/{owner}/{repo}/pages",
54
+ headers=self.headers,
55
+ verify=self.verify,
56
+ proxies=self.proxies,
57
+ )
58
+ try:
59
+ response.raise_for_status()
60
+ except requests.exceptions.HTTPError as e:
61
+ if e.response is not None and e.response.status_code == 404:
62
+ return Response(response=response, data=PagesNotEnabled())
63
+ raise
64
+ try:
65
+ parsed_data = PagesSite(**response.json())
66
+ return Response(response=response, data=parsed_data)
67
+ except ValidationError as e:
68
+ raise ParameterError(f"Invalid parameters: {e.errors()}") from e
69
+
70
+ @require_auth
71
+ def create_pages(
72
+ self,
73
+ owner: str,
74
+ repo: str,
75
+ build_type: str = "workflow",
76
+ source: str | dict | None = None,
77
+ ) -> Response:
78
+ """Enable GitHub Pages for a repository (HTTP 201).
79
+
80
+ build_type 'workflow' (default) publishes via GitHub Actions and
81
+ needs no source; 'legacy' publishes from a branch and requires
82
+ source (a branch name string or {'branch', 'path'} mapping).
83
+
84
+ When Pages is already enabled GitHub responds 409 Conflict — instead
85
+ of raising, this returns a typed PagesAlreadyEnabled result.
86
+
87
+ POST /repos/{owner}/{repo}/pages
88
+ https://docs.github.com/en/rest/pages/pages#create-a-apiname-pages-site
89
+ """
90
+ if build_type not in ("legacy", "workflow"):
91
+ raise ParameterError(
92
+ "Invalid 'build_type': must be 'legacy' or 'workflow'."
93
+ )
94
+ payload: dict = {"build_type": build_type}
95
+ if source is not None:
96
+ payload["source"] = _normalize_pages_source(source)
97
+ elif build_type == "legacy":
98
+ raise ParameterError(
99
+ "build_type 'legacy' requires 'source' (a branch name string "
100
+ "or a {'branch', 'path'} mapping)."
101
+ )
102
+ response = self._session.post(
103
+ url=f"{self.url}/repos/{owner}/{repo}/pages",
104
+ json=payload,
105
+ headers=self.headers,
106
+ verify=self.verify,
107
+ proxies=self.proxies,
108
+ )
109
+ try:
110
+ response.raise_for_status()
111
+ except requests.exceptions.HTTPError as e:
112
+ if e.response is not None and e.response.status_code == 409:
113
+ return Response(response=response, data=PagesAlreadyEnabled())
114
+ raise
115
+ try:
116
+ parsed_data = PagesSite(**response.json())
117
+ return Response(response=response, data=parsed_data)
118
+ except ValidationError as e:
119
+ raise ParameterError(f"Invalid parameters: {e.errors()}") from e
120
+
121
+ @require_auth
122
+ def update_pages(self, owner: str, repo: str, **kwargs) -> Response:
123
+ """Update the GitHub Pages configuration for a repository.
124
+
125
+ Accepts the documented mutable fields (validated by PagesUpdateModel):
126
+ build_type ('legacy' or 'workflow'), source (a branch name string or
127
+ {'branch', 'path'} mapping, for legacy builds), cname (custom
128
+ domain), https_enforced. GitHub responds 204 No Content.
129
+
130
+ PUT /repos/{owner}/{repo}/pages
131
+ https://docs.github.com/en/rest/pages/pages#update-information-about-a-apiname-pages-site
132
+ """
133
+ try:
134
+ model = PagesUpdateModel(**kwargs)
135
+ except ValidationError as e:
136
+ raise ParameterError(f"Invalid parameters: {e.errors()}") from e
137
+ payload = model.model_dump(exclude_none=True)
138
+ if not payload:
139
+ raise ParameterError(
140
+ "No fields to update: pass at least one of build_type, "
141
+ "source, cname, https_enforced."
142
+ )
143
+ if "source" in payload:
144
+ payload["source"] = _normalize_pages_source(payload["source"])
145
+ response = self._session.put(
146
+ url=f"{self.url}/repos/{owner}/{repo}/pages",
147
+ json=payload,
148
+ headers=self.headers,
149
+ verify=self.verify,
150
+ proxies=self.proxies,
151
+ )
152
+ response.raise_for_status()
153
+ return Response(response=response, data={"status": "pages_updated"})
154
+
155
+ @require_auth
156
+ def delete_pages(self, owner: str, repo: str) -> Response:
157
+ """Disable GitHub Pages for a repository and delete the site.
158
+
159
+ Destructive: the MCP layer gates this action behind
160
+ allow_destructive / GITHUB_ALLOW_DESTRUCTIVE. GitHub responds 204.
161
+
162
+ DELETE /repos/{owner}/{repo}/pages
163
+ https://docs.github.com/en/rest/pages/pages#delete-a-apiname-pages-site
164
+ """
165
+ response = self._session.delete(
166
+ url=f"{self.url}/repos/{owner}/{repo}/pages",
167
+ headers=self.headers,
168
+ verify=self.verify,
169
+ proxies=self.proxies,
170
+ )
171
+ response.raise_for_status()
172
+ return Response(response=response, data={"status": "pages_deleted"})
173
+
174
+ @require_auth
175
+ def get_pages_build_latest(self, owner: str, repo: str) -> Response:
176
+ """Get the latest GitHub Pages build for a repository.
177
+
178
+ GET /repos/{owner}/{repo}/pages/builds/latest
179
+ https://docs.github.com/en/rest/pages/pages#get-latest-pages-build
180
+ """
181
+ response = self._session.get(
182
+ url=f"{self.url}/repos/{owner}/{repo}/pages/builds/latest",
183
+ headers=self.headers,
184
+ verify=self.verify,
185
+ proxies=self.proxies,
186
+ )
187
+ response.raise_for_status()
188
+ try:
189
+ parsed_data = PagesBuild(**response.json())
190
+ return Response(response=response, data=parsed_data)
191
+ except ValidationError as e:
192
+ raise ParameterError(f"Invalid parameters: {e.errors()}") from e
193
+
194
+ @require_auth
195
+ def list_pages_builds(self, **kwargs) -> Response:
196
+ """List GitHub Pages builds for a repository (newest first).
197
+
198
+ GET /repos/{owner}/{repo}/pages/builds
199
+ https://docs.github.com/en/rest/pages/pages#list-apiname-pages-builds
200
+ """
201
+ model = PagesBuildsModel(**kwargs)
202
+ response, data = self._fetch_all_pages(
203
+ f"/repos/{model.owner}/{model.repo}/pages/builds", model
204
+ )
205
+ try:
206
+ parsed_data = [PagesBuild(**item) for item in data]
207
+ return Response(response=response, data=parsed_data)
208
+ except ValidationError as e:
209
+ raise ParameterError(f"Invalid parameters: {e.errors()}") from e
210
+
211
+ @require_auth
212
+ def request_pages_build(self, owner: str, repo: str) -> Response:
213
+ """Request a fresh GitHub Pages build without pushing a commit.
214
+
215
+ The programmatic fix for the first-deploy race where the initial
216
+ Pages build never runs (e.g. Pages enabled after the deploy workflow
217
+ already finished): trigger a build of the latest revision on demand.
218
+ GitHub responds 201 with the queued build request.
219
+
220
+ POST /repos/{owner}/{repo}/pages/builds
221
+ https://docs.github.com/en/rest/pages/pages#request-a-apiname-pages-build
222
+ """
223
+ response = self._session.post(
224
+ url=f"{self.url}/repos/{owner}/{repo}/pages/builds",
225
+ headers=self.headers,
226
+ verify=self.verify,
227
+ proxies=self.proxies,
228
+ )
229
+ response.raise_for_status()
230
+ try:
231
+ parsed_data = PagesBuildRequest(**response.json())
232
+ return Response(response=response, data=parsed_data)
233
+ except ValidationError as e:
234
+ raise ParameterError(f"Invalid parameters: {e.errors()}") from e
@@ -5,6 +5,7 @@ from github_agent.api.api_client_commits import Api as CommitsApi
5
5
  from github_agent.api.api_client_contents import Api as ContentsApi
6
6
  from github_agent.api.api_client_issues import Api as IssuesApi
7
7
  from github_agent.api.api_client_orgs import Api as OrgsApi
8
+ from github_agent.api.api_client_pages import Api as PagesApi
8
9
  from github_agent.api.api_client_pulls import Api as PullsApi
9
10
  from github_agent.api.api_client_releases import Api as ReleasesApi
10
11
  from github_agent.api.api_client_repos import Api as ReposApi
@@ -16,6 +17,14 @@ from github_agent.github_response_models import ( # noqa: F401
16
17
  Commit,
17
18
  Content,
18
19
  Issue,
20
+ Organization,
21
+ OrganizationMembership,
22
+ OrganizationSummary,
23
+ PagesAlreadyEnabled,
24
+ PagesBuild,
25
+ PagesBuildRequest,
26
+ PagesNotEnabled,
27
+ PagesSite,
19
28
  PullRequest,
20
29
  Release,
21
30
  Repository,
@@ -32,6 +41,7 @@ class Api(
32
41
  ContentsApi,
33
42
  IssuesApi,
34
43
  OrgsApi,
44
+ PagesApi,
35
45
  PullsApi,
36
46
  ReleasesApi,
37
47
  ReposApi,
@@ -13,6 +13,16 @@ from github_agent.api_client import Api
13
13
  logger = get_logger(__name__)
14
14
 
15
15
 
16
+ def allow_destructive_default() -> bool:
17
+ """Fleet-wide default for the destructive-action gate.
18
+
19
+ Destructive MCP actions (e.g. organization delete, member removal) are
20
+ blocked unless the caller passes allow_destructive=true or the
21
+ GITHUB_ALLOW_DESTRUCTIVE environment variable is set truthy.
22
+ """
23
+ return to_boolean(string=os.getenv("GITHUB_ALLOW_DESTRUCTIVE", "False"))
24
+
25
+
16
26
  def get_client(config: dict | None = None) -> Api:
17
27
  """
18
28
  Factory function to create the GitHub Api client.