tai42-tools-github 0.1.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.
- tai42_tools_github-0.1.0/LICENSE +202 -0
- tai42_tools_github-0.1.0/MANIFEST.in +1 -0
- tai42_tools_github-0.1.0/NOTICE +5 -0
- tai42_tools_github-0.1.0/PKG-INFO +128 -0
- tai42_tools_github-0.1.0/README.md +94 -0
- tai42_tools_github-0.1.0/pyproject.toml +103 -0
- tai42_tools_github-0.1.0/setup.cfg +4 -0
- tai42_tools_github-0.1.0/src/tai42_tools_github/__init__.py +9 -0
- tai42_tools_github-0.1.0/src/tai42_tools_github/_internal/__init__.py +6 -0
- tai42_tools_github-0.1.0/src/tai42_tools_github/_internal/tools/__init__.py +6 -0
- tai42_tools_github-0.1.0/src/tai42_tools_github/_internal/tools/github_client.py +212 -0
- tai42_tools_github-0.1.0/src/tai42_tools_github/docs/index.mdx +40 -0
- tai42_tools_github-0.1.0/src/tai42_tools_github/py.typed +0 -0
- tai42_tools_github-0.1.0/src/tai42_tools_github/tai-plugin.yml +51 -0
- tai42_tools_github-0.1.0/src/tai42_tools_github/tools/__init__.py +4 -0
- tai42_tools_github-0.1.0/src/tai42_tools_github/tools/create_github_webhook.py +46 -0
- tai42_tools_github-0.1.0/src/tai42_tools_github/tools/delete_github_webhook.py +34 -0
- tai42_tools_github-0.1.0/src/tai42_tools_github/tools/list_github_webhooks.py +29 -0
- tai42_tools_github-0.1.0/src/tai42_tools_github.egg-info/PKG-INFO +128 -0
- tai42_tools_github-0.1.0/src/tai42_tools_github.egg-info/SOURCES.txt +21 -0
- tai42_tools_github-0.1.0/src/tai42_tools_github.egg-info/dependency_links.txt +1 -0
- tai42_tools_github-0.1.0/src/tai42_tools_github.egg-info/requires.txt +11 -0
- tai42_tools_github-0.1.0/src/tai42_tools_github.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright [yyyy] [name of copyright owner]
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
prune tests
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tai42-tools-github
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: GitHub provisioning tools for the TAI ecosystem: manifest-loaded repository-webhook setup tools that create a repository webhook, list a repository's webhooks with Link-header pagination, and delete a repository webhook — the provider-side counterpart to the runtime signature verifier.
|
|
5
|
+
Author-email: tai42 <oss@tai42.ai>
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://tai42.ai
|
|
8
|
+
Project-URL: Repository, https://github.com/tai42ai/tai42/tree/main/plugins/tools-github
|
|
9
|
+
Project-URL: Issues, https://github.com/tai42ai/tai42/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/tai42ai/tai42/blob/main/plugins/tools-github/CHANGELOG.md
|
|
11
|
+
Keywords: tai,tools,github,webhooks,provisioning
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.13
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
License-File: NOTICE
|
|
23
|
+
Requires-Dist: tai42-contract<2,>=1.6
|
|
24
|
+
Requires-Dist: tai42-kit[curl]<2,>=1.5
|
|
25
|
+
Requires-Dist: pydantic>=2.12
|
|
26
|
+
Requires-Dist: pydantic-settings>=2.11
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-cov>=5; extra == "dev"
|
|
30
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
31
|
+
Requires-Dist: pyright>=1.1.389; extra == "dev"
|
|
32
|
+
Requires-Dist: pyyaml>=6; extra == "dev"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# tai42-tools-github
|
|
36
|
+
|
|
37
|
+
[](LICENSE)
|
|
38
|
+
|
|
39
|
+
GitHub provisioning tools for the TAI ecosystem — manifest-loaded repository-webhook
|
|
40
|
+
setup tools that create a repository webhook, list a repository's webhooks (following
|
|
41
|
+
`Link`-header pagination to exhaustion), and delete a repository webhook. They are the
|
|
42
|
+
provider-side setup counterpart to a runtime webhook-signature verifier: these tools
|
|
43
|
+
register the hook a verifier later authenticates.
|
|
44
|
+
|
|
45
|
+
All GitHub traffic is direct REST v3 through tai42-kit's curl client (no SDK); the
|
|
46
|
+
`X-GitHub-Api-Version` header is pinned in code. The package registers its tools through
|
|
47
|
+
the `tai42_app` handle from `tai42_contract.app` and never imports the skeleton.
|
|
48
|
+
|
|
49
|
+
## The TAI ecosystem
|
|
50
|
+
|
|
51
|
+
TAI is an open-source runtime for MCP tools, agents, and workflows. A tool is a
|
|
52
|
+
callable the host loads from a plugin manifest; this package supplies the GitHub
|
|
53
|
+
repository-webhook provisioning tools. The ecosystem is open-ended, so this repo is these
|
|
54
|
+
tools' own full doc home, and the documentation site covers the platform-level story:
|
|
55
|
+
|
|
56
|
+
- Tools concept: https://tai42.ai/concepts/tools
|
|
57
|
+
- Build a tool (author guide): https://tai42.ai/guides/authors/tool
|
|
58
|
+
- Ecosystem catalog: https://tai42.ai/reference/catalog
|
|
59
|
+
|
|
60
|
+
## Install
|
|
61
|
+
|
|
62
|
+
Requires **Python 3.13+**. Install from PyPI into the environment that runs the
|
|
63
|
+
server:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
uv add tai42-tools-github
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Or from source — clone this repo and add it as an editable dependency; the
|
|
70
|
+
`tai42-*` dependencies resolve in-tree from the workspace.
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
git clone https://github.com/tai42ai/tai42 # next to your app checkout
|
|
74
|
+
cd /path/to/your/app
|
|
75
|
+
uv add --editable ../tai42/plugins/tools-github
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Catalog
|
|
79
|
+
|
|
80
|
+
| Tool | Description |
|
|
81
|
+
| --- | --- |
|
|
82
|
+
| `create_github_webhook` | Creates a repository webhook on `owner/name` and returns its id, delivery url, events and active flag. The caller supplies the signing secret; GitHub never returns it. |
|
|
83
|
+
| `list_github_webhooks` | Lists every webhook on `owner/name`, following the `Link` header to exhaustion, returning each hook's id, delivery url, events and active flag. |
|
|
84
|
+
| `delete_github_webhook` | Deletes the webhook `hook_id` from `owner/name`; succeeds only on GitHub's `204`, otherwise raises. |
|
|
85
|
+
|
|
86
|
+
## The signing secret is the caller's
|
|
87
|
+
|
|
88
|
+
GitHub does **not** mint the webhook secret — `create_github_webhook` sends the secret the
|
|
89
|
+
caller supplies, GitHub keys each delivery's `X-Hub-Signature-256` with it, and the caller
|
|
90
|
+
keeps its own copy for the verifier that authenticates inbound deliveries. GitHub never
|
|
91
|
+
returns the secret on create or list, and these tools never echo it: it appears in no
|
|
92
|
+
return value and in no raised message.
|
|
93
|
+
|
|
94
|
+
## Error model
|
|
95
|
+
|
|
96
|
+
Every non-2xx GitHub response raises loudly with the status and body — `create` and `list`
|
|
97
|
+
require a 2xx, `delete` requires exactly `204`. Nothing is caught or remapped to success,
|
|
98
|
+
and no raised message ever contains the token or the caller's secret. Redirects are not
|
|
99
|
+
followed: a 3xx off the pinned host would replay the `Authorization` header to another
|
|
100
|
+
origin, so it surfaces as its own status instead.
|
|
101
|
+
|
|
102
|
+
## Configuration
|
|
103
|
+
|
|
104
|
+
| Setting | Env var | Default | Purpose |
|
|
105
|
+
| --- | --- | --- | --- |
|
|
106
|
+
| Token | `TOOLS_GITHUB_TOKEN` | — | The GitHub token (`Authorization: Bearer`). Required. |
|
|
107
|
+
| API base | `TOOLS_GITHUB_API_BASE` | `https://api.github.com` | GitHub REST base. Privileged — receives the `Authorization` header. |
|
|
108
|
+
| Request timeout | `TOOLS_GITHUB_REQUEST_TIMEOUT_SECONDS` | `20` | Per-request timeout. |
|
|
109
|
+
|
|
110
|
+
The token needs the scope GitHub requires to administer repository webhooks
|
|
111
|
+
(`admin:repo_hook` on a classic token, or repository `Webhooks` read/write on a
|
|
112
|
+
fine-grained token). Secrets live only in the environment. A missing or empty token raises
|
|
113
|
+
loudly (fails closed) — never a silently-unauthenticated request.
|
|
114
|
+
|
|
115
|
+
## Development
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
uv venv --python 3.13
|
|
119
|
+
uv pip install --no-sources --extra dev --editable .
|
|
120
|
+
uv run --no-sync pytest --cov --cov-report=term-missing
|
|
121
|
+
uv run --no-sync ruff check .
|
|
122
|
+
uv run --no-sync ruff format --check .
|
|
123
|
+
uv run --no-sync pyright
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## License
|
|
127
|
+
|
|
128
|
+
Apache-2.0. See `LICENSE` and `NOTICE`.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# tai42-tools-github
|
|
2
|
+
|
|
3
|
+
[](LICENSE)
|
|
4
|
+
|
|
5
|
+
GitHub provisioning tools for the TAI ecosystem — manifest-loaded repository-webhook
|
|
6
|
+
setup tools that create a repository webhook, list a repository's webhooks (following
|
|
7
|
+
`Link`-header pagination to exhaustion), and delete a repository webhook. They are the
|
|
8
|
+
provider-side setup counterpart to a runtime webhook-signature verifier: these tools
|
|
9
|
+
register the hook a verifier later authenticates.
|
|
10
|
+
|
|
11
|
+
All GitHub traffic is direct REST v3 through tai42-kit's curl client (no SDK); the
|
|
12
|
+
`X-GitHub-Api-Version` header is pinned in code. The package registers its tools through
|
|
13
|
+
the `tai42_app` handle from `tai42_contract.app` and never imports the skeleton.
|
|
14
|
+
|
|
15
|
+
## The TAI ecosystem
|
|
16
|
+
|
|
17
|
+
TAI is an open-source runtime for MCP tools, agents, and workflows. A tool is a
|
|
18
|
+
callable the host loads from a plugin manifest; this package supplies the GitHub
|
|
19
|
+
repository-webhook provisioning tools. The ecosystem is open-ended, so this repo is these
|
|
20
|
+
tools' own full doc home, and the documentation site covers the platform-level story:
|
|
21
|
+
|
|
22
|
+
- Tools concept: https://tai42.ai/concepts/tools
|
|
23
|
+
- Build a tool (author guide): https://tai42.ai/guides/authors/tool
|
|
24
|
+
- Ecosystem catalog: https://tai42.ai/reference/catalog
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
Requires **Python 3.13+**. Install from PyPI into the environment that runs the
|
|
29
|
+
server:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
uv add tai42-tools-github
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Or from source — clone this repo and add it as an editable dependency; the
|
|
36
|
+
`tai42-*` dependencies resolve in-tree from the workspace.
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
git clone https://github.com/tai42ai/tai42 # next to your app checkout
|
|
40
|
+
cd /path/to/your/app
|
|
41
|
+
uv add --editable ../tai42/plugins/tools-github
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Catalog
|
|
45
|
+
|
|
46
|
+
| Tool | Description |
|
|
47
|
+
| --- | --- |
|
|
48
|
+
| `create_github_webhook` | Creates a repository webhook on `owner/name` and returns its id, delivery url, events and active flag. The caller supplies the signing secret; GitHub never returns it. |
|
|
49
|
+
| `list_github_webhooks` | Lists every webhook on `owner/name`, following the `Link` header to exhaustion, returning each hook's id, delivery url, events and active flag. |
|
|
50
|
+
| `delete_github_webhook` | Deletes the webhook `hook_id` from `owner/name`; succeeds only on GitHub's `204`, otherwise raises. |
|
|
51
|
+
|
|
52
|
+
## The signing secret is the caller's
|
|
53
|
+
|
|
54
|
+
GitHub does **not** mint the webhook secret — `create_github_webhook` sends the secret the
|
|
55
|
+
caller supplies, GitHub keys each delivery's `X-Hub-Signature-256` with it, and the caller
|
|
56
|
+
keeps its own copy for the verifier that authenticates inbound deliveries. GitHub never
|
|
57
|
+
returns the secret on create or list, and these tools never echo it: it appears in no
|
|
58
|
+
return value and in no raised message.
|
|
59
|
+
|
|
60
|
+
## Error model
|
|
61
|
+
|
|
62
|
+
Every non-2xx GitHub response raises loudly with the status and body — `create` and `list`
|
|
63
|
+
require a 2xx, `delete` requires exactly `204`. Nothing is caught or remapped to success,
|
|
64
|
+
and no raised message ever contains the token or the caller's secret. Redirects are not
|
|
65
|
+
followed: a 3xx off the pinned host would replay the `Authorization` header to another
|
|
66
|
+
origin, so it surfaces as its own status instead.
|
|
67
|
+
|
|
68
|
+
## Configuration
|
|
69
|
+
|
|
70
|
+
| Setting | Env var | Default | Purpose |
|
|
71
|
+
| --- | --- | --- | --- |
|
|
72
|
+
| Token | `TOOLS_GITHUB_TOKEN` | — | The GitHub token (`Authorization: Bearer`). Required. |
|
|
73
|
+
| API base | `TOOLS_GITHUB_API_BASE` | `https://api.github.com` | GitHub REST base. Privileged — receives the `Authorization` header. |
|
|
74
|
+
| Request timeout | `TOOLS_GITHUB_REQUEST_TIMEOUT_SECONDS` | `20` | Per-request timeout. |
|
|
75
|
+
|
|
76
|
+
The token needs the scope GitHub requires to administer repository webhooks
|
|
77
|
+
(`admin:repo_hook` on a classic token, or repository `Webhooks` read/write on a
|
|
78
|
+
fine-grained token). Secrets live only in the environment. A missing or empty token raises
|
|
79
|
+
loudly (fails closed) — never a silently-unauthenticated request.
|
|
80
|
+
|
|
81
|
+
## Development
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
uv venv --python 3.13
|
|
85
|
+
uv pip install --no-sources --extra dev --editable .
|
|
86
|
+
uv run --no-sync pytest --cov --cov-report=term-missing
|
|
87
|
+
uv run --no-sync ruff check .
|
|
88
|
+
uv run --no-sync ruff format --check .
|
|
89
|
+
uv run --no-sync pyright
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
Apache-2.0. See `LICENSE` and `NOTICE`.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tai42-tools-github"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "GitHub provisioning tools for the TAI ecosystem: manifest-loaded repository-webhook setup tools that create a repository webhook, list a repository's webhooks with Link-header pagination, and delete a repository webhook — the provider-side counterpart to the runtime signature verifier."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.13"
|
|
11
|
+
license = "Apache-2.0"
|
|
12
|
+
license-files = ["LICENSE", "NOTICE"]
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "tai42", email = "oss@tai42.ai" },
|
|
15
|
+
]
|
|
16
|
+
keywords = ["tai", "tools", "github", "webhooks", "provisioning"]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 4 - Beta",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"Operating System :: OS Independent",
|
|
23
|
+
"Topic :: Software Development :: Version Control :: Git",
|
|
24
|
+
"Typing :: Typed",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
# The pure interface contract these tools register through — the tai42_app
|
|
28
|
+
# handle and the tool facet.
|
|
29
|
+
"tai42-contract>=1.6,<2",
|
|
30
|
+
# Leaf helpers: settings machinery (TaiBaseSettings / settings_cache) and the
|
|
31
|
+
# curl client the GitHub REST module wires to.
|
|
32
|
+
"tai42-kit[curl]>=1.5,<2",
|
|
33
|
+
# Imported directly in the settings class: field/secret modelling.
|
|
34
|
+
"pydantic>=2.12",
|
|
35
|
+
"pydantic-settings>=2.11",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://tai42.ai"
|
|
40
|
+
Repository = "https://github.com/tai42ai/tai42/tree/main/plugins/tools-github"
|
|
41
|
+
Issues = "https://github.com/tai42ai/tai42/issues"
|
|
42
|
+
Changelog = "https://github.com/tai42ai/tai42/blob/main/plugins/tools-github/CHANGELOG.md"
|
|
43
|
+
|
|
44
|
+
[project.optional-dependencies]
|
|
45
|
+
# Public dev/test tooling.
|
|
46
|
+
dev = [
|
|
47
|
+
"pytest>=8",
|
|
48
|
+
"pytest-cov>=5",
|
|
49
|
+
"ruff>=0.6",
|
|
50
|
+
"pyright>=1.1.389",
|
|
51
|
+
# The plugin-spec test parses tai-plugin.yml.
|
|
52
|
+
"pyyaml>=6",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
[tool.setuptools]
|
|
56
|
+
package-dir = { "" = "src" }
|
|
57
|
+
|
|
58
|
+
[tool.setuptools.packages.find]
|
|
59
|
+
where = ["src"]
|
|
60
|
+
include = ["tai42_tools_github*"]
|
|
61
|
+
|
|
62
|
+
[tool.setuptools.package-data]
|
|
63
|
+
tai42_tools_github = ["py.typed", "tai-plugin.yml", "docs/*", "docs/images/*"]
|
|
64
|
+
|
|
65
|
+
[tool.uv]
|
|
66
|
+
cache-keys = [{ file = "pyproject.toml" }]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# The version floors in [project] are the compatibility contract; these sources
|
|
70
|
+
# resolve the tai42-* requirements to the in-tree workspace members instead of
|
|
71
|
+
# the published packages. They are not part of wheel metadata.
|
|
72
|
+
[tool.uv.sources]
|
|
73
|
+
tai42-contract = { workspace = true }
|
|
74
|
+
tai42-kit = { workspace = true }
|
|
75
|
+
[tool.ruff]
|
|
76
|
+
src = ["src"]
|
|
77
|
+
target-version = "py313"
|
|
78
|
+
line-length = 120
|
|
79
|
+
|
|
80
|
+
[tool.ruff.lint]
|
|
81
|
+
select = ["E", "W", "F", "I", "UP", "B", "C4", "SIM", "PIE", "PT", "RUF", "TID"]
|
|
82
|
+
|
|
83
|
+
[tool.ruff.lint.flake8-tidy-imports.banned-api]
|
|
84
|
+
"tai42_skeleton".msg = "tai42-tools-github is contract-facing: it never imports tai42-skeleton."
|
|
85
|
+
|
|
86
|
+
[tool.pytest.ini_options]
|
|
87
|
+
testpaths = ["tests"]
|
|
88
|
+
xfail_strict = true
|
|
89
|
+
# Turn warnings into errors so a real defect that only surfaces as a warning
|
|
90
|
+
# (e.g. a missing await -> "coroutine was never awaited") fails the suite loudly
|
|
91
|
+
# instead of passing green.
|
|
92
|
+
filterwarnings = ["error"]
|
|
93
|
+
|
|
94
|
+
[tool.coverage.run]
|
|
95
|
+
source = ["tai42_tools_github"]
|
|
96
|
+
|
|
97
|
+
[tool.coverage.report]
|
|
98
|
+
fail_under = 95
|
|
99
|
+
|
|
100
|
+
[tool.pyright]
|
|
101
|
+
include = ["src", "tests"]
|
|
102
|
+
pythonVersion = "3.13"
|
|
103
|
+
typeCheckingMode = "basic"
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""tai42-tools-github — GitHub provisioning tools for the TAI ecosystem.
|
|
2
|
+
|
|
3
|
+
An opt-in, manifest-loaded collection of GitHub repository-webhook setup tools: create a
|
|
4
|
+
repository webhook, list a repository's webhooks, and delete a repository webhook. These are the
|
|
5
|
+
provider-side provisioning counterpart to a runtime signature verifier — they set up the hooks a
|
|
6
|
+
verifier later authenticates. Nothing here is imported at package import time: each module registers
|
|
7
|
+
its tool through the global ``tai42_app`` handle and is loaded by the host via the manifest's
|
|
8
|
+
``tools[].module`` field.
|
|
9
|
+
"""
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""Private helper modules backing the GitHub provisioning tools.
|
|
2
|
+
|
|
3
|
+
Not part of the public API. ``tools`` holds the shared GitHub REST client that the thin registered
|
|
4
|
+
entrypoints in ``tai42_tools_github.tools`` delegate to. Nothing here registers through
|
|
5
|
+
``tai42_app``.
|
|
6
|
+
"""
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""Private helper modules backing the GitHub provisioning tools.
|
|
2
|
+
|
|
3
|
+
Not part of the public API: the module here holds the shared GitHub REST client (repository-webhook
|
|
4
|
+
create/list/delete, token auth, and Link-header pagination) that the registered entrypoints in
|
|
5
|
+
``tai42_tools_github.tools`` delegate to. Nothing here registers through ``tai42_app``.
|
|
6
|
+
"""
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""Direct GitHub REST client for the repository-webhook provisioning tools.
|
|
2
|
+
|
|
3
|
+
Speaks GitHub's REST v3 API through tai42-kit's curl client — no SDK. Creates, lists and deletes a
|
|
4
|
+
repository's webhooks under token auth, following the ``Link`` response header to page a list to
|
|
5
|
+
exhaustion under a hard page ceiling.
|
|
6
|
+
|
|
7
|
+
Facts pinned here are read from GitHub's published REST docs (https://docs.github.com/rest) and
|
|
8
|
+
nowhere else:
|
|
9
|
+
|
|
10
|
+
- Requests send ``Accept: application/vnd.github+json`` and pin ``X-GitHub-Api-Version`` so the
|
|
11
|
+
response shape cannot drift with GitHub's default (docs.github.com/rest/overview/api-versions);
|
|
12
|
+
auth is ``Authorization: Bearer <token>``.
|
|
13
|
+
- Create is ``POST /repos/{owner}/{repo}/hooks`` with ``config`` ``{url, content_type, secret}`` and
|
|
14
|
+
an ``events`` list; the response carries ``id``, ``config.url``, ``events`` and ``active`` and
|
|
15
|
+
never the secret (docs.github.com/rest/repos/webhooks#create-a-repository-webhook).
|
|
16
|
+
- List is ``GET /repos/{owner}/{repo}/hooks`` paginated by the ``Link`` header's ``rel="next"``
|
|
17
|
+
URL (docs.github.com/rest/using-the-rest-api/using-pagination-in-the-rest-api).
|
|
18
|
+
- Delete is ``DELETE /repos/{owner}/{repo}/hooks/{hook_id}`` and succeeds with ``204 No Content``
|
|
19
|
+
(docs.github.com/rest/repos/webhooks#delete-a-repository-webhook).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
import re
|
|
26
|
+
from typing import Any, cast
|
|
27
|
+
from urllib.parse import quote
|
|
28
|
+
|
|
29
|
+
from pydantic import SecretStr
|
|
30
|
+
from pydantic_settings import SettingsConfigDict
|
|
31
|
+
from tai42_contract.app import tai42_app
|
|
32
|
+
from tai42_kit.clients.impl.curl import CurlClient
|
|
33
|
+
from tai42_kit.settings import TaiBaseSettings, settings_cache
|
|
34
|
+
|
|
35
|
+
# The GitHub REST API version every request pins. Direct REST carries no SDK-pinned version, and an
|
|
36
|
+
# unpinned request is served GitHub's current default -- so the response shape could change under
|
|
37
|
+
# the tools without a release. Read from GitHub's published api-versions docs.
|
|
38
|
+
GITHUB_API_VERSION = "2022-11-28"
|
|
39
|
+
|
|
40
|
+
# The media type GitHub's REST v3 responses are negotiated under.
|
|
41
|
+
GITHUB_ACCEPT = "application/vnd.github+json"
|
|
42
|
+
|
|
43
|
+
# ``owner/name``: a non-empty owner and a non-empty name, one slash, no whitespace anywhere. Path
|
|
44
|
+
# segments are percent-encoded per part, so this guards shape, not character set.
|
|
45
|
+
_REPO_RE = re.compile(r"^[^/\s]+/[^/\s]+$")
|
|
46
|
+
|
|
47
|
+
# One ``<url>; rel="next"`` element of a ``Link`` response header.
|
|
48
|
+
_LINK_NEXT_RE = re.compile(r'<([^>]+)>\s*;\s*[^,]*\brel="next"')
|
|
49
|
+
|
|
50
|
+
# The hard page ceiling the list loop follows ``Link``'s ``rel="next"`` under. Finite pagination is
|
|
51
|
+
# the server's promise, not the client's guarantee: a buggy or hostile ``Link`` header that always
|
|
52
|
+
# names a next page would loop forever, so the loop RAISES at the ceiling rather than spinning. A
|
|
53
|
+
# repository's webhook count is small, so a page far below this bound is the real terminal case.
|
|
54
|
+
_LIST_PAGE_CEILING = 100
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class GithubToolsSettings(TaiBaseSettings):
|
|
58
|
+
"""GitHub provisioning-tool configuration, read from ``TOOLS_GITHUB_``-prefixed env."""
|
|
59
|
+
|
|
60
|
+
model_config = SettingsConfigDict(env_prefix="TOOLS_GITHUB_")
|
|
61
|
+
|
|
62
|
+
token: SecretStr | None = None
|
|
63
|
+
api_base: str = "https://api.github.com"
|
|
64
|
+
request_timeout_seconds: float = 20
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@settings_cache
|
|
68
|
+
def github_tools_settings() -> GithubToolsSettings:
|
|
69
|
+
return GithubToolsSettings()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _token() -> str:
|
|
73
|
+
"""The configured GitHub token, revealed.
|
|
74
|
+
|
|
75
|
+
Missing and EMPTY are the same failure and both raise naming ``TOOLS_GITHUB_TOKEN``: an empty
|
|
76
|
+
env var parses to ``SecretStr("")`` (not ``None``), so a plain ``is None`` test would send
|
|
77
|
+
``Authorization: Bearer`` with no credential. This is the module's only read path for the value,
|
|
78
|
+
so the error is identical on every path.
|
|
79
|
+
"""
|
|
80
|
+
token = github_tools_settings().token
|
|
81
|
+
if not (token and token.get_secret_value()):
|
|
82
|
+
raise ValueError("TOOLS_GITHUB_TOKEN is not set (missing or empty)")
|
|
83
|
+
return token.get_secret_value()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _headers(*, json_body: bool) -> dict[str, str]:
|
|
87
|
+
"""The request headers for a GitHub REST call. The token rides ``Authorization`` and is never
|
|
88
|
+
echoed into any raised text or return value.
|
|
89
|
+
"""
|
|
90
|
+
headers = {
|
|
91
|
+
"Authorization": f"Bearer {_token()}",
|
|
92
|
+
"Accept": GITHUB_ACCEPT,
|
|
93
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION,
|
|
94
|
+
}
|
|
95
|
+
if json_body:
|
|
96
|
+
headers["Content-Type"] = "application/json"
|
|
97
|
+
return headers
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _split_repo(repo: str) -> tuple[str, str]:
|
|
101
|
+
"""Validate ``repo`` as exactly ``owner/name`` and return ``(owner, name)``.
|
|
102
|
+
|
|
103
|
+
Any other shape -- empty side, extra slash, whitespace, non-string -- raises loudly naming the
|
|
104
|
+
value, so a malformed repo can never be interpolated into a request path.
|
|
105
|
+
"""
|
|
106
|
+
if not (isinstance(repo, str) and _REPO_RE.match(repo)):
|
|
107
|
+
raise ValueError(f"repo must be 'owner/name'; got {repo!r}")
|
|
108
|
+
owner, name = repo.split("/")
|
|
109
|
+
return owner, name
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _hooks_url(owner: str, name: str) -> str:
|
|
113
|
+
"""The ``/repos/{owner}/{name}/hooks`` collection URL with each path segment percent-encoded
|
|
114
|
+
on its own (``safe=""`` encodes ``/ ? #``) so neither part can escape into a different path.
|
|
115
|
+
"""
|
|
116
|
+
return f"{github_tools_settings().api_base}/repos/{quote(owner, safe='')}/{quote(name, safe='')}/hooks"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
async def _http_request(
|
|
120
|
+
method: str,
|
|
121
|
+
url: str,
|
|
122
|
+
*,
|
|
123
|
+
headers: dict[str, str],
|
|
124
|
+
data: str | None = None,
|
|
125
|
+
) -> tuple[int, dict[str, str], str]:
|
|
126
|
+
"""Issue one request through a fresh curl session and return ``(status, lowercased headers, body
|
|
127
|
+
text)``. Redirects are OFF: every request carries ``Authorization: Bearer <token>`` and libcurl
|
|
128
|
+
replays custom headers across a redirect hop -- a 3xx off the pinned host would hand the token to
|
|
129
|
+
another host. A non-2xx is returned as its status and body (never raised here) -- the caller
|
|
130
|
+
decides what a status means.
|
|
131
|
+
"""
|
|
132
|
+
session_ctx = tai42_app.clients.client_ctx(CurlClient, session_params={}, fresh=True)
|
|
133
|
+
async with session_ctx as session:
|
|
134
|
+
resp = await session.request(
|
|
135
|
+
url=url,
|
|
136
|
+
method=cast("Any", method),
|
|
137
|
+
headers=headers,
|
|
138
|
+
data=data,
|
|
139
|
+
timeout=github_tools_settings().request_timeout_seconds,
|
|
140
|
+
allow_redirects=False,
|
|
141
|
+
)
|
|
142
|
+
resp_headers = {key.lower(): value for key, value in resp.headers.items() if value is not None}
|
|
143
|
+
return resp.status_code, resp_headers, resp.text
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _next_page_url(link_header: str | None) -> str | None:
|
|
147
|
+
"""The ``rel="next"`` URL from a ``Link`` response header, or ``None`` when the header is
|
|
148
|
+
absent or names no next page (the last page)."""
|
|
149
|
+
if not link_header:
|
|
150
|
+
return None
|
|
151
|
+
match = _LINK_NEXT_RE.search(link_header)
|
|
152
|
+
return match.group(1) if match else None
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
async def create_webhook(repo: str, url: str, events: list[str], secret: str) -> dict[str, Any]:
|
|
156
|
+
"""POST a new repository webhook and return ``{id, url, events, active}``.
|
|
157
|
+
|
|
158
|
+
``secret`` is sent inside ``config`` and is NEVER returned or echoed into a raised message --
|
|
159
|
+
GitHub does not return it, and the caller owns storing it. A non-2xx raises loudly with GitHub's
|
|
160
|
+
status and body (never caught, never remapped); GitHub's error body carries neither the token
|
|
161
|
+
nor the caller's secret.
|
|
162
|
+
"""
|
|
163
|
+
owner, name = _split_repo(repo)
|
|
164
|
+
body = json.dumps({"config": {"url": url, "content_type": "json", "secret": secret}, "events": events})
|
|
165
|
+
status, _resp_headers, text = await _http_request(
|
|
166
|
+
"POST", _hooks_url(owner, name), headers=_headers(json_body=True), data=body
|
|
167
|
+
)
|
|
168
|
+
if not 200 <= status < 300:
|
|
169
|
+
raise ValueError(f"GitHub webhook create failed: HTTP {status} {text}")
|
|
170
|
+
hook = json.loads(text)
|
|
171
|
+
return {"id": hook["id"], "url": hook["config"]["url"], "events": hook["events"], "active": hook["active"]}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
async def list_webhooks(repo: str) -> list[dict[str, Any]]:
|
|
175
|
+
"""GET every webhook on ``repo`` as ``{id, url, events, active}``, following the ``Link`` header
|
|
176
|
+
to exhaustion under ``_LIST_PAGE_CEILING``.
|
|
177
|
+
|
|
178
|
+
The loop terminates when GitHub names no ``rel="next"`` page, and RAISES at the page ceiling
|
|
179
|
+
rather than following a self-referential ``Link`` header forever. A non-2xx on any page raises
|
|
180
|
+
loudly with GitHub's status and body. GitHub never returns a hook's secret.
|
|
181
|
+
"""
|
|
182
|
+
owner, name = _split_repo(repo)
|
|
183
|
+
next_url: str | None = _hooks_url(owner, name)
|
|
184
|
+
hooks: list[dict[str, Any]] = []
|
|
185
|
+
pages = 0
|
|
186
|
+
while next_url is not None:
|
|
187
|
+
if pages >= _LIST_PAGE_CEILING:
|
|
188
|
+
raise ValueError(f"GitHub webhook list exceeded its {_LIST_PAGE_CEILING}-page ceiling for {repo!r}")
|
|
189
|
+
status, resp_headers, text = await _http_request("GET", next_url, headers=_headers(json_body=False))
|
|
190
|
+
if not 200 <= status < 300:
|
|
191
|
+
raise ValueError(f"GitHub webhook list failed: HTTP {status} {text}")
|
|
192
|
+
for hook in json.loads(text):
|
|
193
|
+
hooks.append(
|
|
194
|
+
{"id": hook["id"], "url": hook["config"]["url"], "events": hook["events"], "active": hook["active"]}
|
|
195
|
+
)
|
|
196
|
+
pages += 1
|
|
197
|
+
next_url = _next_page_url(resp_headers.get("link"))
|
|
198
|
+
return hooks
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
async def delete_webhook(repo: str, hook_id: int) -> dict[str, Any]:
|
|
202
|
+
"""DELETE one webhook and return ``{"repo", "hook_id", "deleted": True}`` on GitHub's ``204``.
|
|
203
|
+
|
|
204
|
+
Any status other than ``204`` -- a 404 for an unknown hook, any other non-2xx -- raises loudly
|
|
205
|
+
with GitHub's status and body, never a silent success.
|
|
206
|
+
"""
|
|
207
|
+
owner, name = _split_repo(repo)
|
|
208
|
+
url = f"{_hooks_url(owner, name)}/{hook_id}"
|
|
209
|
+
status, _resp_headers, text = await _http_request("DELETE", url, headers=_headers(json_body=False))
|
|
210
|
+
if status != 204:
|
|
211
|
+
raise ValueError(f"GitHub webhook delete failed: HTTP {status} {text}")
|
|
212
|
+
return {"repo": repo, "hook_id": hook_id, "deleted": True}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "GitHub provisioning tools"
|
|
3
|
+
description: "The three repository-webhook setup tools and the TOOLS_GITHUB settings that configure them."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Three tools that provision a GitHub repository's webhooks over the REST API: create a
|
|
7
|
+
webhook, list a repository's webhooks, and delete one. They are the provider-side setup
|
|
8
|
+
counterpart to a runtime webhook-signature verifier — the tools register the hook a
|
|
9
|
+
verifier later authenticates.
|
|
10
|
+
|
|
11
|
+
## Configuration
|
|
12
|
+
|
|
13
|
+
| Variable | Default | Effect |
|
|
14
|
+
| --- | --- | --- |
|
|
15
|
+
| `TOOLS_GITHUB_TOKEN` | unset | GitHub token (`Authorization: Bearer`). Missing or empty raises on every tool call. |
|
|
16
|
+
| `TOOLS_GITHUB_API_BASE` | `https://api.github.com` | GitHub REST API origin. Privileged — receives the `Authorization` header. |
|
|
17
|
+
| `TOOLS_GITHUB_REQUEST_TIMEOUT_SECONDS` | `20` | Per-request HTTP timeout in seconds. |
|
|
18
|
+
|
|
19
|
+
The token needs the scope GitHub requires to administer repository webhooks (the
|
|
20
|
+
`admin:repo_hook` scope on a classic token, or repository `Webhooks` read/write on a
|
|
21
|
+
fine-grained token). A missing or empty token raises loudly (fails closed) — never a
|
|
22
|
+
silently-unauthenticated request.
|
|
23
|
+
|
|
24
|
+
## create_github_webhook
|
|
25
|
+
|
|
26
|
+
Creates a repository webhook on `owner/name` and returns its id, delivery url, events and
|
|
27
|
+
active flag. GitHub does not mint the signing secret: the caller supplies it, GitHub keys
|
|
28
|
+
`X-Hub-Signature-256` with it, and the caller keeps its own copy — GitHub never returns
|
|
29
|
+
it and the tool never echoes it.
|
|
30
|
+
|
|
31
|
+
## list_github_webhooks
|
|
32
|
+
|
|
33
|
+
Lists every webhook on `owner/name`, following the `Link` response header to exhaustion,
|
|
34
|
+
and returns each hook's id, delivery url, events and active flag. GitHub never returns a
|
|
35
|
+
hook's secret.
|
|
36
|
+
|
|
37
|
+
## delete_github_webhook
|
|
38
|
+
|
|
39
|
+
Deletes the webhook `hook_id` from `owner/name`. Succeeds only on GitHub's `204`; an
|
|
40
|
+
unknown hook (404) or any other non-2xx raises, never a silent success.
|
|
File without changes
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
spec_version: 1
|
|
2
|
+
namespace: tai42
|
|
3
|
+
name: tools-github
|
|
4
|
+
package: tai42-tools-github
|
|
5
|
+
version: 0.1.0
|
|
6
|
+
description: >-
|
|
7
|
+
GitHub provisioning tools for the TAI ecosystem: manifest-loaded
|
|
8
|
+
repository-webhook setup tools that create a repository webhook, list a
|
|
9
|
+
repository's webhooks with Link-header pagination, and delete a repository
|
|
10
|
+
webhook — the provider-side counterpart to the runtime signature verifier.
|
|
11
|
+
license: Apache-2.0
|
|
12
|
+
homepage: https://tai42.ai
|
|
13
|
+
repository: https://github.com/tai42ai/tai42/tree/main/plugins/tools-github
|
|
14
|
+
contract: '>=1.6,<2'
|
|
15
|
+
categories:
|
|
16
|
+
- provisioning
|
|
17
|
+
- utilities
|
|
18
|
+
tags:
|
|
19
|
+
- github
|
|
20
|
+
- provisioning
|
|
21
|
+
- webhooks
|
|
22
|
+
permissions:
|
|
23
|
+
network: true
|
|
24
|
+
subprocess: false
|
|
25
|
+
filesystem: false
|
|
26
|
+
provides:
|
|
27
|
+
- kind: tool
|
|
28
|
+
name: create_github_webhook
|
|
29
|
+
module: tai42_tools_github.tools.create_github_webhook
|
|
30
|
+
description: >-
|
|
31
|
+
Create a repository webhook and return its id, url, events, and active
|
|
32
|
+
flag.
|
|
33
|
+
tags:
|
|
34
|
+
- github
|
|
35
|
+
- provisioning
|
|
36
|
+
- kind: tool
|
|
37
|
+
name: list_github_webhooks
|
|
38
|
+
module: tai42_tools_github.tools.list_github_webhooks
|
|
39
|
+
description: >-
|
|
40
|
+
List a repository's webhooks, following Link-header pagination to
|
|
41
|
+
exhaustion.
|
|
42
|
+
tags:
|
|
43
|
+
- github
|
|
44
|
+
- provisioning
|
|
45
|
+
- kind: tool
|
|
46
|
+
name: delete_github_webhook
|
|
47
|
+
module: tai42_tools_github.tools.delete_github_webhook
|
|
48
|
+
description: Delete a repository webhook by id.
|
|
49
|
+
tags:
|
|
50
|
+
- github
|
|
51
|
+
- provisioning
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""The ``create_github_webhook`` tool: provision a repository webhook on GitHub.
|
|
2
|
+
|
|
3
|
+
Creates a webhook on ``owner/name`` through the shared REST client and returns the created hook's
|
|
4
|
+
id, delivery url, events and active flag. The GitHub REST call and its version pin live in
|
|
5
|
+
:mod:`tai42_tools_github._internal.tools.github_client`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
from urllib.parse import urlparse
|
|
12
|
+
|
|
13
|
+
from tai42_contract.app import tai42_app
|
|
14
|
+
|
|
15
|
+
from tai42_tools_github._internal.tools.github_client import create_webhook
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@tai42_app.tools.tool(tags={"github", "provisioning"})
|
|
19
|
+
async def create_github_webhook(repo: str, url: str, events: list[str], secret: str) -> dict[str, Any]:
|
|
20
|
+
"""Create a repository webhook and return its id, url, events and active flag.
|
|
21
|
+
|
|
22
|
+
GitHub does not mint the signing secret: the caller supplies ``secret``, GitHub stores it, and
|
|
23
|
+
the caller is responsible for keeping its own copy -- GitHub never returns it and this tool
|
|
24
|
+
never echoes it. The returned dict carries nothing secret.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
repo: The target repository as ``"owner/name"``. Any other shape raises.
|
|
28
|
+
url: The ``https`` delivery URL GitHub POSTs events to. Required and non-empty.
|
|
29
|
+
events: The event names to subscribe (e.g. ``["push"]``). Required and non-empty.
|
|
30
|
+
secret: The webhook signing secret GitHub keys its ``X-Hub-Signature-256`` with. Required
|
|
31
|
+
and non-empty; never returned or echoed into an error.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
``{"id": <hook id>, "url": <delivery url>, "events": [...], "active": <bool>}``.
|
|
35
|
+
"""
|
|
36
|
+
if not url:
|
|
37
|
+
raise ValueError("url is required and must be non-empty")
|
|
38
|
+
parsed = urlparse(url)
|
|
39
|
+
if parsed.scheme != "https" or not parsed.netloc:
|
|
40
|
+
raise ValueError(f"url must be an https URL with a host; got {url!r}")
|
|
41
|
+
if not events:
|
|
42
|
+
raise ValueError("events is required and must be non-empty")
|
|
43
|
+
if not secret:
|
|
44
|
+
raise ValueError("secret is required and must be non-empty")
|
|
45
|
+
|
|
46
|
+
return await create_webhook(repo, url, events, secret)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""The ``delete_github_webhook`` tool: remove a repository webhook on GitHub.
|
|
2
|
+
|
|
3
|
+
Deletes the webhook ``hook_id`` from ``owner/name`` through the shared REST client. The GitHub REST
|
|
4
|
+
call lives in :mod:`tai42_tools_github._internal.tools.github_client`.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from tai42_contract.app import tai42_app
|
|
12
|
+
|
|
13
|
+
from tai42_tools_github._internal.tools.github_client import delete_webhook
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@tai42_app.tools.tool(tags={"github", "provisioning"})
|
|
17
|
+
async def delete_github_webhook(repo: str, hook_id: int) -> dict[str, Any]:
|
|
18
|
+
"""Delete a repository webhook by id.
|
|
19
|
+
|
|
20
|
+
Succeeds only on GitHub's ``204``; an unknown hook (404) or any other non-2xx raises, never a
|
|
21
|
+
silent success.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
repo: The repository as ``"owner/name"``. Any other shape raises.
|
|
25
|
+
hook_id: The webhook id to delete. Must be a positive integer.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
``{"repo": <repo>, "hook_id": <hook id>, "deleted": True}``.
|
|
29
|
+
"""
|
|
30
|
+
# ``bool`` is an ``int`` subclass; exclude it so ``True`` cannot pass as an id.
|
|
31
|
+
if isinstance(hook_id, bool) or not isinstance(hook_id, int) or hook_id <= 0:
|
|
32
|
+
raise ValueError(f"hook_id must be a positive integer; got {hook_id!r}")
|
|
33
|
+
|
|
34
|
+
return await delete_webhook(repo, hook_id)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""The ``list_github_webhooks`` tool: list a repository's webhooks on GitHub.
|
|
2
|
+
|
|
3
|
+
Lists every webhook on ``owner/name`` through the shared REST client, following the ``Link`` header
|
|
4
|
+
to exhaustion, and returns each hook's id, delivery url, events and active flag. The GitHub REST
|
|
5
|
+
call lives in :mod:`tai42_tools_github._internal.tools.github_client`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from tai42_contract.app import tai42_app
|
|
13
|
+
|
|
14
|
+
from tai42_tools_github._internal.tools.github_client import list_webhooks
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@tai42_app.tools.tool(tags={"github", "provisioning"})
|
|
18
|
+
async def list_github_webhooks(repo: str) -> list[dict[str, Any]]:
|
|
19
|
+
"""List a repository's webhooks, following Link-header pagination to exhaustion.
|
|
20
|
+
|
|
21
|
+
GitHub never returns a hook's signing secret, so no returned dict carries one.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
repo: The repository as ``"owner/name"``. Any other shape raises.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
One ``{"id": <hook id>, "url": <delivery url>, "events": [...], "active": <bool>}`` per hook.
|
|
28
|
+
"""
|
|
29
|
+
return await list_webhooks(repo)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tai42-tools-github
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: GitHub provisioning tools for the TAI ecosystem: manifest-loaded repository-webhook setup tools that create a repository webhook, list a repository's webhooks with Link-header pagination, and delete a repository webhook — the provider-side counterpart to the runtime signature verifier.
|
|
5
|
+
Author-email: tai42 <oss@tai42.ai>
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://tai42.ai
|
|
8
|
+
Project-URL: Repository, https://github.com/tai42ai/tai42/tree/main/plugins/tools-github
|
|
9
|
+
Project-URL: Issues, https://github.com/tai42ai/tai42/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/tai42ai/tai42/blob/main/plugins/tools-github/CHANGELOG.md
|
|
11
|
+
Keywords: tai,tools,github,webhooks,provisioning
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.13
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
License-File: NOTICE
|
|
23
|
+
Requires-Dist: tai42-contract<2,>=1.6
|
|
24
|
+
Requires-Dist: tai42-kit[curl]<2,>=1.5
|
|
25
|
+
Requires-Dist: pydantic>=2.12
|
|
26
|
+
Requires-Dist: pydantic-settings>=2.11
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-cov>=5; extra == "dev"
|
|
30
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
31
|
+
Requires-Dist: pyright>=1.1.389; extra == "dev"
|
|
32
|
+
Requires-Dist: pyyaml>=6; extra == "dev"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# tai42-tools-github
|
|
36
|
+
|
|
37
|
+
[](LICENSE)
|
|
38
|
+
|
|
39
|
+
GitHub provisioning tools for the TAI ecosystem — manifest-loaded repository-webhook
|
|
40
|
+
setup tools that create a repository webhook, list a repository's webhooks (following
|
|
41
|
+
`Link`-header pagination to exhaustion), and delete a repository webhook. They are the
|
|
42
|
+
provider-side setup counterpart to a runtime webhook-signature verifier: these tools
|
|
43
|
+
register the hook a verifier later authenticates.
|
|
44
|
+
|
|
45
|
+
All GitHub traffic is direct REST v3 through tai42-kit's curl client (no SDK); the
|
|
46
|
+
`X-GitHub-Api-Version` header is pinned in code. The package registers its tools through
|
|
47
|
+
the `tai42_app` handle from `tai42_contract.app` and never imports the skeleton.
|
|
48
|
+
|
|
49
|
+
## The TAI ecosystem
|
|
50
|
+
|
|
51
|
+
TAI is an open-source runtime for MCP tools, agents, and workflows. A tool is a
|
|
52
|
+
callable the host loads from a plugin manifest; this package supplies the GitHub
|
|
53
|
+
repository-webhook provisioning tools. The ecosystem is open-ended, so this repo is these
|
|
54
|
+
tools' own full doc home, and the documentation site covers the platform-level story:
|
|
55
|
+
|
|
56
|
+
- Tools concept: https://tai42.ai/concepts/tools
|
|
57
|
+
- Build a tool (author guide): https://tai42.ai/guides/authors/tool
|
|
58
|
+
- Ecosystem catalog: https://tai42.ai/reference/catalog
|
|
59
|
+
|
|
60
|
+
## Install
|
|
61
|
+
|
|
62
|
+
Requires **Python 3.13+**. Install from PyPI into the environment that runs the
|
|
63
|
+
server:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
uv add tai42-tools-github
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Or from source — clone this repo and add it as an editable dependency; the
|
|
70
|
+
`tai42-*` dependencies resolve in-tree from the workspace.
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
git clone https://github.com/tai42ai/tai42 # next to your app checkout
|
|
74
|
+
cd /path/to/your/app
|
|
75
|
+
uv add --editable ../tai42/plugins/tools-github
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Catalog
|
|
79
|
+
|
|
80
|
+
| Tool | Description |
|
|
81
|
+
| --- | --- |
|
|
82
|
+
| `create_github_webhook` | Creates a repository webhook on `owner/name` and returns its id, delivery url, events and active flag. The caller supplies the signing secret; GitHub never returns it. |
|
|
83
|
+
| `list_github_webhooks` | Lists every webhook on `owner/name`, following the `Link` header to exhaustion, returning each hook's id, delivery url, events and active flag. |
|
|
84
|
+
| `delete_github_webhook` | Deletes the webhook `hook_id` from `owner/name`; succeeds only on GitHub's `204`, otherwise raises. |
|
|
85
|
+
|
|
86
|
+
## The signing secret is the caller's
|
|
87
|
+
|
|
88
|
+
GitHub does **not** mint the webhook secret — `create_github_webhook` sends the secret the
|
|
89
|
+
caller supplies, GitHub keys each delivery's `X-Hub-Signature-256` with it, and the caller
|
|
90
|
+
keeps its own copy for the verifier that authenticates inbound deliveries. GitHub never
|
|
91
|
+
returns the secret on create or list, and these tools never echo it: it appears in no
|
|
92
|
+
return value and in no raised message.
|
|
93
|
+
|
|
94
|
+
## Error model
|
|
95
|
+
|
|
96
|
+
Every non-2xx GitHub response raises loudly with the status and body — `create` and `list`
|
|
97
|
+
require a 2xx, `delete` requires exactly `204`. Nothing is caught or remapped to success,
|
|
98
|
+
and no raised message ever contains the token or the caller's secret. Redirects are not
|
|
99
|
+
followed: a 3xx off the pinned host would replay the `Authorization` header to another
|
|
100
|
+
origin, so it surfaces as its own status instead.
|
|
101
|
+
|
|
102
|
+
## Configuration
|
|
103
|
+
|
|
104
|
+
| Setting | Env var | Default | Purpose |
|
|
105
|
+
| --- | --- | --- | --- |
|
|
106
|
+
| Token | `TOOLS_GITHUB_TOKEN` | — | The GitHub token (`Authorization: Bearer`). Required. |
|
|
107
|
+
| API base | `TOOLS_GITHUB_API_BASE` | `https://api.github.com` | GitHub REST base. Privileged — receives the `Authorization` header. |
|
|
108
|
+
| Request timeout | `TOOLS_GITHUB_REQUEST_TIMEOUT_SECONDS` | `20` | Per-request timeout. |
|
|
109
|
+
|
|
110
|
+
The token needs the scope GitHub requires to administer repository webhooks
|
|
111
|
+
(`admin:repo_hook` on a classic token, or repository `Webhooks` read/write on a
|
|
112
|
+
fine-grained token). Secrets live only in the environment. A missing or empty token raises
|
|
113
|
+
loudly (fails closed) — never a silently-unauthenticated request.
|
|
114
|
+
|
|
115
|
+
## Development
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
uv venv --python 3.13
|
|
119
|
+
uv pip install --no-sources --extra dev --editable .
|
|
120
|
+
uv run --no-sync pytest --cov --cov-report=term-missing
|
|
121
|
+
uv run --no-sync ruff check .
|
|
122
|
+
uv run --no-sync ruff format --check .
|
|
123
|
+
uv run --no-sync pyright
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## License
|
|
127
|
+
|
|
128
|
+
Apache-2.0. See `LICENSE` and `NOTICE`.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
NOTICE
|
|
4
|
+
README.md
|
|
5
|
+
pyproject.toml
|
|
6
|
+
src/tai42_tools_github/__init__.py
|
|
7
|
+
src/tai42_tools_github/py.typed
|
|
8
|
+
src/tai42_tools_github/tai-plugin.yml
|
|
9
|
+
src/tai42_tools_github.egg-info/PKG-INFO
|
|
10
|
+
src/tai42_tools_github.egg-info/SOURCES.txt
|
|
11
|
+
src/tai42_tools_github.egg-info/dependency_links.txt
|
|
12
|
+
src/tai42_tools_github.egg-info/requires.txt
|
|
13
|
+
src/tai42_tools_github.egg-info/top_level.txt
|
|
14
|
+
src/tai42_tools_github/_internal/__init__.py
|
|
15
|
+
src/tai42_tools_github/_internal/tools/__init__.py
|
|
16
|
+
src/tai42_tools_github/_internal/tools/github_client.py
|
|
17
|
+
src/tai42_tools_github/docs/index.mdx
|
|
18
|
+
src/tai42_tools_github/tools/__init__.py
|
|
19
|
+
src/tai42_tools_github/tools/create_github_webhook.py
|
|
20
|
+
src/tai42_tools_github/tools/delete_github_webhook.py
|
|
21
|
+
src/tai42_tools_github/tools/list_github_webhooks.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tai42_tools_github
|