codex-workspace-bootstrap 0.7.0__tar.gz → 0.8.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.
- {codex_workspace_bootstrap-0.7.0/src/codex_workspace_bootstrap.egg-info → codex_workspace_bootstrap-0.8.0}/PKG-INFO +51 -4
- {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/README.md +50 -3
- {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/pyproject.toml +1 -1
- {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/src/codex_workspace_bootstrap/__init__.py +1 -1
- {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/src/codex_workspace_bootstrap/audit.py +8 -6
- {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/src/codex_workspace_bootstrap/cli.py +19 -9
- codex_workspace_bootstrap-0.8.0/src/codex_workspace_bootstrap/integrations/__init__.py +1 -0
- codex_workspace_bootstrap-0.8.0/src/codex_workspace_bootstrap/integrations/github.py +79 -0
- codex_workspace_bootstrap-0.8.0/src/codex_workspace_bootstrap/integrations/github_actions_worker.py +266 -0
- codex_workspace_bootstrap-0.8.0/src/codex_workspace_bootstrap/integrations/github_app.py +30 -0
- codex_workspace_bootstrap-0.8.0/src/codex_workspace_bootstrap/integrations/github_app_service.py +94 -0
- codex_workspace_bootstrap-0.8.0/src/codex_workspace_bootstrap/integrations/github_checkout.py +274 -0
- codex_workspace_bootstrap-0.8.0/src/codex_workspace_bootstrap/integrations/github_cloud_run.py +821 -0
- codex_workspace_bootstrap-0.8.0/src/codex_workspace_bootstrap/integrations/github_delivery.py +220 -0
- codex_workspace_bootstrap-0.8.0/src/codex_workspace_bootstrap/integrations/github_manifest.py +204 -0
- codex_workspace_bootstrap-0.8.0/src/codex_workspace_bootstrap/integrations/github_runtime.py +332 -0
- codex_workspace_bootstrap-0.8.0/src/codex_workspace_bootstrap/integrations/github_webhook.py +136 -0
- {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/src/codex_workspace_bootstrap/preflight.py +70 -4
- {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0/src/codex_workspace_bootstrap.egg-info}/PKG-INFO +51 -4
- codex_workspace_bootstrap-0.8.0/src/codex_workspace_bootstrap.egg-info/SOURCES.txt +47 -0
- {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/tests/test_audit.py +24 -0
- {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/tests/test_cli.py +32 -1
- codex_workspace_bootstrap-0.8.0/tests/test_github_actions_worker.py +208 -0
- codex_workspace_bootstrap-0.8.0/tests/test_github_app_registration.py +34 -0
- codex_workspace_bootstrap-0.8.0/tests/test_github_app_service.py +144 -0
- codex_workspace_bootstrap-0.8.0/tests/test_github_checkout.py +217 -0
- codex_workspace_bootstrap-0.8.0/tests/test_github_cloud_run.py +297 -0
- codex_workspace_bootstrap-0.8.0/tests/test_github_delivery.py +266 -0
- codex_workspace_bootstrap-0.8.0/tests/test_github_integration.py +94 -0
- codex_workspace_bootstrap-0.8.0/tests/test_github_manifest.py +161 -0
- codex_workspace_bootstrap-0.8.0/tests/test_github_runtime.py +570 -0
- codex_workspace_bootstrap-0.8.0/tests/test_github_webhook.py +160 -0
- {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/tests/test_preflight.py +132 -0
- {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/tests/test_public_repo_patterns.py +30 -0
- codex_workspace_bootstrap-0.7.0/src/codex_workspace_bootstrap.egg-info/SOURCES.txt +0 -26
- {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/LICENSE +0 -0
- {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/setup.cfg +0 -0
- {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/src/codex_workspace_bootstrap/__main__.py +0 -0
- {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/src/codex_workspace_bootstrap/agents.py +0 -0
- {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/src/codex_workspace_bootstrap/doctor.py +0 -0
- {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/src/codex_workspace_bootstrap/fixes.py +0 -0
- {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/src/codex_workspace_bootstrap/instructions.py +0 -0
- {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/src/codex_workspace_bootstrap/sarif.py +0 -0
- {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/src/codex_workspace_bootstrap.egg-info/dependency_links.txt +0 -0
- {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/src/codex_workspace_bootstrap.egg-info/entry_points.txt +0 -0
- {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/src/codex_workspace_bootstrap.egg-info/top_level.txt +0 -0
- {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/tests/test_doctor.py +0 -0
- {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/tests/test_instruction_integrity.py +0 -0
- {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/tests/test_release_consistency.py +0 -0
- {codex_workspace_bootstrap-0.7.0 → codex_workspace_bootstrap-0.8.0}/tests/test_sarif.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codex-workspace-bootstrap
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Preflight AI coding repositories before Codex, Copilot, Cline, Claude, Gemini, Continue, or Cursor touches them.
|
|
5
5
|
Author: kohli217
|
|
6
6
|
License: MIT
|
|
@@ -40,10 +40,23 @@ Dynamic: license-file
|
|
|
40
40
|
|
|
41
41
|
Windows-first. Local by default. CI-friendly. Designed for repositories used with Codex, Copilot, Cline, Claude Code, Gemini CLI, Continue, Cursor, and similar coding agents.
|
|
42
42
|
|
|
43
|
-
[日本語ガイド](docs/README.ja.md) · [Examples](docs/EXAMPLES.md) · [GitHub Action](docs/GITHUB_ACTION.md) · [Roadmap](docs/ROADMAP.md) · [Releases](https://github.com/kohli217/codex-workspace-bootstrap/releases)
|
|
43
|
+
[日本語ガイド](docs/README.ja.md) · [Examples](docs/EXAMPLES.md) · [GitHub Action](docs/GITHUB_ACTION.md) · [GitHub App](docs/GITHUB_APP.md) · [Free GitHub App deployment](deploy/cloudflare/README.md) · [Cloud Run alternative](deploy/cloudrun/README.md) · [Integration contract](docs/INTEGRATIONS.md) · [Roadmap](docs/ROADMAP.md) · [Releases](https://github.com/kohli217/codex-workspace-bootstrap/releases)
|
|
44
44
|
|
|
45
45
|
> Community-maintained project. Not an official OpenAI product and not affiliated with OpenAI.
|
|
46
46
|
|
|
47
|
+
## Why add this to your repository?
|
|
48
|
+
|
|
49
|
+
AI coding agents can move fast, but they can also follow stale or conflicting repository instructions. `codex-workspace-bootstrap` adds one repeatable check before that happens.
|
|
50
|
+
|
|
51
|
+
| Common failure | What `cwb` does |
|
|
52
|
+
| --- | --- |
|
|
53
|
+
| One instruction says `npm`, another says `pnpm` | Flags the mismatch before an agent edits the repository. |
|
|
54
|
+
| An instruction tells the agent to run a test/lint script that does not exist | Reports the invalid command instead of letting the mistake reach the agent. |
|
|
55
|
+
| A risky file such as `.env.production` is Git-tracked | Surfaces it as a blocking risk signal without printing its contents. |
|
|
56
|
+
| The repository is still missing important readiness pieces | `cwb preflight . --require-ready` can fail CI until the repository reaches `READY`. |
|
|
57
|
+
|
|
58
|
+
It is local-first: the core preflight does not send repository contents to a remote AI service.
|
|
59
|
+
|
|
47
60
|
## The 20-second demo
|
|
48
61
|
|
|
49
62
|
```powershell
|
|
@@ -219,6 +232,14 @@ cwb preflight . --require-ready
|
|
|
219
232
|
|
|
220
233
|
Use `--require-ready` when CI should reject both `NEEDS ATTENTION` and `BLOCKED`, including missing repository-wide AI instructions or essential repository markers.
|
|
221
234
|
|
|
235
|
+
For a remote service such as a GitHub App, skip host-machine tool availability checks while keeping repository evidence, Git tracking, risk filenames, and instruction integrity checks:
|
|
236
|
+
|
|
237
|
+
```powershell
|
|
238
|
+
cwb preflight . --repository-only
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
This prevents a remote scanner from reporting the scanner host's Node.js, package-manager, WSL, PowerShell, or Codex availability as if it belonged to the inspected repository.
|
|
242
|
+
|
|
222
243
|
### Detailed audit
|
|
223
244
|
|
|
224
245
|
```powershell
|
|
@@ -267,7 +288,7 @@ The reusable Action is published on GitHub Marketplace.
|
|
|
267
288
|
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
|
|
268
289
|
with:
|
|
269
290
|
python-version: "3.13"
|
|
270
|
-
- uses: kohli217/codex-workspace-bootstrap@v0.
|
|
291
|
+
- uses: kohli217/codex-workspace-bootstrap@v0.8.0
|
|
271
292
|
with:
|
|
272
293
|
path: .
|
|
273
294
|
strict: "true"
|
|
@@ -281,6 +302,32 @@ See [docs/GITHUB_ACTION.md](docs/GITHUB_ACTION.md).
|
|
|
281
302
|
|
|
282
303
|
Read-only evaluations against real public repositories are documented in [docs/PUBLIC_REPO_EVALUATIONS.md](docs/PUBLIC_REPO_EVALUATIONS.md). They are reproducible technical evaluations, not claims of third-party adoption.
|
|
283
304
|
|
|
305
|
+
## GitHub App
|
|
306
|
+
|
|
307
|
+
For public OSS repositories, CWB can also run as a self-hosted GitHub App. A verified `push` or `pull_request` event triggers repository-only preflight against the exact event revision and publishes a **CWB Preflight** Check Run on that commit.
|
|
308
|
+
|
|
309
|
+
The preferred zero-cost development deployment uses:
|
|
310
|
+
|
|
311
|
+
```text
|
|
312
|
+
GitHub App
|
|
313
|
+
-> Cloudflare Worker + Queue (Free)
|
|
314
|
+
-> GitHub Actions in this public repository
|
|
315
|
+
-> short-lived repository-scoped installation token
|
|
316
|
+
-> CWB Preflight Check
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
This path does not require a Google Cloud billing account. It is intentionally **public-repository-only**: private-repository events are rejected before they are queued into the public Actions worker.
|
|
320
|
+
|
|
321
|
+
The App keeps the minimum repository permissions documented by the project: Checks read/write, Contents read-only, and Pull requests read-only.
|
|
322
|
+
|
|
323
|
+
See [docs/GITHUB_APP.md](docs/GITHUB_APP.md) for the integration contract and [deploy/cloudflare/README.md](deploy/cloudflare/README.md) for the completely free development deployment.
|
|
324
|
+
|
|
325
|
+
## Integration boundary
|
|
326
|
+
|
|
327
|
+
The CLI, reusable GitHub Action, GitHub App, and future AI-agent skills share one preflight engine and one policy evaluator. Machine-readable reports declare a schema version so integrations can detect incompatible changes instead of silently drifting.
|
|
328
|
+
|
|
329
|
+
See [docs/INTEGRATIONS.md](docs/INTEGRATIONS.md). The minimum GitHub App permissions and webhook subscriptions are defined in [docs/GITHUB_APP.md](docs/GITHUB_APP.md) and mirrored by code-level regression tests.
|
|
330
|
+
|
|
284
331
|
## Where it fits
|
|
285
332
|
|
|
286
333
|
This project is a **preflight layer**, not an AI coding agent and not a deep security scanner.
|
|
@@ -334,7 +381,7 @@ py -m pip install codex-workspace-bootstrap
|
|
|
334
381
|
Pinned GitHub release artifact:
|
|
335
382
|
|
|
336
383
|
```powershell
|
|
337
|
-
py -m pip install "https://github.com/kohli217/codex-workspace-bootstrap/releases/download/v0.
|
|
384
|
+
py -m pip install "https://github.com/kohli217/codex-workspace-bootstrap/releases/download/v0.8.0/codex_workspace_bootstrap-0.8.0-py3-none-any.whl"
|
|
338
385
|
```
|
|
339
386
|
|
|
340
387
|
## Development
|
|
@@ -12,10 +12,23 @@
|
|
|
12
12
|
|
|
13
13
|
Windows-first. Local by default. CI-friendly. Designed for repositories used with Codex, Copilot, Cline, Claude Code, Gemini CLI, Continue, Cursor, and similar coding agents.
|
|
14
14
|
|
|
15
|
-
[日本語ガイド](docs/README.ja.md) · [Examples](docs/EXAMPLES.md) · [GitHub Action](docs/GITHUB_ACTION.md) · [Roadmap](docs/ROADMAP.md) · [Releases](https://github.com/kohli217/codex-workspace-bootstrap/releases)
|
|
15
|
+
[日本語ガイド](docs/README.ja.md) · [Examples](docs/EXAMPLES.md) · [GitHub Action](docs/GITHUB_ACTION.md) · [GitHub App](docs/GITHUB_APP.md) · [Free GitHub App deployment](deploy/cloudflare/README.md) · [Cloud Run alternative](deploy/cloudrun/README.md) · [Integration contract](docs/INTEGRATIONS.md) · [Roadmap](docs/ROADMAP.md) · [Releases](https://github.com/kohli217/codex-workspace-bootstrap/releases)
|
|
16
16
|
|
|
17
17
|
> Community-maintained project. Not an official OpenAI product and not affiliated with OpenAI.
|
|
18
18
|
|
|
19
|
+
## Why add this to your repository?
|
|
20
|
+
|
|
21
|
+
AI coding agents can move fast, but they can also follow stale or conflicting repository instructions. `codex-workspace-bootstrap` adds one repeatable check before that happens.
|
|
22
|
+
|
|
23
|
+
| Common failure | What `cwb` does |
|
|
24
|
+
| --- | --- |
|
|
25
|
+
| One instruction says `npm`, another says `pnpm` | Flags the mismatch before an agent edits the repository. |
|
|
26
|
+
| An instruction tells the agent to run a test/lint script that does not exist | Reports the invalid command instead of letting the mistake reach the agent. |
|
|
27
|
+
| A risky file such as `.env.production` is Git-tracked | Surfaces it as a blocking risk signal without printing its contents. |
|
|
28
|
+
| The repository is still missing important readiness pieces | `cwb preflight . --require-ready` can fail CI until the repository reaches `READY`. |
|
|
29
|
+
|
|
30
|
+
It is local-first: the core preflight does not send repository contents to a remote AI service.
|
|
31
|
+
|
|
19
32
|
## The 20-second demo
|
|
20
33
|
|
|
21
34
|
```powershell
|
|
@@ -191,6 +204,14 @@ cwb preflight . --require-ready
|
|
|
191
204
|
|
|
192
205
|
Use `--require-ready` when CI should reject both `NEEDS ATTENTION` and `BLOCKED`, including missing repository-wide AI instructions or essential repository markers.
|
|
193
206
|
|
|
207
|
+
For a remote service such as a GitHub App, skip host-machine tool availability checks while keeping repository evidence, Git tracking, risk filenames, and instruction integrity checks:
|
|
208
|
+
|
|
209
|
+
```powershell
|
|
210
|
+
cwb preflight . --repository-only
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
This prevents a remote scanner from reporting the scanner host's Node.js, package-manager, WSL, PowerShell, or Codex availability as if it belonged to the inspected repository.
|
|
214
|
+
|
|
194
215
|
### Detailed audit
|
|
195
216
|
|
|
196
217
|
```powershell
|
|
@@ -239,7 +260,7 @@ The reusable Action is published on GitHub Marketplace.
|
|
|
239
260
|
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
|
|
240
261
|
with:
|
|
241
262
|
python-version: "3.13"
|
|
242
|
-
- uses: kohli217/codex-workspace-bootstrap@v0.
|
|
263
|
+
- uses: kohli217/codex-workspace-bootstrap@v0.8.0
|
|
243
264
|
with:
|
|
244
265
|
path: .
|
|
245
266
|
strict: "true"
|
|
@@ -253,6 +274,32 @@ See [docs/GITHUB_ACTION.md](docs/GITHUB_ACTION.md).
|
|
|
253
274
|
|
|
254
275
|
Read-only evaluations against real public repositories are documented in [docs/PUBLIC_REPO_EVALUATIONS.md](docs/PUBLIC_REPO_EVALUATIONS.md). They are reproducible technical evaluations, not claims of third-party adoption.
|
|
255
276
|
|
|
277
|
+
## GitHub App
|
|
278
|
+
|
|
279
|
+
For public OSS repositories, CWB can also run as a self-hosted GitHub App. A verified `push` or `pull_request` event triggers repository-only preflight against the exact event revision and publishes a **CWB Preflight** Check Run on that commit.
|
|
280
|
+
|
|
281
|
+
The preferred zero-cost development deployment uses:
|
|
282
|
+
|
|
283
|
+
```text
|
|
284
|
+
GitHub App
|
|
285
|
+
-> Cloudflare Worker + Queue (Free)
|
|
286
|
+
-> GitHub Actions in this public repository
|
|
287
|
+
-> short-lived repository-scoped installation token
|
|
288
|
+
-> CWB Preflight Check
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
This path does not require a Google Cloud billing account. It is intentionally **public-repository-only**: private-repository events are rejected before they are queued into the public Actions worker.
|
|
292
|
+
|
|
293
|
+
The App keeps the minimum repository permissions documented by the project: Checks read/write, Contents read-only, and Pull requests read-only.
|
|
294
|
+
|
|
295
|
+
See [docs/GITHUB_APP.md](docs/GITHUB_APP.md) for the integration contract and [deploy/cloudflare/README.md](deploy/cloudflare/README.md) for the completely free development deployment.
|
|
296
|
+
|
|
297
|
+
## Integration boundary
|
|
298
|
+
|
|
299
|
+
The CLI, reusable GitHub Action, GitHub App, and future AI-agent skills share one preflight engine and one policy evaluator. Machine-readable reports declare a schema version so integrations can detect incompatible changes instead of silently drifting.
|
|
300
|
+
|
|
301
|
+
See [docs/INTEGRATIONS.md](docs/INTEGRATIONS.md). The minimum GitHub App permissions and webhook subscriptions are defined in [docs/GITHUB_APP.md](docs/GITHUB_APP.md) and mirrored by code-level regression tests.
|
|
302
|
+
|
|
256
303
|
## Where it fits
|
|
257
304
|
|
|
258
305
|
This project is a **preflight layer**, not an AI coding agent and not a deep security scanner.
|
|
@@ -306,7 +353,7 @@ py -m pip install codex-workspace-bootstrap
|
|
|
306
353
|
Pinned GitHub release artifact:
|
|
307
354
|
|
|
308
355
|
```powershell
|
|
309
|
-
py -m pip install "https://github.com/kohli217/codex-workspace-bootstrap/releases/download/v0.
|
|
356
|
+
py -m pip install "https://github.com/kohli217/codex-workspace-bootstrap/releases/download/v0.8.0/codex_workspace_bootstrap-0.8.0-py3-none-any.whl"
|
|
310
357
|
```
|
|
311
358
|
|
|
312
359
|
## Development
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "codex-workspace-bootstrap"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.8.0"
|
|
8
8
|
description = "Preflight AI coding repositories before Codex, Copilot, Cline, Claude, Gemini, Continue, or Cursor touches them."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -194,7 +194,7 @@ def _preview(paths: list[str], limit: int = 8) -> str:
|
|
|
194
194
|
return f"{visible}{suffix}"
|
|
195
195
|
|
|
196
196
|
|
|
197
|
-
def audit_repository(root: Path) -> list[Check]:
|
|
197
|
+
def audit_repository(root: Path, *, include_local_toolchain: bool = True) -> list[Check]:
|
|
198
198
|
root = root.resolve()
|
|
199
199
|
checks: list[Check] = []
|
|
200
200
|
|
|
@@ -285,8 +285,9 @@ def audit_repository(root: Path) -> list[Check]:
|
|
|
285
285
|
)
|
|
286
286
|
)
|
|
287
287
|
|
|
288
|
-
|
|
289
|
-
|
|
288
|
+
if include_local_toolchain:
|
|
289
|
+
for label, command in COMMON_TOOLS:
|
|
290
|
+
checks.append(_tool_check(label, command))
|
|
290
291
|
|
|
291
292
|
if (root / "package.json").is_file() and not (root / "package.json").is_symlink():
|
|
292
293
|
managers = detect_node_package_managers(root)
|
|
@@ -300,9 +301,10 @@ def audit_repository(root: Path) -> list[Check]:
|
|
|
300
301
|
+ ", ".join(sorted(managers)),
|
|
301
302
|
)
|
|
302
303
|
)
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
304
|
+
if include_local_toolchain:
|
|
305
|
+
for manager in sorted(managers):
|
|
306
|
+
command = NODE_PACKAGE_MANAGER_COMMANDS[manager]
|
|
307
|
+
checks.append(_tool_check(manager, command))
|
|
306
308
|
else:
|
|
307
309
|
checks.append(
|
|
308
310
|
Check(
|
|
@@ -10,7 +10,7 @@ from .agents import generate_agents
|
|
|
10
10
|
from .audit import audit_repository, summary
|
|
11
11
|
from .doctor import doctor_findings
|
|
12
12
|
from .fixes import apply_fix_plan, build_fix_plan
|
|
13
|
-
from .preflight import build_preflight, render_markdown
|
|
13
|
+
from .preflight import build_preflight, evaluate_preflight_policy, render_markdown
|
|
14
14
|
from .sarif import checks_to_sarif, preflight_report_to_sarif
|
|
15
15
|
|
|
16
16
|
|
|
@@ -46,6 +46,11 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
46
46
|
action="store_true",
|
|
47
47
|
help="Return a non-zero exit code unless the preflight state is READY",
|
|
48
48
|
)
|
|
49
|
+
preflight.add_argument(
|
|
50
|
+
"--repository-only",
|
|
51
|
+
action="store_true",
|
|
52
|
+
help="Skip local toolchain availability checks and evaluate repository evidence only",
|
|
53
|
+
)
|
|
49
54
|
|
|
50
55
|
fix = sub.add_parser("fix", help="Preview safe repository-readiness fixes")
|
|
51
56
|
fix.add_argument("path", nargs="?", default=".")
|
|
@@ -91,13 +96,17 @@ def _run_preflight(
|
|
|
91
96
|
strict: bool,
|
|
92
97
|
fail_on_integrity: bool,
|
|
93
98
|
require_ready: bool,
|
|
99
|
+
repository_only: bool,
|
|
94
100
|
) -> int:
|
|
95
101
|
root = Path(path).expanduser().resolve()
|
|
96
102
|
if not root.exists() or not root.is_dir():
|
|
97
103
|
print(f"error: repository path does not exist or is not a directory: {root}", file=sys.stderr)
|
|
98
104
|
return 2
|
|
99
105
|
|
|
100
|
-
report = build_preflight(
|
|
106
|
+
report = build_preflight(
|
|
107
|
+
root,
|
|
108
|
+
include_local_toolchain=not repository_only,
|
|
109
|
+
)
|
|
101
110
|
|
|
102
111
|
print("AI Repository Preflight")
|
|
103
112
|
print(f"Repository: {root}")
|
|
@@ -163,13 +172,13 @@ def _run_preflight(
|
|
|
163
172
|
"Preflight SARIF report",
|
|
164
173
|
)
|
|
165
174
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
return 0
|
|
175
|
+
decision = evaluate_preflight_policy(
|
|
176
|
+
report,
|
|
177
|
+
strict=strict,
|
|
178
|
+
fail_on_integrity=fail_on_integrity,
|
|
179
|
+
require_ready=require_ready,
|
|
180
|
+
)
|
|
181
|
+
return 0 if decision.passed else 1
|
|
173
182
|
|
|
174
183
|
|
|
175
184
|
def _run_fix(path: str, apply: bool) -> int:
|
|
@@ -301,6 +310,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
301
310
|
args.strict,
|
|
302
311
|
args.fail_on_integrity,
|
|
303
312
|
args.require_ready,
|
|
313
|
+
args.repository_only,
|
|
304
314
|
)
|
|
305
315
|
if args.command == "fix":
|
|
306
316
|
return _run_fix(args.path, args.apply)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Delivery adapters built on the stable preflight integration contract."""
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
from ..preflight import (
|
|
7
|
+
PREFLIGHT_REPORT_SCHEMA_VERSION,
|
|
8
|
+
PolicyDecision,
|
|
9
|
+
evaluate_preflight_policy,
|
|
10
|
+
render_markdown,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
GitHubCheckConclusion = Literal["success", "neutral", "failure"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class GitHubCheckResult:
|
|
19
|
+
"""GitHub Check Run fields derived from one preflight report."""
|
|
20
|
+
|
|
21
|
+
name: str
|
|
22
|
+
conclusion: GitHubCheckConclusion
|
|
23
|
+
title: str
|
|
24
|
+
summary: str
|
|
25
|
+
policy: PolicyDecision
|
|
26
|
+
|
|
27
|
+
def to_check_run_fields(self) -> dict[str, object]:
|
|
28
|
+
"""Return Check Run fields that a caller can combine with head_sha."""
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
"name": self.name,
|
|
32
|
+
"status": "completed",
|
|
33
|
+
"conclusion": self.conclusion,
|
|
34
|
+
"output": {
|
|
35
|
+
"title": self.title,
|
|
36
|
+
"summary": self.summary,
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def build_github_check(
|
|
42
|
+
report: dict[str, object],
|
|
43
|
+
*,
|
|
44
|
+
strict: bool = True,
|
|
45
|
+
fail_on_integrity: bool = False,
|
|
46
|
+
require_ready: bool = False,
|
|
47
|
+
name: str = "CWB Preflight",
|
|
48
|
+
) -> GitHubCheckResult:
|
|
49
|
+
"""Map a preflight report to a GitHub Check without making network calls."""
|
|
50
|
+
|
|
51
|
+
schema_version = report.get("schema_version")
|
|
52
|
+
if schema_version != PREFLIGHT_REPORT_SCHEMA_VERSION:
|
|
53
|
+
raise ValueError(
|
|
54
|
+
"unsupported preflight schema version: "
|
|
55
|
+
f"{schema_version!r}; expected {PREFLIGHT_REPORT_SCHEMA_VERSION}"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
decision = evaluate_preflight_policy(
|
|
59
|
+
report,
|
|
60
|
+
strict=strict,
|
|
61
|
+
fail_on_integrity=fail_on_integrity,
|
|
62
|
+
require_ready=require_ready,
|
|
63
|
+
)
|
|
64
|
+
state = str(report.get("state", "UNKNOWN"))
|
|
65
|
+
|
|
66
|
+
if not decision.passed:
|
|
67
|
+
conclusion: GitHubCheckConclusion = "failure"
|
|
68
|
+
elif state == "READY":
|
|
69
|
+
conclusion = "success"
|
|
70
|
+
else:
|
|
71
|
+
conclusion = "neutral"
|
|
72
|
+
|
|
73
|
+
return GitHubCheckResult(
|
|
74
|
+
name=name,
|
|
75
|
+
conclusion=conclusion,
|
|
76
|
+
title=f"CWB preflight: {state}",
|
|
77
|
+
summary=render_markdown(report),
|
|
78
|
+
policy=decision,
|
|
79
|
+
)
|
codex_workspace_bootstrap-0.8.0/src/codex_workspace_bootstrap/integrations/github_actions_worker.py
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import base64
|
|
5
|
+
from dataclasses import asdict
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from urllib.error import HTTPError, URLError
|
|
9
|
+
from urllib.parse import urlencode, urlparse
|
|
10
|
+
from urllib.request import Request, urlopen
|
|
11
|
+
|
|
12
|
+
from .github_runtime import (
|
|
13
|
+
GitHubAppRuntimeError,
|
|
14
|
+
GitHubScanResult,
|
|
15
|
+
execute_github_scan_with_token,
|
|
16
|
+
)
|
|
17
|
+
from .github_webhook import GitHubWebhookTarget
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
_MAX_RESPONSE_BYTES = 1024 * 1024
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class GitHubActionsWorkerError(RuntimeError):
|
|
24
|
+
"""Raised when the free GitHub Actions worker cannot authenticate or run."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _base64url_decode(value: str) -> bytes:
|
|
28
|
+
if not value.strip():
|
|
29
|
+
raise GitHubActionsWorkerError("scan payload must not be empty")
|
|
30
|
+
padding = "=" * (-len(value) % 4)
|
|
31
|
+
try:
|
|
32
|
+
return base64.urlsafe_b64decode((value + padding).encode("ascii"))
|
|
33
|
+
except (ValueError, UnicodeEncodeError) as exc:
|
|
34
|
+
raise GitHubActionsWorkerError("scan payload is not valid base64url") from exc
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def decode_scan_payload(encoded: str) -> tuple[str, str, GitHubWebhookTarget]:
|
|
38
|
+
try:
|
|
39
|
+
payload = json.loads(_base64url_decode(encoded).decode("utf-8"))
|
|
40
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
41
|
+
raise GitHubActionsWorkerError("scan payload is not valid UTF-8 JSON") from exc
|
|
42
|
+
if not isinstance(payload, dict):
|
|
43
|
+
raise GitHubActionsWorkerError("scan payload must be a JSON object")
|
|
44
|
+
|
|
45
|
+
delivery_id = payload.get("delivery_id")
|
|
46
|
+
broker_grant = payload.get("broker_grant")
|
|
47
|
+
target = payload.get("target")
|
|
48
|
+
if not isinstance(delivery_id, str) or not delivery_id.strip():
|
|
49
|
+
raise GitHubActionsWorkerError("scan payload is missing delivery_id")
|
|
50
|
+
if not isinstance(broker_grant, str) or not broker_grant.strip():
|
|
51
|
+
raise GitHubActionsWorkerError("scan payload is missing broker_grant")
|
|
52
|
+
if not isinstance(target, dict):
|
|
53
|
+
raise GitHubActionsWorkerError("scan payload is missing target")
|
|
54
|
+
|
|
55
|
+
event = target.get("event")
|
|
56
|
+
repository = target.get("repository")
|
|
57
|
+
head_sha = target.get("head_sha")
|
|
58
|
+
installation_id = target.get("installation_id")
|
|
59
|
+
if event not in {"push", "pull_request"}:
|
|
60
|
+
raise GitHubActionsWorkerError("scan payload has unsupported event")
|
|
61
|
+
if not isinstance(repository, str) or repository.count("/") != 1:
|
|
62
|
+
raise GitHubActionsWorkerError("scan payload has invalid repository")
|
|
63
|
+
if not isinstance(head_sha, str) or not head_sha.strip():
|
|
64
|
+
raise GitHubActionsWorkerError("scan payload is missing head_sha")
|
|
65
|
+
if (
|
|
66
|
+
isinstance(installation_id, bool)
|
|
67
|
+
or not isinstance(installation_id, int)
|
|
68
|
+
or installation_id <= 0
|
|
69
|
+
):
|
|
70
|
+
raise GitHubActionsWorkerError("scan payload is missing installation_id")
|
|
71
|
+
|
|
72
|
+
number = target.get("pull_request_number")
|
|
73
|
+
if number is not None and (
|
|
74
|
+
isinstance(number, bool)
|
|
75
|
+
or not isinstance(number, int)
|
|
76
|
+
or number <= 0
|
|
77
|
+
):
|
|
78
|
+
raise GitHubActionsWorkerError("scan payload has invalid pull_request_number")
|
|
79
|
+
|
|
80
|
+
def optional_text(name: str) -> str | None:
|
|
81
|
+
value = target.get(name)
|
|
82
|
+
if value is None:
|
|
83
|
+
return None
|
|
84
|
+
if not isinstance(value, str) or not value.strip():
|
|
85
|
+
raise GitHubActionsWorkerError(f"scan payload has invalid {name}")
|
|
86
|
+
return value
|
|
87
|
+
|
|
88
|
+
return delivery_id, broker_grant, GitHubWebhookTarget(
|
|
89
|
+
event=event,
|
|
90
|
+
repository=repository,
|
|
91
|
+
head_sha=head_sha,
|
|
92
|
+
installation_id=installation_id,
|
|
93
|
+
action=optional_text("action"),
|
|
94
|
+
pull_request_number=number,
|
|
95
|
+
merge_commit_sha=optional_text("merge_commit_sha"),
|
|
96
|
+
ref=optional_text("ref"),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _read_json_response(response) -> dict[str, object]:
|
|
101
|
+
raw = response.read(_MAX_RESPONSE_BYTES + 1)
|
|
102
|
+
if len(raw) > _MAX_RESPONSE_BYTES:
|
|
103
|
+
raise GitHubActionsWorkerError("remote response exceeded size limit")
|
|
104
|
+
try:
|
|
105
|
+
value = json.loads(raw.decode("utf-8"))
|
|
106
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
107
|
+
raise GitHubActionsWorkerError("remote service returned invalid JSON") from exc
|
|
108
|
+
if not isinstance(value, dict):
|
|
109
|
+
raise GitHubActionsWorkerError("remote service returned non-object JSON")
|
|
110
|
+
return value
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def request_actions_oidc_token(
|
|
114
|
+
*,
|
|
115
|
+
audience: str,
|
|
116
|
+
request_url: str | None = None,
|
|
117
|
+
request_token: str | None = None,
|
|
118
|
+
) -> str:
|
|
119
|
+
url = request_url or os.environ.get("ACTIONS_ID_TOKEN_REQUEST_URL", "")
|
|
120
|
+
bearer = request_token or os.environ.get("ACTIONS_ID_TOKEN_REQUEST_TOKEN", "")
|
|
121
|
+
if not url or not bearer:
|
|
122
|
+
raise GitHubActionsWorkerError(
|
|
123
|
+
"GitHub Actions OIDC environment is unavailable; id-token: write is required"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
separator = "&" if "?" in url else "?"
|
|
127
|
+
request = Request(
|
|
128
|
+
url + separator + urlencode({"audience": audience}),
|
|
129
|
+
headers={"Authorization": f"Bearer {bearer}"},
|
|
130
|
+
method="GET",
|
|
131
|
+
)
|
|
132
|
+
try:
|
|
133
|
+
with urlopen(request, timeout=20) as response:
|
|
134
|
+
body = _read_json_response(response)
|
|
135
|
+
except HTTPError as exc:
|
|
136
|
+
raise GitHubActionsWorkerError(
|
|
137
|
+
f"GitHub Actions OIDC request failed with HTTP {exc.code}"
|
|
138
|
+
) from exc
|
|
139
|
+
except URLError as exc:
|
|
140
|
+
raise GitHubActionsWorkerError("GitHub Actions OIDC request failed") from exc
|
|
141
|
+
|
|
142
|
+
value = body.get("value")
|
|
143
|
+
if not isinstance(value, str) or not value.strip():
|
|
144
|
+
raise GitHubActionsWorkerError("GitHub Actions OIDC response is missing value")
|
|
145
|
+
return value
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _validated_token_endpoint(endpoint: str) -> str:
|
|
149
|
+
parsed = urlparse(endpoint)
|
|
150
|
+
if (
|
|
151
|
+
parsed.scheme != "https"
|
|
152
|
+
or not parsed.netloc
|
|
153
|
+
or parsed.username is not None
|
|
154
|
+
or parsed.password is not None
|
|
155
|
+
or parsed.query
|
|
156
|
+
or parsed.fragment
|
|
157
|
+
):
|
|
158
|
+
raise GitHubActionsWorkerError(
|
|
159
|
+
"token endpoint must be an absolute HTTPS URL without credentials, query, or fragment"
|
|
160
|
+
)
|
|
161
|
+
return endpoint.rstrip("/")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def request_installation_token(
|
|
165
|
+
*,
|
|
166
|
+
endpoint: str,
|
|
167
|
+
oidc_token: str,
|
|
168
|
+
delivery_id: str,
|
|
169
|
+
broker_grant: str,
|
|
170
|
+
target: GitHubWebhookTarget,
|
|
171
|
+
) -> str:
|
|
172
|
+
if target.installation_id is None:
|
|
173
|
+
raise GitHubActionsWorkerError("scan target is missing installation_id")
|
|
174
|
+
url = _validated_token_endpoint(endpoint)
|
|
175
|
+
request = Request(
|
|
176
|
+
url,
|
|
177
|
+
data=json.dumps(
|
|
178
|
+
{
|
|
179
|
+
"delivery_id": delivery_id,
|
|
180
|
+
"broker_grant": broker_grant,
|
|
181
|
+
"target": asdict(target),
|
|
182
|
+
},
|
|
183
|
+
separators=(",", ":"),
|
|
184
|
+
).encode("utf-8"),
|
|
185
|
+
headers={
|
|
186
|
+
"Authorization": f"Bearer {oidc_token}",
|
|
187
|
+
"Content-Type": "application/json",
|
|
188
|
+
"User-Agent": "codex-workspace-bootstrap",
|
|
189
|
+
},
|
|
190
|
+
method="POST",
|
|
191
|
+
)
|
|
192
|
+
try:
|
|
193
|
+
with urlopen(request, timeout=20) as response:
|
|
194
|
+
body = _read_json_response(response)
|
|
195
|
+
except HTTPError as exc:
|
|
196
|
+
raise GitHubActionsWorkerError(
|
|
197
|
+
f"installation-token broker returned HTTP {exc.code}"
|
|
198
|
+
) from exc
|
|
199
|
+
except URLError as exc:
|
|
200
|
+
raise GitHubActionsWorkerError(
|
|
201
|
+
"installation-token broker request failed"
|
|
202
|
+
) from exc
|
|
203
|
+
|
|
204
|
+
token = body.get("token")
|
|
205
|
+
if not isinstance(token, str) or not token.strip():
|
|
206
|
+
raise GitHubActionsWorkerError(
|
|
207
|
+
"installation-token broker response is missing token"
|
|
208
|
+
)
|
|
209
|
+
return token
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def execute_actions_scan(
|
|
213
|
+
*,
|
|
214
|
+
encoded_payload: str,
|
|
215
|
+
token_endpoint: str,
|
|
216
|
+
) -> GitHubScanResult:
|
|
217
|
+
delivery_id, broker_grant, target = decode_scan_payload(encoded_payload)
|
|
218
|
+
audience = _validated_token_endpoint(token_endpoint)
|
|
219
|
+
oidc_token = request_actions_oidc_token(audience=audience)
|
|
220
|
+
installation_token = request_installation_token(
|
|
221
|
+
endpoint=audience,
|
|
222
|
+
oidc_token=oidc_token,
|
|
223
|
+
delivery_id=delivery_id,
|
|
224
|
+
broker_grant=broker_grant,
|
|
225
|
+
target=target,
|
|
226
|
+
)
|
|
227
|
+
return execute_github_scan_with_token(
|
|
228
|
+
target,
|
|
229
|
+
installation_token=installation_token,
|
|
230
|
+
external_id=delivery_id,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def main(argv: list[str] | None = None) -> int:
|
|
235
|
+
parser = argparse.ArgumentParser()
|
|
236
|
+
parser.add_argument(
|
|
237
|
+
"--payload",
|
|
238
|
+
default=os.environ.get("CWB_SCAN_PAYLOAD", ""),
|
|
239
|
+
help="base64url-encoded normalized GitHub scan target",
|
|
240
|
+
)
|
|
241
|
+
parser.add_argument(
|
|
242
|
+
"--token-endpoint",
|
|
243
|
+
default=os.environ.get("CWB_TOKEN_ENDPOINT", ""),
|
|
244
|
+
help="Cloudflare HTTPS installation-token broker endpoint",
|
|
245
|
+
)
|
|
246
|
+
args = parser.parse_args(argv)
|
|
247
|
+
|
|
248
|
+
try:
|
|
249
|
+
result = execute_actions_scan(
|
|
250
|
+
encoded_payload=args.payload,
|
|
251
|
+
token_endpoint=args.token_endpoint,
|
|
252
|
+
)
|
|
253
|
+
except (GitHubActionsWorkerError, GitHubAppRuntimeError) as exc:
|
|
254
|
+
print(f"CWB GitHub App scan failed: {exc}")
|
|
255
|
+
return 1
|
|
256
|
+
|
|
257
|
+
suffix = " (deduplicated)" if result.deduplicated else ""
|
|
258
|
+
print(
|
|
259
|
+
f"CWB GitHub App scan: {result.repository}@{result.commit_sha} "
|
|
260
|
+
f"-> {result.conclusion}{suffix}"
|
|
261
|
+
)
|
|
262
|
+
return 0
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
if __name__ == "__main__":
|
|
266
|
+
raise SystemExit(main())
|