codex-workspace-bootstrap 0.8.0__tar.gz → 0.9.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.8.0/src/codex_workspace_bootstrap.egg-info → codex_workspace_bootstrap-0.9.0}/PKG-INFO +10 -6
- {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/README.md +9 -5
- {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/pyproject.toml +1 -1
- {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap/__init__.py +1 -1
- {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap/instructions.py +335 -26
- {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0/src/codex_workspace_bootstrap.egg-info}/PKG-INFO +10 -6
- {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/tests/test_cli.py +1 -1
- {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/tests/test_instruction_integrity.py +510 -0
- codex_workspace_bootstrap-0.9.0/tests/test_public_repo_patterns.py +344 -0
- codex_workspace_bootstrap-0.8.0/tests/test_public_repo_patterns.py +0 -126
- {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/LICENSE +0 -0
- {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/setup.cfg +0 -0
- {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap/__main__.py +0 -0
- {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap/agents.py +0 -0
- {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap/audit.py +0 -0
- {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap/cli.py +0 -0
- {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap/doctor.py +0 -0
- {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap/fixes.py +0 -0
- {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap/integrations/__init__.py +0 -0
- {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap/integrations/github.py +0 -0
- {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap/integrations/github_actions_worker.py +0 -0
- {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap/integrations/github_app.py +0 -0
- {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap/integrations/github_app_service.py +0 -0
- {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap/integrations/github_checkout.py +0 -0
- {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap/integrations/github_cloud_run.py +0 -0
- {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap/integrations/github_delivery.py +0 -0
- {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap/integrations/github_manifest.py +0 -0
- {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap/integrations/github_runtime.py +0 -0
- {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap/integrations/github_webhook.py +0 -0
- {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap/preflight.py +0 -0
- {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap/sarif.py +0 -0
- {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap.egg-info/SOURCES.txt +0 -0
- {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap.egg-info/dependency_links.txt +0 -0
- {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap.egg-info/entry_points.txt +0 -0
- {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/src/codex_workspace_bootstrap.egg-info/top_level.txt +0 -0
- {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/tests/test_audit.py +0 -0
- {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/tests/test_doctor.py +0 -0
- {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/tests/test_github_actions_worker.py +0 -0
- {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/tests/test_github_app_registration.py +0 -0
- {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/tests/test_github_app_service.py +0 -0
- {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/tests/test_github_checkout.py +0 -0
- {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/tests/test_github_cloud_run.py +0 -0
- {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/tests/test_github_delivery.py +0 -0
- {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/tests/test_github_integration.py +0 -0
- {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/tests/test_github_manifest.py +0 -0
- {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/tests/test_github_runtime.py +0 -0
- {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/tests/test_github_webhook.py +0 -0
- {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/tests/test_preflight.py +0 -0
- {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.0}/tests/test_release_consistency.py +0 -0
- {codex_workspace_bootstrap-0.8.0 → codex_workspace_bootstrap-0.9.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.9.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
|
|
@@ -288,7 +288,7 @@ The reusable Action is published on GitHub Marketplace.
|
|
|
288
288
|
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
|
|
289
289
|
with:
|
|
290
290
|
python-version: "3.13"
|
|
291
|
-
- uses: kohli217/codex-workspace-bootstrap@v0.
|
|
291
|
+
- uses: kohli217/codex-workspace-bootstrap@v0.9.0
|
|
292
292
|
with:
|
|
293
293
|
path: .
|
|
294
294
|
strict: "true"
|
|
@@ -304,9 +304,11 @@ Read-only evaluations against real public repositories are documented in [docs/P
|
|
|
304
304
|
|
|
305
305
|
## GitHub App
|
|
306
306
|
|
|
307
|
+
**Public App:** [CWB Preflight](https://github.com/apps/cwb-preflight) · [Install](https://github.com/apps/cwb-preflight/installations/new)
|
|
308
|
+
|
|
307
309
|
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
310
|
|
|
309
|
-
The
|
|
311
|
+
The verified zero-cost public-OSS deployment uses:
|
|
310
312
|
|
|
311
313
|
```text
|
|
312
314
|
GitHub App
|
|
@@ -320,7 +322,9 @@ This path does not require a Google Cloud billing account. It is intentionally *
|
|
|
320
322
|
|
|
321
323
|
The App keeps the minimum repository permissions documented by the project: Checks read/write, Contents read-only, and Pull requests read-only.
|
|
322
324
|
|
|
323
|
-
|
|
325
|
+
The current public App has been revalidated after promotion: the renamed `CWB Preflight` App produced a successful `READY` Check Run through the full Cloudflare → Queue → GitHub Actions → installation-token → exact-revision checkout path.
|
|
326
|
+
|
|
327
|
+
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 deployment.
|
|
324
328
|
|
|
325
329
|
## Integration boundary
|
|
326
330
|
|
|
@@ -347,7 +351,7 @@ The intent is to complement those tools, not replace them.
|
|
|
347
351
|
- Suspected secret files are not opened or printed by the filename-risk check.
|
|
348
352
|
- Repository contents are not sent to a remote AI service by the core audit.
|
|
349
353
|
- Instruction files are read locally for deterministic linting; extracted commands are never executed by the integrity lint.
|
|
350
|
-
- Repository instruction/configuration and project-marker
|
|
354
|
+
- Repository instruction/configuration and project-marker contents are not trusted through symbolic links. CWB may recognize the exact lexical aliases `CLAUDE.md -> AGENTS.md` and `GEMINI.md -> AGENTS.md` (or `./AGENTS.md`) only when the sibling `AGENTS.md` is a regular file; linting reads that regular file directly rather than following either link. Gemini aliases are recognized only when `GEMINI.md` is an effective Gemini context filename.
|
|
351
355
|
- `init-agents` and automatic fixes refuse symlinked `AGENTS.md` targets rather than writing through them.
|
|
352
356
|
- Existing regular `AGENTS.md` files are protected unless overwrite is explicit.
|
|
353
357
|
- A passing preflight is evidence about the checks performed, **not a security guarantee**.
|
|
@@ -381,7 +385,7 @@ py -m pip install codex-workspace-bootstrap
|
|
|
381
385
|
Pinned GitHub release artifact:
|
|
382
386
|
|
|
383
387
|
```powershell
|
|
384
|
-
py -m pip install "https://github.com/kohli217/codex-workspace-bootstrap/releases/download/v0.
|
|
388
|
+
py -m pip install "https://github.com/kohli217/codex-workspace-bootstrap/releases/download/v0.9.0/codex_workspace_bootstrap-0.9.0-py3-none-any.whl"
|
|
385
389
|
```
|
|
386
390
|
|
|
387
391
|
## Development
|
|
@@ -260,7 +260,7 @@ The reusable Action is published on GitHub Marketplace.
|
|
|
260
260
|
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
|
|
261
261
|
with:
|
|
262
262
|
python-version: "3.13"
|
|
263
|
-
- uses: kohli217/codex-workspace-bootstrap@v0.
|
|
263
|
+
- uses: kohli217/codex-workspace-bootstrap@v0.9.0
|
|
264
264
|
with:
|
|
265
265
|
path: .
|
|
266
266
|
strict: "true"
|
|
@@ -276,9 +276,11 @@ Read-only evaluations against real public repositories are documented in [docs/P
|
|
|
276
276
|
|
|
277
277
|
## GitHub App
|
|
278
278
|
|
|
279
|
+
**Public App:** [CWB Preflight](https://github.com/apps/cwb-preflight) · [Install](https://github.com/apps/cwb-preflight/installations/new)
|
|
280
|
+
|
|
279
281
|
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
282
|
|
|
281
|
-
The
|
|
283
|
+
The verified zero-cost public-OSS deployment uses:
|
|
282
284
|
|
|
283
285
|
```text
|
|
284
286
|
GitHub App
|
|
@@ -292,7 +294,9 @@ This path does not require a Google Cloud billing account. It is intentionally *
|
|
|
292
294
|
|
|
293
295
|
The App keeps the minimum repository permissions documented by the project: Checks read/write, Contents read-only, and Pull requests read-only.
|
|
294
296
|
|
|
295
|
-
|
|
297
|
+
The current public App has been revalidated after promotion: the renamed `CWB Preflight` App produced a successful `READY` Check Run through the full Cloudflare → Queue → GitHub Actions → installation-token → exact-revision checkout path.
|
|
298
|
+
|
|
299
|
+
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 deployment.
|
|
296
300
|
|
|
297
301
|
## Integration boundary
|
|
298
302
|
|
|
@@ -319,7 +323,7 @@ The intent is to complement those tools, not replace them.
|
|
|
319
323
|
- Suspected secret files are not opened or printed by the filename-risk check.
|
|
320
324
|
- Repository contents are not sent to a remote AI service by the core audit.
|
|
321
325
|
- Instruction files are read locally for deterministic linting; extracted commands are never executed by the integrity lint.
|
|
322
|
-
- Repository instruction/configuration and project-marker
|
|
326
|
+
- Repository instruction/configuration and project-marker contents are not trusted through symbolic links. CWB may recognize the exact lexical aliases `CLAUDE.md -> AGENTS.md` and `GEMINI.md -> AGENTS.md` (or `./AGENTS.md`) only when the sibling `AGENTS.md` is a regular file; linting reads that regular file directly rather than following either link. Gemini aliases are recognized only when `GEMINI.md` is an effective Gemini context filename.
|
|
323
327
|
- `init-agents` and automatic fixes refuse symlinked `AGENTS.md` targets rather than writing through them.
|
|
324
328
|
- Existing regular `AGENTS.md` files are protected unless overwrite is explicit.
|
|
325
329
|
- A passing preflight is evidence about the checks performed, **not a security guarantee**.
|
|
@@ -353,7 +357,7 @@ py -m pip install codex-workspace-bootstrap
|
|
|
353
357
|
Pinned GitHub release artifact:
|
|
354
358
|
|
|
355
359
|
```powershell
|
|
356
|
-
py -m pip install "https://github.com/kohli217/codex-workspace-bootstrap/releases/download/v0.
|
|
360
|
+
py -m pip install "https://github.com/kohli217/codex-workspace-bootstrap/releases/download/v0.9.0/codex_workspace_bootstrap-0.9.0-py3-none-any.whl"
|
|
357
361
|
```
|
|
358
362
|
|
|
359
363
|
## 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.9.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"
|
|
@@ -81,8 +81,8 @@ _COMMAND_PATTERNS: tuple[re.Pattern[str], ...] = (
|
|
|
81
81
|
re.compile(r"\bpython\s+-m\s+pytest(?:\s+[^\n`]+)?", re.I),
|
|
82
82
|
re.compile(r"(?<![\w.-])pytest(?:\s+[^\n`]+)?", re.I),
|
|
83
83
|
re.compile(r"\bpython\s+-m\s+unittest(?:\s+[^\n`]+)?", re.I),
|
|
84
|
-
re.compile(r"\b(?:npm|pnpm|bun)\s+(?:run\s+)?[\w
|
|
85
|
-
re.compile(r"\byarn\s+(?:run\s+)?[\w
|
|
84
|
+
re.compile(r"\b(?:npm|pnpm|bun)\s+(?:run\s+)?[\w:./=@-]+(?:\s+[^\n`]+)?", re.I),
|
|
85
|
+
re.compile(r"\byarn\s+(?:run\s+)?[\w:./=@-]+(?:\s+[^\n`]+)?", re.I),
|
|
86
86
|
re.compile(r"\bgo\s+test(?:\s+[^\n`]+)?", re.I),
|
|
87
87
|
re.compile(r"\bcargo\s+test(?:\s+[^\n`]+)?", re.I),
|
|
88
88
|
re.compile(r"(?<![\w.-])(?:make|just)\s+[\w:.-]+(?:\s+[^\n`]+)?", re.I),
|
|
@@ -308,6 +308,23 @@ def _agent_signals(root: Path) -> list[InstructionSignal]:
|
|
|
308
308
|
return found
|
|
309
309
|
|
|
310
310
|
|
|
311
|
+
def _safe_sibling_instruction_alias(path: Path, target_name: str) -> Path | None:
|
|
312
|
+
if not path.is_symlink():
|
|
313
|
+
return None
|
|
314
|
+
try:
|
|
315
|
+
lexical_target = path.readlink().as_posix()
|
|
316
|
+
except OSError:
|
|
317
|
+
return None
|
|
318
|
+
|
|
319
|
+
if lexical_target not in {target_name, f"./{target_name}"}:
|
|
320
|
+
return None
|
|
321
|
+
|
|
322
|
+
target = path.parent / target_name
|
|
323
|
+
if target.is_symlink() or not target.is_file():
|
|
324
|
+
return None
|
|
325
|
+
return target
|
|
326
|
+
|
|
327
|
+
|
|
311
328
|
def _hierarchical_named_signals(
|
|
312
329
|
root: Path,
|
|
313
330
|
filename: str,
|
|
@@ -327,8 +344,38 @@ def _hierarchical_named_signals(
|
|
|
327
344
|
return found
|
|
328
345
|
|
|
329
346
|
|
|
347
|
+
def _safe_alias_signals(
|
|
348
|
+
root: Path,
|
|
349
|
+
alias_name: str,
|
|
350
|
+
target_name: str,
|
|
351
|
+
tool: str,
|
|
352
|
+
) -> list[InstructionSignal]:
|
|
353
|
+
found: list[InstructionSignal] = []
|
|
354
|
+
for directory, _dirnames, filenames in _walk_repository(root):
|
|
355
|
+
if alias_name not in filenames:
|
|
356
|
+
continue
|
|
357
|
+
alias = directory / alias_name
|
|
358
|
+
if _safe_sibling_instruction_alias(alias, target_name) is None:
|
|
359
|
+
continue
|
|
360
|
+
|
|
361
|
+
rel = alias.relative_to(root).as_posix()
|
|
362
|
+
scope_path = directory.relative_to(root).as_posix()
|
|
363
|
+
scope = "." if scope_path == "." else scope_path
|
|
364
|
+
found.append(InstructionSignal(tool, rel, scope, "alias"))
|
|
365
|
+
return found
|
|
366
|
+
|
|
367
|
+
|
|
330
368
|
def _claude_signals(root: Path) -> list[InstructionSignal]:
|
|
331
|
-
|
|
369
|
+
found = _hierarchical_named_signals(root, "CLAUDE.md", "Claude Code")
|
|
370
|
+
found.extend(
|
|
371
|
+
_safe_alias_signals(
|
|
372
|
+
root,
|
|
373
|
+
"CLAUDE.md",
|
|
374
|
+
"AGENTS.md",
|
|
375
|
+
"Claude Code",
|
|
376
|
+
)
|
|
377
|
+
)
|
|
378
|
+
return found
|
|
332
379
|
|
|
333
380
|
|
|
334
381
|
def _gemini_context_filenames(root: Path) -> tuple[str, ...]:
|
|
@@ -375,8 +422,19 @@ def _gemini_context_filenames(root: Path) -> tuple[str, ...]:
|
|
|
375
422
|
|
|
376
423
|
def _gemini_signals(root: Path) -> list[InstructionSignal]:
|
|
377
424
|
found: list[InstructionSignal] = []
|
|
378
|
-
|
|
425
|
+
filenames = _gemini_context_filenames(root)
|
|
426
|
+
for filename in filenames:
|
|
379
427
|
found.extend(_hierarchical_named_signals(root, filename, "Gemini CLI"))
|
|
428
|
+
|
|
429
|
+
if "GEMINI.md" in filenames:
|
|
430
|
+
found.extend(
|
|
431
|
+
_safe_alias_signals(
|
|
432
|
+
root,
|
|
433
|
+
"GEMINI.md",
|
|
434
|
+
"AGENTS.md",
|
|
435
|
+
"Gemini CLI",
|
|
436
|
+
)
|
|
437
|
+
)
|
|
380
438
|
return found
|
|
381
439
|
|
|
382
440
|
|
|
@@ -512,7 +570,20 @@ def detect_instruction_signals(root: Path) -> list[InstructionSignal]:
|
|
|
512
570
|
|
|
513
571
|
|
|
514
572
|
def _read_instruction(root: Path, signal: InstructionSignal) -> str:
|
|
515
|
-
|
|
573
|
+
path = root / signal.path
|
|
574
|
+
if signal.kind == "alias":
|
|
575
|
+
alias_targets = {
|
|
576
|
+
("Claude Code", "CLAUDE.md"): "AGENTS.md",
|
|
577
|
+
("Gemini CLI", "GEMINI.md"): "AGENTS.md",
|
|
578
|
+
}
|
|
579
|
+
target_name = alias_targets.get((signal.tool, path.name))
|
|
580
|
+
if target_name is None:
|
|
581
|
+
return ""
|
|
582
|
+
target = _safe_sibling_instruction_alias(path, target_name)
|
|
583
|
+
if target is None:
|
|
584
|
+
return ""
|
|
585
|
+
return _safe_read(target)
|
|
586
|
+
return _safe_read(path)
|
|
516
587
|
|
|
517
588
|
|
|
518
589
|
def _command_regions(text: str) -> list[str]:
|
|
@@ -684,12 +755,249 @@ def _repo_package_managers(root: Path, scope: str) -> set[str]:
|
|
|
684
755
|
return set()
|
|
685
756
|
|
|
686
757
|
|
|
687
|
-
def _script_names(root: Path, scope: str) -> set[str]:
|
|
758
|
+
def _script_names(root: Path, scope: str) -> set[str] | None:
|
|
688
759
|
for directory in _directory_chain_to_root(root, scope):
|
|
689
|
-
|
|
690
|
-
if
|
|
760
|
+
package_path = directory / "package.json"
|
|
761
|
+
if package_path.is_file() and not package_path.is_symlink():
|
|
762
|
+
_managers, scripts = _package_evidence_at(directory)
|
|
691
763
|
return scripts
|
|
692
|
-
return
|
|
764
|
+
return None
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
def _workspace_target_for_command(command: str) -> tuple[bool, str | None]:
|
|
768
|
+
tokens = _package_command_tokens(command.strip())
|
|
769
|
+
if not tokens:
|
|
770
|
+
return False, None
|
|
771
|
+
|
|
772
|
+
manager = tokens[0].lower()
|
|
773
|
+
if manager not in {"npm", "pnpm", "yarn", "bun"}:
|
|
774
|
+
return False, None
|
|
775
|
+
|
|
776
|
+
if manager == "yarn" and len(tokens) >= 3 and tokens[1] == "workspace":
|
|
777
|
+
return True, tokens[2]
|
|
778
|
+
|
|
779
|
+
if manager == "npm":
|
|
780
|
+
targets: list[str] = []
|
|
781
|
+
index = 1
|
|
782
|
+
while index < len(tokens):
|
|
783
|
+
token = tokens[index]
|
|
784
|
+
if token == "--":
|
|
785
|
+
break
|
|
786
|
+
if token in {"--workspace", "-w"}:
|
|
787
|
+
if index + 1 < len(tokens):
|
|
788
|
+
targets.append(tokens[index + 1])
|
|
789
|
+
else:
|
|
790
|
+
targets.append("")
|
|
791
|
+
index += 2
|
|
792
|
+
continue
|
|
793
|
+
if token.startswith("--workspace="):
|
|
794
|
+
targets.append(token[len("--workspace="):])
|
|
795
|
+
index += 1
|
|
796
|
+
continue
|
|
797
|
+
if token.startswith("-w="):
|
|
798
|
+
targets.append(token[len("-w="):])
|
|
799
|
+
index += 1
|
|
800
|
+
continue
|
|
801
|
+
index += 1
|
|
802
|
+
|
|
803
|
+
if not targets:
|
|
804
|
+
return False, None
|
|
805
|
+
nonempty = [target for target in targets if target]
|
|
806
|
+
if len(nonempty) != 1 or len(targets) != 1:
|
|
807
|
+
return True, None
|
|
808
|
+
return True, nonempty[0]
|
|
809
|
+
|
|
810
|
+
target_options = {"--filter", "-F", "--workspace", "-w"}
|
|
811
|
+
target_long_options = {"--filter", "--workspace"}
|
|
812
|
+
directory_options = {"--dir", "-C", "--prefix", "--cwd"}
|
|
813
|
+
directory_long_options = {"--dir", "--prefix", "--cwd"}
|
|
814
|
+
targets: list[str] = []
|
|
815
|
+
|
|
816
|
+
index = 1
|
|
817
|
+
while index < len(tokens):
|
|
818
|
+
token = tokens[index]
|
|
819
|
+
if token in target_options:
|
|
820
|
+
if index + 1 < len(tokens):
|
|
821
|
+
targets.append(tokens[index + 1])
|
|
822
|
+
else:
|
|
823
|
+
targets.append("")
|
|
824
|
+
index += 2
|
|
825
|
+
continue
|
|
826
|
+
|
|
827
|
+
matched_inline = False
|
|
828
|
+
for option in target_long_options:
|
|
829
|
+
prefix = f"{option}="
|
|
830
|
+
if token.startswith(prefix):
|
|
831
|
+
targets.append(token[len(prefix):])
|
|
832
|
+
matched_inline = True
|
|
833
|
+
break
|
|
834
|
+
if matched_inline:
|
|
835
|
+
index += 1
|
|
836
|
+
continue
|
|
837
|
+
|
|
838
|
+
if token in directory_options:
|
|
839
|
+
index += 2
|
|
840
|
+
continue
|
|
841
|
+
if any(token.startswith(f"{option}=") for option in directory_long_options):
|
|
842
|
+
index += 1
|
|
843
|
+
continue
|
|
844
|
+
|
|
845
|
+
if not token.startswith("-"):
|
|
846
|
+
break
|
|
847
|
+
index += 1
|
|
848
|
+
|
|
849
|
+
if not targets:
|
|
850
|
+
return False, None
|
|
851
|
+
|
|
852
|
+
nonempty = [target for target in targets if target]
|
|
853
|
+
if len(nonempty) != 1 or len(targets) != 1:
|
|
854
|
+
return True, None
|
|
855
|
+
return True, nonempty[0]
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
def _directory_target_for_command(command: str) -> tuple[bool, str | None]:
|
|
859
|
+
tokens = _package_command_tokens(command.strip())
|
|
860
|
+
if not tokens:
|
|
861
|
+
return False, None
|
|
862
|
+
|
|
863
|
+
manager = tokens[0].lower()
|
|
864
|
+
if manager not in {"npm", "pnpm", "yarn", "bun"}:
|
|
865
|
+
return False, None
|
|
866
|
+
|
|
867
|
+
supported_options: set[str]
|
|
868
|
+
supported_long_options: set[str]
|
|
869
|
+
if manager == "pnpm":
|
|
870
|
+
supported_options = {"--dir", "-C"}
|
|
871
|
+
supported_long_options = {"--dir"}
|
|
872
|
+
elif manager == "npm":
|
|
873
|
+
supported_options = {"--prefix"}
|
|
874
|
+
supported_long_options = {"--prefix"}
|
|
875
|
+
elif manager in {"yarn", "bun"}:
|
|
876
|
+
supported_options = {"--cwd"}
|
|
877
|
+
supported_long_options = {"--cwd"}
|
|
878
|
+
else:
|
|
879
|
+
return False, None
|
|
880
|
+
|
|
881
|
+
targets: list[str] = []
|
|
882
|
+
index = 1
|
|
883
|
+
while index < len(tokens):
|
|
884
|
+
token = tokens[index]
|
|
885
|
+
if token in supported_options:
|
|
886
|
+
if index + 1 < len(tokens):
|
|
887
|
+
targets.append(tokens[index + 1])
|
|
888
|
+
else:
|
|
889
|
+
targets.append("")
|
|
890
|
+
index += 2
|
|
891
|
+
continue
|
|
892
|
+
|
|
893
|
+
matched_inline = False
|
|
894
|
+
for option in supported_long_options:
|
|
895
|
+
prefix = f"{option}="
|
|
896
|
+
if token.startswith(prefix):
|
|
897
|
+
targets.append(token[len(prefix):])
|
|
898
|
+
matched_inline = True
|
|
899
|
+
break
|
|
900
|
+
if matched_inline:
|
|
901
|
+
index += 1
|
|
902
|
+
continue
|
|
903
|
+
|
|
904
|
+
if token in {"--filter", "-F", "--workspace", "-w"}:
|
|
905
|
+
index += 2
|
|
906
|
+
continue
|
|
907
|
+
if token.startswith(("--filter=", "--workspace=")):
|
|
908
|
+
index += 1
|
|
909
|
+
continue
|
|
910
|
+
|
|
911
|
+
if manager == "yarn" and token == "workspace":
|
|
912
|
+
break
|
|
913
|
+
if not token.startswith("-"):
|
|
914
|
+
break
|
|
915
|
+
index += 1
|
|
916
|
+
|
|
917
|
+
if not targets:
|
|
918
|
+
return False, None
|
|
919
|
+
nonempty = [target for target in targets if target]
|
|
920
|
+
if len(nonempty) != 1 or len(targets) != 1:
|
|
921
|
+
return True, None
|
|
922
|
+
return True, nonempty[0]
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
def _safe_repository_directory(root: Path, target: str) -> Path | None:
|
|
926
|
+
value = target.strip().replace("\\", "/")
|
|
927
|
+
if not value or value.startswith("/") or re.match(r"^[A-Za-z]:/", value):
|
|
928
|
+
return None
|
|
929
|
+
|
|
930
|
+
parts = [part for part in value.split("/") if part not in {"", "."}]
|
|
931
|
+
if any(part == ".." for part in parts):
|
|
932
|
+
return None
|
|
933
|
+
|
|
934
|
+
candidate = root.joinpath(*parts) if parts else root
|
|
935
|
+
try:
|
|
936
|
+
candidate.resolve().relative_to(root.resolve())
|
|
937
|
+
except (OSError, ValueError):
|
|
938
|
+
return None
|
|
939
|
+
return candidate
|
|
940
|
+
|
|
941
|
+
|
|
942
|
+
def _directory_script_names(root: Path, target: str) -> set[str] | None:
|
|
943
|
+
directory = _safe_repository_directory(root, target)
|
|
944
|
+
if directory is None:
|
|
945
|
+
return None
|
|
946
|
+
|
|
947
|
+
package_path = directory / "package.json"
|
|
948
|
+
if not package_path.is_file() or package_path.is_symlink():
|
|
949
|
+
return None
|
|
950
|
+
|
|
951
|
+
package = _package_json(package_path)
|
|
952
|
+
raw_scripts = package.get("scripts")
|
|
953
|
+
if not isinstance(raw_scripts, dict):
|
|
954
|
+
return set()
|
|
955
|
+
return {
|
|
956
|
+
str(name)
|
|
957
|
+
for name, value in raw_scripts.items()
|
|
958
|
+
if isinstance(name, str) and isinstance(value, str)
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
|
|
962
|
+
def _workspace_script_names(root: Path, target: str) -> set[str] | None:
|
|
963
|
+
matches: list[set[str]] = []
|
|
964
|
+
for directory, _dirnames, filenames in _walk_repository(root):
|
|
965
|
+
if "package.json" not in filenames:
|
|
966
|
+
continue
|
|
967
|
+
package_path = directory / "package.json"
|
|
968
|
+
package = _package_json(package_path)
|
|
969
|
+
if package.get("name") != target:
|
|
970
|
+
continue
|
|
971
|
+
raw_scripts = package.get("scripts")
|
|
972
|
+
if isinstance(raw_scripts, dict):
|
|
973
|
+
scripts = {
|
|
974
|
+
str(name)
|
|
975
|
+
for name, value in raw_scripts.items()
|
|
976
|
+
if isinstance(name, str) and isinstance(value, str)
|
|
977
|
+
}
|
|
978
|
+
else:
|
|
979
|
+
scripts = set()
|
|
980
|
+
matches.append(scripts)
|
|
981
|
+
|
|
982
|
+
if len(matches) != 1:
|
|
983
|
+
return None
|
|
984
|
+
return matches[0]
|
|
985
|
+
|
|
986
|
+
|
|
987
|
+
def _script_names_for_command(root: Path, scope: str, command: str) -> set[str] | None:
|
|
988
|
+
has_workspace_target, workspace_target = _workspace_target_for_command(command)
|
|
989
|
+
if has_workspace_target:
|
|
990
|
+
if workspace_target is None:
|
|
991
|
+
return None
|
|
992
|
+
return _workspace_script_names(root, workspace_target)
|
|
993
|
+
|
|
994
|
+
has_directory_target, directory_target = _directory_target_for_command(command)
|
|
995
|
+
if has_directory_target:
|
|
996
|
+
if directory_target is None:
|
|
997
|
+
return None
|
|
998
|
+
return _directory_script_names(root, directory_target)
|
|
999
|
+
|
|
1000
|
+
return _script_names(root, scope)
|
|
693
1001
|
|
|
694
1002
|
|
|
695
1003
|
def _manager_for_command(command: str) -> str | None:
|
|
@@ -876,7 +1184,6 @@ def lint_instructions(
|
|
|
876
1184
|
instruction_managers[path] = managers
|
|
877
1185
|
|
|
878
1186
|
repo_managers = _repo_package_managers(root, signal.scope)
|
|
879
|
-
scripts = _script_names(root, signal.scope)
|
|
880
1187
|
|
|
881
1188
|
if len(repo_managers) == 1:
|
|
882
1189
|
expected = next(iter(repo_managers))
|
|
@@ -893,23 +1200,25 @@ def lint_instructions(
|
|
|
893
1200
|
)
|
|
894
1201
|
)
|
|
895
1202
|
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
)
|
|
1203
|
+
for command in commands:
|
|
1204
|
+
parsed = _script_for_command(command)
|
|
1205
|
+
if not parsed:
|
|
1206
|
+
continue
|
|
1207
|
+
scripts = _script_names_for_command(root, signal.scope, command)
|
|
1208
|
+
if scripts is None:
|
|
1209
|
+
continue
|
|
1210
|
+
_manager, script, explicit_run = parsed
|
|
1211
|
+
if script not in scripts and (script == "test" or explicit_run):
|
|
1212
|
+
findings.append(
|
|
1213
|
+
InstructionFinding(
|
|
1214
|
+
"missing-package-script",
|
|
1215
|
+
"warning",
|
|
1216
|
+
f"{path} references '{command}', but the targeted or nearest package.json for scope '{signal.scope}' has no '{script}' script.",
|
|
1217
|
+
(path,),
|
|
1218
|
+
(command,),
|
|
1219
|
+
signal.scope,
|
|
912
1220
|
)
|
|
1221
|
+
)
|
|
913
1222
|
|
|
914
1223
|
for scope, per_file in _same_scope_groups(signals, instruction_managers).items():
|
|
915
1224
|
manager_sets = {manager for managers in per_file.values() for manager in managers}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codex-workspace-bootstrap
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.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
|
|
@@ -288,7 +288,7 @@ The reusable Action is published on GitHub Marketplace.
|
|
|
288
288
|
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
|
|
289
289
|
with:
|
|
290
290
|
python-version: "3.13"
|
|
291
|
-
- uses: kohli217/codex-workspace-bootstrap@v0.
|
|
291
|
+
- uses: kohli217/codex-workspace-bootstrap@v0.9.0
|
|
292
292
|
with:
|
|
293
293
|
path: .
|
|
294
294
|
strict: "true"
|
|
@@ -304,9 +304,11 @@ Read-only evaluations against real public repositories are documented in [docs/P
|
|
|
304
304
|
|
|
305
305
|
## GitHub App
|
|
306
306
|
|
|
307
|
+
**Public App:** [CWB Preflight](https://github.com/apps/cwb-preflight) · [Install](https://github.com/apps/cwb-preflight/installations/new)
|
|
308
|
+
|
|
307
309
|
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
310
|
|
|
309
|
-
The
|
|
311
|
+
The verified zero-cost public-OSS deployment uses:
|
|
310
312
|
|
|
311
313
|
```text
|
|
312
314
|
GitHub App
|
|
@@ -320,7 +322,9 @@ This path does not require a Google Cloud billing account. It is intentionally *
|
|
|
320
322
|
|
|
321
323
|
The App keeps the minimum repository permissions documented by the project: Checks read/write, Contents read-only, and Pull requests read-only.
|
|
322
324
|
|
|
323
|
-
|
|
325
|
+
The current public App has been revalidated after promotion: the renamed `CWB Preflight` App produced a successful `READY` Check Run through the full Cloudflare → Queue → GitHub Actions → installation-token → exact-revision checkout path.
|
|
326
|
+
|
|
327
|
+
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 deployment.
|
|
324
328
|
|
|
325
329
|
## Integration boundary
|
|
326
330
|
|
|
@@ -347,7 +351,7 @@ The intent is to complement those tools, not replace them.
|
|
|
347
351
|
- Suspected secret files are not opened or printed by the filename-risk check.
|
|
348
352
|
- Repository contents are not sent to a remote AI service by the core audit.
|
|
349
353
|
- Instruction files are read locally for deterministic linting; extracted commands are never executed by the integrity lint.
|
|
350
|
-
- Repository instruction/configuration and project-marker
|
|
354
|
+
- Repository instruction/configuration and project-marker contents are not trusted through symbolic links. CWB may recognize the exact lexical aliases `CLAUDE.md -> AGENTS.md` and `GEMINI.md -> AGENTS.md` (or `./AGENTS.md`) only when the sibling `AGENTS.md` is a regular file; linting reads that regular file directly rather than following either link. Gemini aliases are recognized only when `GEMINI.md` is an effective Gemini context filename.
|
|
351
355
|
- `init-agents` and automatic fixes refuse symlinked `AGENTS.md` targets rather than writing through them.
|
|
352
356
|
- Existing regular `AGENTS.md` files are protected unless overwrite is explicit.
|
|
353
357
|
- A passing preflight is evidence about the checks performed, **not a security guarantee**.
|
|
@@ -381,7 +385,7 @@ py -m pip install codex-workspace-bootstrap
|
|
|
381
385
|
Pinned GitHub release artifact:
|
|
382
386
|
|
|
383
387
|
```powershell
|
|
384
|
-
py -m pip install "https://github.com/kohli217/codex-workspace-bootstrap/releases/download/v0.
|
|
388
|
+
py -m pip install "https://github.com/kohli217/codex-workspace-bootstrap/releases/download/v0.9.0/codex_workspace_bootstrap-0.9.0-py3-none-any.whl"
|
|
385
389
|
```
|
|
386
390
|
|
|
387
391
|
## Development
|
|
@@ -101,7 +101,7 @@ def test_version_flag_reports_package_version(capsys: pytest.CaptureFixture[str]
|
|
|
101
101
|
main(["--version"])
|
|
102
102
|
|
|
103
103
|
assert exc_info.value.code == 0
|
|
104
|
-
assert "codex-workspace-bootstrap 0.
|
|
104
|
+
assert "codex-workspace-bootstrap 0.9.0" in capsys.readouterr().out
|
|
105
105
|
|
|
106
106
|
|
|
107
107
|
def test_audit_writes_sarif(tmp_path: Path) -> None:
|