agentbridge-cli 0.1.11__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.
- agentbridge_cli-0.1.11/.codex/config.toml +1 -0
- agentbridge_cli-0.1.11/.codex/skills/build-release/SKILL.md +98 -0
- agentbridge_cli-0.1.11/.codex/skills/build-release/agents/openai.yaml +4 -0
- agentbridge_cli-0.1.11/.codex/skills/build-release/scripts/release_build.py +403 -0
- agentbridge_cli-0.1.11/.env.example +20 -0
- agentbridge_cli-0.1.11/.github/dependabot.yml +23 -0
- agentbridge_cli-0.1.11/.github/workflows/ci.yml +36 -0
- agentbridge_cli-0.1.11/.github/workflows/dependency-review.yml +51 -0
- agentbridge_cli-0.1.11/.github/workflows/release.yml +228 -0
- agentbridge_cli-0.1.11/.gitignore +57 -0
- agentbridge_cli-0.1.11/AGENTS.md +53 -0
- agentbridge_cli-0.1.11/CLAUDE.md +1 -0
- agentbridge_cli-0.1.11/LICENSE +21 -0
- agentbridge_cli-0.1.11/Makefile +5 -0
- agentbridge_cli-0.1.11/PKG-INFO +157 -0
- agentbridge_cli-0.1.11/README.md +127 -0
- agentbridge_cli-0.1.11/TODO.txt +1 -0
- agentbridge_cli-0.1.11/agentbridge/__init__.py +8 -0
- agentbridge_cli-0.1.11/agentbridge/_build_info.py +1 -0
- agentbridge_cli-0.1.11/agentbridge/config.py +85 -0
- agentbridge_cli-0.1.11/agentbridge/dashboard.py +494 -0
- agentbridge_cli-0.1.11/agentbridge/models.py +334 -0
- agentbridge_cli-0.1.11/agentbridge/pool.py +338 -0
- agentbridge_cli-0.1.11/agentbridge/server.py +2881 -0
- agentbridge_cli-0.1.11/agentbridge/templates/dashboard/base.html +160 -0
- agentbridge_cli-0.1.11/agentbridge/templates/dashboard/chat.html +1361 -0
- agentbridge_cli-0.1.11/agentbridge/templates/dashboard/detail.html +142 -0
- agentbridge_cli-0.1.11/agentbridge/templates/dashboard/page.html +900 -0
- agentbridge_cli-0.1.11/agentbridge/templates/dashboard/pool.html +9 -0
- agentbridge_cli-0.1.11/agentbridge/templates/dashboard/requests.html +67 -0
- agentbridge_cli-0.1.11/architecture.png +0 -0
- agentbridge_cli-0.1.11/docs/plans/2026-02-14-dashboard-design.md +104 -0
- agentbridge_cli-0.1.11/docs/plans/2026-02-14-dashboard-implementation.md +1036 -0
- agentbridge_cli-0.1.11/hatch_build.py +24 -0
- agentbridge_cli-0.1.11/image-assets/icon/icon-1024.png +0 -0
- agentbridge_cli-0.1.11/image-assets/logo/logo-1024.png +0 -0
- agentbridge_cli-0.1.11/image-assets/manifest.json +77 -0
- agentbridge_cli-0.1.11/image-assets/sources/icon-source-keyed.png +0 -0
- agentbridge_cli-0.1.11/image-assets/sources/icon-source.png +0 -0
- agentbridge_cli-0.1.11/image-assets/sources/logo-source-keyed.png +0 -0
- agentbridge_cli-0.1.11/image-assets/sources/logo-source.png +0 -0
- agentbridge_cli-0.1.11/image-assets/sources/social-background-source.png +0 -0
- agentbridge_cli-0.1.11/image-assets/sources/social-source.png +0 -0
- agentbridge_cli-0.1.11/image-assets/web-seo/android-chrome-192.png +0 -0
- agentbridge_cli-0.1.11/image-assets/web-seo/android-chrome-512.png +0 -0
- agentbridge_cli-0.1.11/image-assets/web-seo/apple-touch-icon.png +0 -0
- agentbridge_cli-0.1.11/image-assets/web-seo/favicon/favicon-16.png +0 -0
- agentbridge_cli-0.1.11/image-assets/web-seo/favicon/favicon-32.png +0 -0
- agentbridge_cli-0.1.11/image-assets/web-seo/favicon/favicon-48.png +0 -0
- agentbridge_cli-0.1.11/image-assets/web-seo/favicon/favicon.ico +0 -0
- agentbridge_cli-0.1.11/image-assets/web-seo/og-image-1200x630.png +0 -0
- agentbridge_cli-0.1.11/image-assets/web-seo/site.webmanifest +21 -0
- agentbridge_cli-0.1.11/logo.png +0 -0
- agentbridge_cli-0.1.11/pyproject.toml +66 -0
- agentbridge_cli-0.1.11/scripts/dashboard_load_test.py +188 -0
- agentbridge_cli-0.1.11/tests/fixtures/ocr_test_document.png +0 -0
- agentbridge_cli-0.1.11/tests/test_config.py +41 -0
- agentbridge_cli-0.1.11/tests/test_dashboard.py +598 -0
- agentbridge_cli-0.1.11/tests/test_image_utils.py +471 -0
- agentbridge_cli-0.1.11/tests/test_model_mapping.py +303 -0
- agentbridge_cli-0.1.11/tests/test_models.py +584 -0
- agentbridge_cli-0.1.11/tests/test_pool.py +601 -0
- agentbridge_cli-0.1.11/tests/test_server.py +1101 -0
- agentbridge_cli-0.1.11/tests/test_session_logger.py +328 -0
- agentbridge_cli-0.1.11/uv.lock +1151 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sandbox_mode = "workspace-write"
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: build-release
|
|
3
|
+
description: Build, publish, and verify AgentBridge releases. Use when the user invokes /build-release or asks to cut, launch, tag, publish, monitor, or verify an agentbridge-cli PyPI or GitHub release.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Build Release
|
|
7
|
+
|
|
8
|
+
Use the repository-owned GitHub Actions workflow and PyPI Trusted Publishing.
|
|
9
|
+
Do not create a local tag or upload with local credentials: pushing a new
|
|
10
|
+
`pyproject.toml` version to `main` triggers `.github/workflows/release.yml`,
|
|
11
|
+
which tests, scans, builds, creates `v<version>`, creates the GitHub Release,
|
|
12
|
+
and publishes `agentbridge-cli`.
|
|
13
|
+
|
|
14
|
+
Use the next unused patch version unless the user requests another valid,
|
|
15
|
+
unused semantic version. Treat an untagged version absent from PyPI as pending.
|
|
16
|
+
|
|
17
|
+
## Flow
|
|
18
|
+
|
|
19
|
+
1. Fetch release state and require a clean, synchronized `main` worktree:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
git fetch origin main --tags
|
|
23
|
+
git status --short --branch
|
|
24
|
+
git log --oneline origin/main..HEAD
|
|
25
|
+
git log --oneline HEAD..origin/main
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Stop on unrelated changes, unpublished commits, divergence, or another branch.
|
|
29
|
+
Do not clean, pull, commit unrelated files, or switch branches.
|
|
30
|
+
|
|
31
|
+
2. Select the release version from the repository root:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
python3 .codex/skills/build-release/scripts/release_build.py next-version
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Update only `[project].version` in `pyproject.toml` with `apply_patch`, then
|
|
38
|
+
refresh the lockfile mechanically:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
uv lock
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
3. Run the deterministic release preflight with the exact version:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
python3 .codex/skills/build-release/scripts/release_build.py preflight --version <version>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The preflight requires consistent project and lock metadata, an unused PyPI
|
|
51
|
+
version and tag, clean patch formatting, a frozen lock, passing tests on Python
|
|
52
|
+
3.12 and 3.13, Ruff, a successful wheel and sdist build, artifact audits, and a
|
|
53
|
+
wheel installation smoke test. Stop at the exact failed gate.
|
|
54
|
+
|
|
55
|
+
4. Review and publish the release commit:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
git diff --check
|
|
59
|
+
git diff -- pyproject.toml uv.lock
|
|
60
|
+
git status --short
|
|
61
|
+
git add pyproject.toml uv.lock <other-intended-release-files>
|
|
62
|
+
git commit -m "Release <version>"
|
|
63
|
+
release_sha="$(git rev-parse HEAD)"
|
|
64
|
+
git push origin HEAD:main
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Never stage unrelated user changes. Do not create the tag locally; the release
|
|
68
|
+
workflow owns it.
|
|
69
|
+
|
|
70
|
+
5. Find and monitor the release workflow for the pushed commit:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
gh run list --workflow release.yml --commit "$release_sha" --limit 5 \
|
|
74
|
+
--json databaseId,status,conclusion,event,headSha,url
|
|
75
|
+
gh run watch <run-id> --exit-status
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
If the commit-filtered result has not appeared, poll briefly. Select the
|
|
79
|
+
`push` run for the exact SHA. A manual dispatch publishes only when its
|
|
80
|
+
`publish` input is explicitly true.
|
|
81
|
+
|
|
82
|
+
6. After the workflow succeeds, verify exact-version files on PyPI and inspect
|
|
83
|
+
the GitHub Release:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
python3 .codex/skills/build-release/scripts/release_build.py wait-pypi --version <version>
|
|
87
|
+
gh release view v<version> --json url,tagName,assets
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Do not report success until PyPI returns files for the exact version. If the
|
|
91
|
+
workflow fails, inspect `gh run view <run-id> --log-failed` and report the
|
|
92
|
+
failed gate before considering recovery.
|
|
93
|
+
|
|
94
|
+
## Final Response
|
|
95
|
+
|
|
96
|
+
Lead with the exact PyPI version URL. Report the tag, release workflow URL and
|
|
97
|
+
conclusion, GitHub Release URL, pushed commit, and every distribution filename.
|
|
98
|
+
On failure, report the exact command or job and the next safe recovery action.
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Select, build, audit, and verify agentbridge-cli releases."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import email.parser
|
|
8
|
+
import hashlib
|
|
9
|
+
import json
|
|
10
|
+
import re
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
import tarfile
|
|
14
|
+
import tempfile
|
|
15
|
+
import time
|
|
16
|
+
import tomllib
|
|
17
|
+
import urllib.error
|
|
18
|
+
import urllib.request
|
|
19
|
+
import zipfile
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
REPO_ROOT = Path(__file__).resolve().parents[4]
|
|
23
|
+
PACKAGE_NAME = "agentbridge-cli"
|
|
24
|
+
IMPORT_NAME = "agentbridge"
|
|
25
|
+
VERSION_PATTERN = re.compile(r"^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$")
|
|
26
|
+
TEST_PYTHONS = ("3.12", "3.13")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def run(args: list[str], *, capture: bool = False) -> str:
|
|
30
|
+
print("+", " ".join(args), flush=True)
|
|
31
|
+
result = subprocess.run(
|
|
32
|
+
args,
|
|
33
|
+
cwd=REPO_ROOT,
|
|
34
|
+
check=True,
|
|
35
|
+
text=True,
|
|
36
|
+
stdout=subprocess.PIPE if capture else None,
|
|
37
|
+
)
|
|
38
|
+
return result.stdout.strip() if capture else ""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def read_toml(path: Path) -> dict[str, object]:
|
|
42
|
+
with path.open("rb") as stream:
|
|
43
|
+
return tomllib.load(stream)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def parse_version(version: str) -> tuple[int, int, int]:
|
|
47
|
+
match = VERSION_PATTERN.fullmatch(version)
|
|
48
|
+
if match is None:
|
|
49
|
+
raise SystemExit(f"release version must be MAJOR.MINOR.PATCH: {version!r}")
|
|
50
|
+
return tuple(int(part) for part in match.groups()) # type: ignore[return-value]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def project_metadata() -> dict[str, object]:
|
|
54
|
+
project = read_toml(REPO_ROOT / "pyproject.toml").get("project")
|
|
55
|
+
if not isinstance(project, dict):
|
|
56
|
+
raise SystemExit("pyproject.toml is missing [project]")
|
|
57
|
+
return project
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def project_version() -> str:
|
|
61
|
+
version = project_metadata().get("version")
|
|
62
|
+
if not isinstance(version, str):
|
|
63
|
+
raise SystemExit("project.version must be a string")
|
|
64
|
+
parse_version(version)
|
|
65
|
+
return version
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def lock_version() -> str:
|
|
69
|
+
packages = read_toml(REPO_ROOT / "uv.lock").get("package", [])
|
|
70
|
+
if not isinstance(packages, list):
|
|
71
|
+
raise SystemExit("uv.lock has an invalid package list")
|
|
72
|
+
for package in packages:
|
|
73
|
+
if isinstance(package, dict) and package.get("name") == PACKAGE_NAME:
|
|
74
|
+
version = package.get("version")
|
|
75
|
+
if isinstance(version, str):
|
|
76
|
+
return version
|
|
77
|
+
raise SystemExit(f"uv.lock is missing {PACKAGE_NAME!r}")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def fetch_pypi() -> dict[str, object]:
|
|
81
|
+
url = f"https://pypi.org/pypi/{PACKAGE_NAME}/json"
|
|
82
|
+
try:
|
|
83
|
+
with urllib.request.urlopen(url, timeout=20) as response:
|
|
84
|
+
data = json.load(response)
|
|
85
|
+
except urllib.error.HTTPError as exc:
|
|
86
|
+
if exc.code == 404:
|
|
87
|
+
return {}
|
|
88
|
+
raise
|
|
89
|
+
if not isinstance(data, dict):
|
|
90
|
+
raise SystemExit("unexpected PyPI JSON response")
|
|
91
|
+
return data
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def pypi_files(version: str) -> list[dict[str, object]]:
|
|
95
|
+
releases = fetch_pypi().get("releases", {})
|
|
96
|
+
if not isinstance(releases, dict):
|
|
97
|
+
raise SystemExit("unexpected PyPI releases payload")
|
|
98
|
+
files = releases.get(version, [])
|
|
99
|
+
if not isinstance(files, list):
|
|
100
|
+
raise SystemExit(f"unexpected PyPI file payload for {version}")
|
|
101
|
+
return [item for item in files if isinstance(item, dict)]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def next_version(_: argparse.Namespace) -> None:
|
|
105
|
+
candidate = project_version()
|
|
106
|
+
releases = fetch_pypi().get("releases", {})
|
|
107
|
+
if not isinstance(releases, dict):
|
|
108
|
+
raise SystemExit("unexpected PyPI releases payload")
|
|
109
|
+
while releases.get(candidate):
|
|
110
|
+
major, minor, patch = parse_version(candidate)
|
|
111
|
+
candidate = f"{major}.{minor}.{patch + 1}"
|
|
112
|
+
print(candidate)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def tag_exists(version: str) -> bool:
|
|
116
|
+
tag = f"v{version}"
|
|
117
|
+
local = subprocess.run(
|
|
118
|
+
["git", "rev-parse", "-q", "--verify", f"refs/tags/{tag}"],
|
|
119
|
+
cwd=REPO_ROOT,
|
|
120
|
+
stdout=subprocess.DEVNULL,
|
|
121
|
+
stderr=subprocess.DEVNULL,
|
|
122
|
+
)
|
|
123
|
+
remote = subprocess.run(
|
|
124
|
+
["git", "ls-remote", "--exit-code", "--tags", "origin", f"refs/tags/{tag}"],
|
|
125
|
+
cwd=REPO_ROOT,
|
|
126
|
+
stdout=subprocess.DEVNULL,
|
|
127
|
+
stderr=subprocess.DEVNULL,
|
|
128
|
+
)
|
|
129
|
+
return local.returncode == 0 or remote.returncode == 0
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def check_version(version: str) -> None:
|
|
133
|
+
parse_version(version)
|
|
134
|
+
project = project_metadata()
|
|
135
|
+
actual = {
|
|
136
|
+
"project.name": project.get("name"),
|
|
137
|
+
"project.version": project.get("version"),
|
|
138
|
+
"project.requires-python": project.get("requires-python"),
|
|
139
|
+
"uv.lock version": lock_version(),
|
|
140
|
+
}
|
|
141
|
+
expected = {
|
|
142
|
+
"project.name": PACKAGE_NAME,
|
|
143
|
+
"project.version": version,
|
|
144
|
+
"project.requires-python": ">=3.12",
|
|
145
|
+
"uv.lock version": version,
|
|
146
|
+
}
|
|
147
|
+
mismatches = {
|
|
148
|
+
key: value for key, value in actual.items() if value != expected[key]
|
|
149
|
+
}
|
|
150
|
+
if mismatches:
|
|
151
|
+
details = ", ".join(
|
|
152
|
+
f"{key}={value!r}, expected {expected[key]!r}"
|
|
153
|
+
for key, value in mismatches.items()
|
|
154
|
+
)
|
|
155
|
+
raise SystemExit(f"release metadata mismatch: {details}")
|
|
156
|
+
if pypi_files(version):
|
|
157
|
+
raise SystemExit(f"{PACKAGE_NAME}=={version} already exists on PyPI")
|
|
158
|
+
if tag_exists(version):
|
|
159
|
+
raise SystemExit(f"v{version} already exists locally or on origin")
|
|
160
|
+
print(json.dumps(actual, indent=2))
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def sha256(path: Path) -> str:
|
|
164
|
+
digest = hashlib.sha256()
|
|
165
|
+
with path.open("rb") as stream:
|
|
166
|
+
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
|
167
|
+
digest.update(block)
|
|
168
|
+
return digest.hexdigest()
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def is_private_environment_file(name: str) -> bool:
|
|
172
|
+
basename = Path(name).name
|
|
173
|
+
return basename == ".env" or (
|
|
174
|
+
basename.startswith(".env.") and not basename.endswith(".example")
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def audit_wheel(wheel: Path, version: str) -> dict[str, object]:
|
|
179
|
+
with zipfile.ZipFile(wheel) as archive:
|
|
180
|
+
names = archive.namelist()
|
|
181
|
+
metadata_name = next(
|
|
182
|
+
(name for name in names if name.endswith(".dist-info/METADATA")),
|
|
183
|
+
None,
|
|
184
|
+
)
|
|
185
|
+
entry_points_name = next(
|
|
186
|
+
(name for name in names if name.endswith(".dist-info/entry_points.txt")),
|
|
187
|
+
None,
|
|
188
|
+
)
|
|
189
|
+
metadata = (
|
|
190
|
+
email.parser.Parser().parsestr(archive.read(metadata_name).decode())
|
|
191
|
+
if metadata_name is not None
|
|
192
|
+
else None
|
|
193
|
+
)
|
|
194
|
+
entry_points = (
|
|
195
|
+
archive.read(entry_points_name).decode()
|
|
196
|
+
if entry_points_name is not None
|
|
197
|
+
else ""
|
|
198
|
+
)
|
|
199
|
+
required = {
|
|
200
|
+
f"{IMPORT_NAME}/__init__.py",
|
|
201
|
+
f"{IMPORT_NAME}/server.py",
|
|
202
|
+
f"{IMPORT_NAME}/dashboard.py",
|
|
203
|
+
f"{IMPORT_NAME}/models.py",
|
|
204
|
+
f"{IMPORT_NAME}/pool.py",
|
|
205
|
+
f"{IMPORT_NAME}/config.py",
|
|
206
|
+
f"{IMPORT_NAME}/_build_info.py",
|
|
207
|
+
f"{IMPORT_NAME}/templates/dashboard/base.html",
|
|
208
|
+
f"{IMPORT_NAME}/templates/dashboard/chat.html",
|
|
209
|
+
f"{IMPORT_NAME}/templates/dashboard/detail.html",
|
|
210
|
+
f"{IMPORT_NAME}/templates/dashboard/page.html",
|
|
211
|
+
f"{IMPORT_NAME}/templates/dashboard/pool.html",
|
|
212
|
+
f"{IMPORT_NAME}/templates/dashboard/requests.html",
|
|
213
|
+
}
|
|
214
|
+
checks = {
|
|
215
|
+
"filename_version": version in wheel.name,
|
|
216
|
+
"universal_wheel": wheel.name.endswith("-py3-none-any.whl"),
|
|
217
|
+
"metadata_name": metadata is not None and metadata.get("Name") == PACKAGE_NAME,
|
|
218
|
+
"metadata_version": metadata is not None and metadata.get("Version") == version,
|
|
219
|
+
"requires_python": metadata is not None and metadata.get("Requires-Python") == ">=3.12",
|
|
220
|
+
"console_script": "agentbridge = agentbridge.server:main" in entry_points,
|
|
221
|
+
"required_package_files": required.issubset(names),
|
|
222
|
+
"has_license": any(name.endswith(".dist-info/licenses/LICENSE") for name in names),
|
|
223
|
+
"no_cache_files": not any(
|
|
224
|
+
"__pycache__" in Path(name).parts or name.endswith(".pyc")
|
|
225
|
+
for name in names
|
|
226
|
+
),
|
|
227
|
+
"no_private_environment_files": not any(
|
|
228
|
+
is_private_environment_file(name) for name in names
|
|
229
|
+
),
|
|
230
|
+
}
|
|
231
|
+
failed = [name for name, passed in checks.items() if not passed]
|
|
232
|
+
result = {"file": wheel.name, "sha256": sha256(wheel), "checks": checks}
|
|
233
|
+
if failed:
|
|
234
|
+
print(json.dumps(result, indent=2), file=sys.stderr)
|
|
235
|
+
raise SystemExit(f"wheel audit failed: {failed}")
|
|
236
|
+
return result
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def audit_sdist(sdist: Path, version: str) -> dict[str, object]:
|
|
240
|
+
with tarfile.open(sdist, "r:gz") as archive:
|
|
241
|
+
members = archive.getmembers()
|
|
242
|
+
names = [member.name for member in members]
|
|
243
|
+
pyproject_member = next(
|
|
244
|
+
(member for member in members if member.name.endswith("/pyproject.toml")),
|
|
245
|
+
None,
|
|
246
|
+
)
|
|
247
|
+
if pyproject_member is None:
|
|
248
|
+
raise SystemExit("sdist is missing pyproject.toml")
|
|
249
|
+
stream = archive.extractfile(pyproject_member)
|
|
250
|
+
if stream is None:
|
|
251
|
+
raise SystemExit("could not read pyproject.toml from sdist")
|
|
252
|
+
built_project = tomllib.loads(stream.read().decode()).get("project", {})
|
|
253
|
+
forbidden_parts = {".git", ".venv", "__pycache__", "dist", "logs"}
|
|
254
|
+
checks = {
|
|
255
|
+
"filename_version": version in sdist.name,
|
|
256
|
+
"metadata_name": (
|
|
257
|
+
isinstance(built_project, dict)
|
|
258
|
+
and built_project.get("name") == PACKAGE_NAME
|
|
259
|
+
),
|
|
260
|
+
"metadata_version": (
|
|
261
|
+
isinstance(built_project, dict)
|
|
262
|
+
and built_project.get("version") == version
|
|
263
|
+
),
|
|
264
|
+
"has_license": any(name.endswith("/LICENSE") for name in names),
|
|
265
|
+
"has_readme": any(name.endswith("/README.md") for name in names),
|
|
266
|
+
"has_build_hook": any(name.endswith("/hatch_build.py") for name in names),
|
|
267
|
+
"has_package": any(name.endswith(f"/{IMPORT_NAME}/server.py") for name in names),
|
|
268
|
+
"no_build_outputs": not any(
|
|
269
|
+
forbidden_parts.intersection(Path(name).parts) for name in names
|
|
270
|
+
),
|
|
271
|
+
"no_private_environment_files": not any(
|
|
272
|
+
is_private_environment_file(name) for name in names
|
|
273
|
+
),
|
|
274
|
+
}
|
|
275
|
+
failed = [name for name, passed in checks.items() if not passed]
|
|
276
|
+
result = {"file": sdist.name, "sha256": sha256(sdist), "checks": checks}
|
|
277
|
+
if failed:
|
|
278
|
+
print(json.dumps(result, indent=2), file=sys.stderr)
|
|
279
|
+
raise SystemExit(f"sdist audit failed: {failed}")
|
|
280
|
+
return result
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def smoke_wheel(wheel: Path, version: str) -> None:
|
|
284
|
+
python = run(["uv", "python", "find", "3.12"], capture=True)
|
|
285
|
+
with tempfile.TemporaryDirectory(prefix="agentbridge-smoke-") as directory:
|
|
286
|
+
environment = Path(directory) / "venv"
|
|
287
|
+
run(["uv", "venv", "--python", python, str(environment)])
|
|
288
|
+
installed_python = environment / "bin" / "python"
|
|
289
|
+
run(
|
|
290
|
+
[
|
|
291
|
+
"uv",
|
|
292
|
+
"pip",
|
|
293
|
+
"install",
|
|
294
|
+
"--python",
|
|
295
|
+
str(installed_python),
|
|
296
|
+
"--no-deps",
|
|
297
|
+
str(wheel),
|
|
298
|
+
]
|
|
299
|
+
)
|
|
300
|
+
code = (
|
|
301
|
+
"from importlib.metadata import version; "
|
|
302
|
+
"import agentbridge; "
|
|
303
|
+
f"assert version('{PACKAGE_NAME}') == '{version}'; "
|
|
304
|
+
f"assert agentbridge.__version__ == '{version}'; "
|
|
305
|
+
"print(agentbridge.__version__)"
|
|
306
|
+
)
|
|
307
|
+
run([str(installed_python), "-c", code])
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def preflight(args: argparse.Namespace) -> None:
|
|
311
|
+
version = args.version
|
|
312
|
+
check_version(version)
|
|
313
|
+
run(["git", "diff", "--check"])
|
|
314
|
+
run(["uv", "lock", "--check"])
|
|
315
|
+
for python in TEST_PYTHONS:
|
|
316
|
+
run(
|
|
317
|
+
[
|
|
318
|
+
"uv",
|
|
319
|
+
"run",
|
|
320
|
+
"--frozen",
|
|
321
|
+
"--extra",
|
|
322
|
+
"test",
|
|
323
|
+
"--python",
|
|
324
|
+
python,
|
|
325
|
+
"pytest",
|
|
326
|
+
"-q",
|
|
327
|
+
]
|
|
328
|
+
)
|
|
329
|
+
run(
|
|
330
|
+
[
|
|
331
|
+
"uv",
|
|
332
|
+
"run",
|
|
333
|
+
"--frozen",
|
|
334
|
+
"--extra",
|
|
335
|
+
"test",
|
|
336
|
+
"--python",
|
|
337
|
+
TEST_PYTHONS[0],
|
|
338
|
+
"ruff",
|
|
339
|
+
"check",
|
|
340
|
+
"agentbridge",
|
|
341
|
+
"tests",
|
|
342
|
+
".codex/skills/build-release/scripts/release_build.py",
|
|
343
|
+
]
|
|
344
|
+
)
|
|
345
|
+
with tempfile.TemporaryDirectory(prefix="agentbridge-release-") as directory:
|
|
346
|
+
output = Path(directory) / "dist"
|
|
347
|
+
run(["uv", "build", "--out-dir", str(output)])
|
|
348
|
+
wheels = sorted(output.glob("*.whl"))
|
|
349
|
+
sdists = sorted(output.glob("*.tar.gz"))
|
|
350
|
+
artifacts = [path for path in output.iterdir() if path.name != ".gitignore"]
|
|
351
|
+
if len(wheels) != 1 or len(sdists) != 1 or len(artifacts) != 2:
|
|
352
|
+
raise SystemExit(
|
|
353
|
+
f"expected one wheel and one sdist, found {sorted(path.name for path in artifacts)}"
|
|
354
|
+
)
|
|
355
|
+
results = [audit_wheel(wheels[0], version), audit_sdist(sdists[0], version)]
|
|
356
|
+
smoke_wheel(wheels[0], version)
|
|
357
|
+
print(
|
|
358
|
+
json.dumps(
|
|
359
|
+
{"package": PACKAGE_NAME, "version": version, "artifacts": results},
|
|
360
|
+
indent=2,
|
|
361
|
+
)
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def wait_pypi(args: argparse.Namespace) -> None:
|
|
366
|
+
parse_version(args.version)
|
|
367
|
+
for attempt in range(1, args.attempts + 1):
|
|
368
|
+
files = pypi_files(args.version)
|
|
369
|
+
if files:
|
|
370
|
+
print(f"https://pypi.org/project/{PACKAGE_NAME}/{args.version}/")
|
|
371
|
+
for item in files:
|
|
372
|
+
filename = item.get("filename")
|
|
373
|
+
if isinstance(filename, str):
|
|
374
|
+
print(filename)
|
|
375
|
+
return
|
|
376
|
+
print(f"waiting for {PACKAGE_NAME} {args.version} ({attempt}/{args.attempts})", flush=True)
|
|
377
|
+
time.sleep(args.interval)
|
|
378
|
+
raise SystemExit(f"{PACKAGE_NAME} {args.version} did not appear on PyPI")
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def main() -> None:
|
|
382
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
383
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
384
|
+
|
|
385
|
+
next_parser = commands.add_parser("next-version")
|
|
386
|
+
next_parser.set_defaults(func=next_version)
|
|
387
|
+
|
|
388
|
+
preflight_parser = commands.add_parser("preflight")
|
|
389
|
+
preflight_parser.add_argument("--version", required=True)
|
|
390
|
+
preflight_parser.set_defaults(func=preflight)
|
|
391
|
+
|
|
392
|
+
wait_parser = commands.add_parser("wait-pypi")
|
|
393
|
+
wait_parser.add_argument("--version", required=True)
|
|
394
|
+
wait_parser.add_argument("--attempts", type=int, default=60)
|
|
395
|
+
wait_parser.add_argument("--interval", type=float, default=10)
|
|
396
|
+
wait_parser.set_defaults(func=wait_pypi)
|
|
397
|
+
|
|
398
|
+
args = parser.parse_args()
|
|
399
|
+
args.func(args)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
if __name__ == "__main__":
|
|
403
|
+
main()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Optional: Set custom port
|
|
2
|
+
# PORT=8082
|
|
3
|
+
|
|
4
|
+
# Optional: Set number of pooled clients
|
|
5
|
+
# POOL_SIZE=3
|
|
6
|
+
|
|
7
|
+
# Optional: Set request timeout in seconds
|
|
8
|
+
# CLAUDE_TIMEOUT=120
|
|
9
|
+
|
|
10
|
+
# Optional: Set Codex request timeout in seconds
|
|
11
|
+
# CODEX_TIMEOUT=600
|
|
12
|
+
|
|
13
|
+
# Optional: Native Codex image generation timeout and raster bounds
|
|
14
|
+
# CODEX_IMAGE_TIMEOUT=600
|
|
15
|
+
# MAX_IMAGE_INPUT_BYTES=67108864
|
|
16
|
+
# MAX_IMAGE_OUTPUT_BYTES=33554432
|
|
17
|
+
# MAX_IMAGE_PIXELS=40000000
|
|
18
|
+
|
|
19
|
+
# Optional: OpenRouter requests can read this from ~/.config/agentbridge/.env
|
|
20
|
+
# OPENROUTER_API_KEY=sk-or-...
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
updates:
|
|
3
|
+
- package-ecosystem: uv
|
|
4
|
+
directory: /
|
|
5
|
+
schedule:
|
|
6
|
+
interval: weekly
|
|
7
|
+
day: monday
|
|
8
|
+
time: "08:00"
|
|
9
|
+
timezone: Europe/Lisbon
|
|
10
|
+
cooldown:
|
|
11
|
+
default-days: 7
|
|
12
|
+
open-pull-requests-limit: 5
|
|
13
|
+
|
|
14
|
+
- package-ecosystem: github-actions
|
|
15
|
+
directory: /
|
|
16
|
+
schedule:
|
|
17
|
+
interval: weekly
|
|
18
|
+
day: monday
|
|
19
|
+
time: "08:00"
|
|
20
|
+
timezone: Europe/Lisbon
|
|
21
|
+
cooldown:
|
|
22
|
+
default-days: 7
|
|
23
|
+
open-pull-requests-limit: 5
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
test:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
strategy:
|
|
16
|
+
matrix:
|
|
17
|
+
python-version: ["3.12", "3.13"]
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
|
20
|
+
with:
|
|
21
|
+
persist-credentials: false
|
|
22
|
+
|
|
23
|
+
- name: Install uv
|
|
24
|
+
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5
|
|
25
|
+
with:
|
|
26
|
+
enable-cache: true
|
|
27
|
+
cache-dependency-glob: "uv.lock"
|
|
28
|
+
|
|
29
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
30
|
+
run: uv python install ${{ matrix.python-version }}
|
|
31
|
+
|
|
32
|
+
- name: Install dependencies
|
|
33
|
+
run: uv sync --all-extras --python ${{ matrix.python-version }}
|
|
34
|
+
|
|
35
|
+
- name: Run tests
|
|
36
|
+
run: uv run --python ${{ matrix.python-version }} pytest tests/ -v --tb=short
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
name: Dependency review
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
push:
|
|
8
|
+
branches:
|
|
9
|
+
- main
|
|
10
|
+
merge_group:
|
|
11
|
+
types:
|
|
12
|
+
- checks_requested
|
|
13
|
+
|
|
14
|
+
permissions:
|
|
15
|
+
contents: read
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
dependency-review:
|
|
19
|
+
name: Dependency review
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
steps:
|
|
22
|
+
- name: Checkout repository
|
|
23
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
24
|
+
|
|
25
|
+
- name: Dependency review (pull request)
|
|
26
|
+
if: ${{ github.event_name == 'pull_request' }}
|
|
27
|
+
uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
|
|
28
|
+
with:
|
|
29
|
+
fail-on-severity: high
|
|
30
|
+
fail-on-scopes: runtime, development, unknown
|
|
31
|
+
retry-on-snapshot-warnings: true
|
|
32
|
+
|
|
33
|
+
- name: Dependency review (push audit)
|
|
34
|
+
if: ${{ github.event_name == 'push' }}
|
|
35
|
+
uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
|
|
36
|
+
with:
|
|
37
|
+
fail-on-severity: high
|
|
38
|
+
fail-on-scopes: runtime, development, unknown
|
|
39
|
+
retry-on-snapshot-warnings: true
|
|
40
|
+
base-ref: ${{ github.event.before }}
|
|
41
|
+
head-ref: ${{ github.sha }}
|
|
42
|
+
|
|
43
|
+
- name: Dependency review (merge group)
|
|
44
|
+
if: ${{ github.event_name == 'merge_group' }}
|
|
45
|
+
uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
|
|
46
|
+
with:
|
|
47
|
+
fail-on-severity: high
|
|
48
|
+
fail-on-scopes: runtime, development, unknown
|
|
49
|
+
retry-on-snapshot-warnings: true
|
|
50
|
+
base-ref: ${{ github.event.merge_group.base_sha }}
|
|
51
|
+
head-ref: ${{ github.event.merge_group.head_sha }}
|