diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a361228..ee6fbec 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,6 +3,10 @@ name: Release on: push: tags: ["v*"] + # Dispatch publishes to TestPyPI only. There is deliberately no input selecting the + # target: a dry run that can be pointed at production PyPI by choosing the wrong + # dropdown entry is a worse hazard than the one it exists to remove. + workflow_dispatch: permissions: contents: read @@ -24,14 +28,28 @@ jobs: # The tag is the only thing a human types in this pipeline, and the version is # written in exactly one place. If they disagree, PyPI would receive a package # declaring a version nobody tagged, permanently. + # Runs on both triggers: the declared and installed versions must agree whether or + # not a tag is involved, and a dry run is the cheapest place to catch a disagreement. + - name: Declared and installed versions must agree + run: | + declared="$(python -c 'import tomllib,pathlib; print(tomllib.loads(pathlib.Path("pyproject.toml").read_text())["project"]["version"])')" + installed="$(python -c 'import importlib.metadata as m; print(m.version("agentguard"))')" + echo "pyproject=$declared installed=$installed" + if [ "$declared" != "$installed" ]; then + echo "::error::pyproject declares '$declared' but the installed distribution is '$installed'" + exit 1 + fi + + # Tag-only: on a workflow_dispatch, GITHUB_REF_NAME is a branch name, and comparing a + # branch to a version would fail every dry run for the wrong reason. - name: Tag must match the declared version + if: startsWith(github.ref, 'refs/tags/') run: | tag="${GITHUB_REF_NAME#v}" declared="$(python -c 'import tomllib,pathlib; print(tomllib.loads(pathlib.Path("pyproject.toml").read_text())["project"]["version"])')" - installed="$(python -c 'import importlib.metadata as m; print(m.version("agentguard-sast"))')" - echo "tag=$tag pyproject=$declared installed=$installed" - if [ "$tag" != "$declared" ] || [ "$tag" != "$installed" ]; then - echo "::error::tag '$tag' disagrees with pyproject ('$declared') / installed ('$installed')" + echo "tag=$tag pyproject=$declared" + if [ "$tag" != "$declared" ]; then + echo "::error::tag '$tag' disagrees with pyproject ('$declared')" exit 1 fi @@ -61,12 +79,43 @@ jobs: name: distributions path: dist/ + # Rehearsal. Exercises what a local install cannot: PyPI's metadata validation on + # receipt, the OIDC token exchange, and how the project page renders. It validates the + # *mechanism*; TestPyPI has its own trusted-publisher configuration, so a green run here + # says nothing about whether the production publisher is configured correctly. + publish-testpypi: + needs: build + if: github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + environment: + name: testpypi + url: https://test.pypi.org/p/agentguard + permissions: + id-token: write + steps: + - uses: actions/download-artifact@v8 + with: + name: distributions + path: dist/ + - uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://test.pypi.org/legacy/ + # A version can only be uploaded once. Without this, the second rehearsal of any + # given version fails on a conflict that says nothing about the release. Set here + # and deliberately not on the production job, where a conflict is a real signal. + skip-existing: true + publish: needs: build + # Both halves are load-bearing. A workflow_dispatch can be run against a *tag* ref, so + # a tag-only condition would let a manual dispatch reach production PyPI - which is the + # one action in this pipeline that cannot be undone. Requiring the push event as well + # means production is reachable only by pushing a tag. + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest environment: name: pypi - url: https://pypi.org/p/agentguard-sast + url: https://pypi.org/p/agentguard permissions: id-token: write steps: @@ -78,6 +127,7 @@ jobs: github-release: needs: publish + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest permissions: contents: write diff --git a/CHANGELOG.md b/CHANGELOG.md index 62298e5..a337d58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,20 @@ All notable changes follow [Keep a Changelog](https://keepachangelog.com/en/1.1. ### Changed +- **The distribution is named `agentguard`, not `agentguard-sast`.** Renamed before first + publication, so no package under the old name has ever existed on PyPI — early git history + referencing `agentguard-sast` describes a name that was never published, not one that was + retired. The import package and the CLI entry point were already `agentguard` and are + unchanged; only the name you `pip install` is affected, and only for anyone who built from + source before this release. +- **Exit code semantics.** A scan that does not complete now exits `2` where some cases previously + exited `0`. Automation that treated `0` as "clean" was previously being misled; it is now correct. + Files skipped by declared policy (`max_file_size_kb`) remain non-failing. +- **Config schema.** `plugins`, `disabled_rules`, and `severity_overrides` are no longer accepted in + a `.agentguard.yml` discovered in the repository under scan; they now require an explicit + `--config`. Migration: pass `--config .agentguard.yml` to keep the previous behaviour for a + repository you own, or move those keys to an operator-supplied file. `agentguard init` now emits + the repository-safe subset. - **Rules declare their context; the engine enforces it.** `RuleMetadata` gained `languages` (required), `ignore_regions`, `require_nodes`, and `fixture_policy`. Language gating, comment/docstring/annotation awareness, node-kind gating, and test-fixture @@ -35,6 +49,10 @@ All notable changes follow [Keep a Changelog](https://keepachangelog.com/en/1.1. ### Fixed +- A rule that raised on every file, or a file that could not be decoded, previously produced exit + code `0` — indistinguishable from a clean scan, so CI reported green with zero coverage. The exit + code now honours the same invariant that SARIF `executionSuccessful` already reported. Exit `2` + outranks the `--fail-on` threshold. - `AG002` read any `.exec()` or `.eval()` method as the builtin, because call-name resolution returned the bare attribute when the receiver was not a plain name. `super().exec(*command)` was reported as critical arbitrary code execution. @@ -83,24 +101,6 @@ All notable changes follow [Keep a Changelog](https://keepachangelog.com/en/1.1. can only tighten a scan. The same fix closes repository control over `follow_symlinks`, `max_file_size_kb`, and `exclude`. -### Fixed - -- A rule that raised on every file, or a file that could not be decoded, previously produced exit - code `0` — indistinguishable from a clean scan, so CI reported green with zero coverage. The exit - code now honours the same invariant that SARIF `executionSuccessful` already reported. Exit `2` - outranks the `--fail-on` threshold. - -### Changed - -- **Exit code semantics.** A scan that does not complete now exits `2` where some cases previously - exited `0`. Automation that treated `0` as "clean" was previously being misled; it is now correct. - Files skipped by declared policy (`max_file_size_kb`) remain non-failing. -- **Config schema.** `plugins`, `disabled_rules`, and `severity_overrides` are no longer accepted in - a `.agentguard.yml` discovered in the repository under scan; they now require an explicit - `--config`. Migration: pass `--config .agentguard.yml` to keep the previous behaviour for a - repository you own, or move those keys to an operator-supplied file. `agentguard init` now emits - the repository-safe subset. - ## [0.1.0] - 2026-07-16 — NEVER PUBLISHED > **This release does not exist.** The entry below was written in advance and the release diff --git a/README.md b/README.md index bed9e2c..8019045 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Offline. Never executes the code it scans. Designed to be run on repositories yo not read. ```bash -pipx install agentguard-sast +pipx install agentguard agentguard scan ./project ``` @@ -122,10 +122,10 @@ AgentGuard requires Python 3.10 or newer. ```bash # Isolated CLI installation (recommended) -pipx install agentguard-sast +pipx install agentguard # Or with pip -python -m pip install agentguard-sast +python -m pip install agentguard # From source git clone https://github.com/amic25/agentguard.git diff --git a/WORKLOG.md b/WORKLOG.md index 85a8707..644f61e 100644 --- a/WORKLOG.md +++ b/WORKLOG.md @@ -1604,3 +1604,215 @@ Either way this is a decision about a namespace you own, so it stays with you. - The `[0.1.0] — NEVER PUBLISHED` annotation untouched. - PRs #12, #13, #14, #15 untouched. #14 adds a pattern to the unbounded AG001 and will be gated by the linearity job on its own PR now that `--check` runs in CI. + +--- + +## Unit 21 — distribution renamed to `agentguard` — 2026-07-30 + +Status: complete +Changed: `pyproject.toml`, `src/agentguard/__init__.py`, `tests/test_cli.py`, +`.github/workflows/release.yml`, `README.md`, `docs/launch/REDDIT.md`, `CHANGELOG.md` + +### Nine sites, not six + +The brief listed six. Post-#19 there are **nine**, and the three additions are the ones +that would have failed loudest: + +``` +pyproject.toml:6 name +src/agentguard/__init__.py:16 importlib.metadata lookup <- added by #19 +tests/test_cli.py:65 metadata assertion <- added by #19 +.github/workflows/release.yml:31 tag/version check <- added by #19 +.github/workflows/release.yml:69 PyPI environment URL +README.md:12, 125, 128 install instructions +docs/launch/REDDIT.md:10 install instruction +``` + +Renaming without the three from #19 would have left `__version__` falling back to +`0.0.0+unknown`, so `--version`, the JSON `tool.version`, and the SARIF driver version +would all have reported a version that does not exist — and the release `verify` job's tag +check would have failed on a `PackageNotFoundError` after the tag was pushed. Precisely the +class of failure #19 was written to prevent, reintroduced by a rename that looked textual. + +Seven references remain in `WORKLOG.md` and are deliberate: that file is the historical +record, and the old name is part of it. + +### Verified after a clean reinstall + +A stale editable install would have masked a broken metadata lookup, so the old +distribution was uninstalled first: + +``` +distribution -> agentguard +__version__ -> 0.2.0 +agentguard --version -> AgentGuard 0.2.0 +version("agentguard-sast") -> PackageNotFoundError (correct: the old name is gone) +built artifacts -> agentguard-0.2.0.tar.gz, agentguard-0.2.0-py3-none-any.whl +twine check -> PASSED (both) +wheel metadata -> Name: agentguard, License-Expression: Apache-2.0 +wheel package dir -> agentguard/ (import name unchanged, as expected) +clean-venv install -> pip show name: agentguard, --version: AgentGuard 0.2.0 +pytest -> 198 passed, 1 xfailed +bench -> 33 of 34 behave as labelled +linearity gate -> exit 0 +``` + +### CHANGELOG had two `### Changed` and two `### Fixed` under `[Unreleased]` + +Not caused by this unit — accumulated across earlier commits, each appending its own +section rather than merging into the existing one. Adding the rename note made it three, +which is how it surfaced. Consolidated to one of each in Keep a Changelog order (Added, +Changed, Fixed, Removed, Security); all four affected bullets verified still present, none +dropped in the move. + +Bench delta: none — a distribution name change touches no rule. +Decisions taken alone: +1. **`WORKLOG.md` left untouched.** It records what was true at the time, and rewriting it + to say `agentguard` would make the earlier entries wrong. +2. **CHANGELOG sections consolidated** rather than left duplicated, since the file is + about to be read by anyone evaluating a first release. +Next: unit 2, the TestPyPI dry run. + +--- + +## Unit 22 — TestPyPI dry run — 2026-07-30 + +Status: complete (job added; **the run itself needs a publisher I cannot configure** — see +the residual gap below) +Changed: `.github/workflows/release.yml` + +`workflow_dispatch` now publishes to TestPyPI through the same trusted-publishing shape as +production: `id-token: write`, a named environment, `pypa/gh-action-pypi-publish`, and the +same `verify → build` gate. Only the `repository-url` and the environment differ. + +``` +job needs runs when +verify - both triggers +build verify both triggers +publish-testpypi build workflow_dispatch only +publish build push AND refs/tags/** +github-release publish push AND refs/tags/** +``` + +### A hole found while writing it + +`workflow_dispatch` can be run against a **tag** ref, not only a branch. With production +gated on `startsWith(github.ref, 'refs/tags/')` alone, a manual dispatch on a tag would +have satisfied it and published to production PyPI — the one action in this pipeline that +cannot be undone, reachable from a dropdown. Production now also requires +`github.event_name == 'push'`. Evaluated across every combination: + +``` +event ref TestPyPI production +push refs/tags/v0.2.0 False True +workflow_dispatch refs/heads/main True False +workflow_dispatch refs/tags/v0.2.0 True False <- the hole, now closed +``` + +There is deliberately **no input**选 selecting the publish target. A dry run that can be +aimed at production by picking the wrong dropdown entry is a worse hazard than the one it +removes. + +### Two adjustments the dry run forced + +- **The tag check had to be split.** On a dispatch, `GITHUB_REF_NAME` is a branch name, so + comparing it to a version would fail every dry run for the wrong reason. It is now two + steps: *declared vs installed* runs on both triggers (a disagreement is worth catching in + a rehearsal), and *tag vs declared* is guarded by `startsWith(github.ref, 'refs/tags/')`. +- **`skip-existing: true` on the TestPyPI job only.** TestPyPI accepts a version once, so + the second rehearsal of `0.2.0` would otherwise fail on a conflict that says nothing + about the release. Deliberately not set on production, where a version conflict is a real + signal that something is wrong. + +### Residual gap — stated plainly + +**A green dry run validates the mechanism, not the production configuration.** TestPyPI +holds its own trusted-publisher record, separate from PyPI's. A successful TestPyPI publish +proves the workflow shape, the OIDC exchange, metadata acceptance, and page rendering — and +proves *nothing* about whether the PyPI publisher for `agentguard` names the right owner, +repository, workflow filename, and environment. That one is verifiable only by inspection, +which is unit 23. + +**I have not run the dispatch.** It needs a TestPyPI project with a trusted publisher for +this repository and a GitHub environment named `testpypi`, both of which are yours. Once +they exist: Actions → Release → Run workflow, from any branch. + +After it runs, the page checks worth doing are: README rendering as Markdown rather than +raw markup, `License-Expression: Apache-2.0` showing as a licence badge rather than the +wall of text the pre-#19 metadata would have produced, and the four project URLs resolving. + +Bench delta: none — workflow only. +Decisions taken alone: +1. **No target input on the dispatch.** Rejected a `choice` input for production-vs-test: + the failure mode it introduces is worse than the flexibility it buys. +2. **`skip-existing` on TestPyPI only**, reasoning above. +Next: unit 23, documenting the settings CI cannot see. + +--- + +## Unit 23 — settings outside version control, and corpus containment as a decision — 2026-07-30 + +Status: complete +Changed: `docs/GITHUB_SETUP.md`, `docs/DECISIONS.md` + +### Three live problems found while writing the checklist + +Checking each setting rather than describing it turned up three things wrong right now: + +1. **No GitHub environments exist.** `gh api .../environments` returns `[]`, while + `release.yml` references `pypi` and now `testpypi`. GitHub creates one implicitly on + first use, so a publish would still succeed — but with **no protection rules**, meaning + anyone able to push a tag can publish. And if the PyPI publisher record names an + environment, the names must match exactly or the OIDC exchange fails. +2. **The About field still advertises "vulnerable dependencies".** AG009 was deleted in + #17; the repository page has claimed the capability ever since. Not fixed here — it is + a setting, and settings are yours — but it is the clearest possible demonstration of why + the checklist exists: nothing in CI renders that string, so nothing could fail. +3. **`GITHUB_SETUP.md` was recommending the mistake.** Line 6 told the reader to require + the `test` and `package` checks. `test` is one of the three phantom contexts that + blocked every merge for two sessions. The document that onboards a maintainer was + teaching them to reproduce the incident. Corrected, with the reason attached. + +### What the checklist covers + +Six surfaces, each with what depends on it, a verify command, and whether the breakage is +recoverable: PyPI trusted publisher (**unrecoverable** — fails after the tag is spent), +the `pypi`/`testpypi` environments, branch-ruleset required contexts, About and topics, +dependency graph and Dependabot alerts, and the social preview upload. + +Live state recorded at the time of writing: +``` +environments [] <- neither exists +required contexts ["CodeQL","build","ci-ok"] <- all real check-run names +vulnerability-alerts 204 No Content <- enabled +About "...vulnerable dependencies..." <- stale, AG009 is gone +``` + +Plus a five-item pre-tag sequence, ordered so the one unrecoverable item is checked last +and checked twice. + +### Two `DECISIONS.md` entries + +**"A CI-verifiable repository still has a configuration surface CI cannot see."** The +honest framing is that an inspection checklist is a weak control — manual, staleable, only +run by someone who remembers. It is there because the alternative is nothing. Four +incidents, and the shape is identical every time: *the repository is green, and the thing +that is wrong is not in the repository*. The entry also records the one real mitigation — +pulling settings into version control where possible, which is what `ci-ok` does by +converting a branch-protection problem into a workflow problem. + +**"Corpus containment is per-consumer, because no single boundary holds."** Four readers, +no two sharing a mechanism, and the directory irrelevant to all four — `tests/corpus/` is a +convention this project observes and nothing else does. The standing rule is stated: any +file whose shape implies a role will be interpreted by something regardless of location, +and new corpus files of those shapes get checked against the reader list before landing. +Both existing mitigations are noted as non-generalising. + +Verified: 198 passed + 1 xfailed, lint, mypy, bench 33 of 34. +Bench delta: none — documentation only. +Decisions taken alone: +1. **Did not change the About field.** It is a setting and settings are yours; flagged + instead, with the corrected wording supplied in `GITHUB_SETUP.md` so it can be pasted. +2. **Corrected the stale branch-protection recommendation** rather than only documenting + the general rule — a document actively recommending a known failure is worse than one + that is silent. diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 6dfd3ad..cab97b3 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -236,3 +236,73 @@ bias: the same reader labelled them twice and disagreed with himself on 5 of 20, five moving the same direction. A number that cannot be re-derived and whose labels are disputed is a marketing claim. The dataset ships with its method and its bias so the labels can be argued with, which is worth more than the headline. + +--- + +## A CI-verifiable repository still has a configuration surface CI cannot see + +Everything in this project is designed to be checked by a command: rules declare their +context and a schema enforces it, the linearity gate discovers unbounded patterns by +introspection, `make bench` reproduces every published number, and the release proves +itself before publishing. That discipline stops at the repository boundary. + +**Cost:** an inspection checklist is the weakest kind of control. It is manual, it goes +stale, and it is only run by someone who remembers to run it. + +**Why anyway:** because the alternative is nothing. Four incidents so far, and in every one +the repository looked healthy: + +| Setting | What happened | +|---|---| +| Required contexts `test`, `pytest`, `CI` | Every PR sat at `BLOCKED` with all checks green. Three names that no job produces, typed by hand. | +| Dependency graph disabled | `dependency-review.yml` failed on every PR with an error naming a feature, not a defect. | +| About description | Advertised "vulnerable dependencies" for the whole life of the branch that deleted AG009. No signal at all — nothing renders it in CI. | +| PyPI trusted publisher | Not yet triggered. Would fail the OIDC exchange **after** the tag is pushed, spending the version. | + +The shape is consistent: **the repository is green, and the thing that is wrong is not in +the repository.** No test can fail, because there is nothing to run. + +Two things follow. First, `docs/GITHUB_SETUP.md` carries a checklist naming every such +surface, what depends on it, how to verify it, and whether the breakage is recoverable — +the trusted publisher is marked unrecoverable, because it fails after the tag is spent. +Second, anything that *can* be pulled into version control should be: `ci-ok` exists so a +matrix can change without editing branch protection, which converts a settings problem into +a workflow problem, where CI can see it. + +--- + +## Corpus containment is per-consumer, because no single boundary holds + +A security scanner's test corpus is, by construction, indistinguishable from a vulnerable +codebase. It has to be: a corpus that does not look dangerous does not test anything. + +**Cost:** containment cannot be solved once. Each new consumer of the repository reads the +corpus in its own way and needs its own exclusion, and the list only grows. + +**Why anyway:** because every attempt to draw one boundary has failed. Four readers so far, +each needing a different mechanism: + +| Reader | Read the corpus as | Fix | +|---|---|---| +| GitHub dependency graph | project dependency manifests | fictional package names in fixture `requirements.txt` | +| GitHub dependency review | a real advisory against a real dependency | same fix; the advisory was genuine, the dependency was not | +| sdist consumers, package-cache secret scanners | committed credentials | `/tests` excluded from the sdist | +| Ruff | project source with undefined names and unsafe calls | `extend-exclude` in `pyproject.toml` | + +Note that no two share a mechanism, and that the directory the files live in was irrelevant +to all four. `tests/corpus/` is a convention this project observes and nothing else does. + +### The standing rule + +**Any file whose *shape* implies a role — `.env`, `requirements.txt`, `package.json`, +lockfiles, anything under `.github/` — will be interpreted by something, regardless of the +directory it sits in.** + +Before adding a corpus file of one of those shapes, check it against the current reader +list above. The check is cheap and the failures are not: the dependency-review incident +surfaced as a genuine high-severity advisory on a pull request, and reading it as a real +finding rather than a corpus artifact would have been entirely reasonable. + +Two mitigations are already in place and worth keeping: fixture manifests name fictional +packages and say why in a header comment, and the sdist ships no tests at all. Neither +generalises to the next reader. diff --git a/docs/GITHUB_SETUP.md b/docs/GITHUB_SETUP.md index a1f0646..1a25539 100644 --- a/docs/GITHUB_SETUP.md +++ b/docs/GITHUB_SETUP.md @@ -2,7 +2,11 @@ Recommended repository description: -> Open-source AI agent security scanner for secrets, prompt injection, unsafe tools, excessive privileges, system access, APIs, validation, and dependencies. +> Open-source AI agent security scanner for secrets, prompt injection, unsafe tools, excessive privileges, system access, APIs, and validation. + +Note the omission of "dependencies": AG009 was deleted and AgentGuard does not check +dependencies against vulnerability advisories. The description is a claim surface `grep` +cannot reach — see [Settings outside version control](#settings-outside-version-control). Recommended topics: @@ -15,9 +19,108 @@ After creating the repository: 3. Create the five issues in [GOOD_FIRST_ISSUES.md](GOOD_FIRST_ISSUES.md). 4. Enable Discussions categories: Announcements, Q&A, Ideas, Show and tell, and Rule proposals. 5. Pin an introductory Discussion using `.github/DISCUSSION_TEMPLATE/welcome.yml` as the guide. -6. Protect `main`: require pull requests, one approval, conversation resolution, and the `test`, `package`, and `CodeQL` checks; disallow force pushes and deletions. +6. Protect `main`: require pull requests, conversation resolution, and the `ci-ok`, `build`, and `CodeQL` checks; disallow force pushes and deletions. **Pick every required context from GitHub's suggestion list, never by typing it** — `test` and `package` are not check-run names and can never report. See below. 7. Configure PyPI trusted publishing for the `release.yml` workflow before tagging a release. 8. Register for the OpenSSF Best Practices badge, then add the assigned project badge. 9. Add a social preview derived from `docs/assets/social-preview.svg`. Sponsors is intentionally empty until a funding account is created. Update `.github/FUNDING.yml` then publish a sponsor prospectus describing maintenance, response, and roadmap funding goals. + + +--- + +## Settings outside version control + +Everything below is required by something in this repository and is stored in GitHub or +PyPI settings, where no test, linter, or CI job can see it. Four incidents so far were +caused by one of these being wrong, and in each case the repository looked healthy: + +| Incident | Setting | Symptom | +|---|---|---| +| Merges blocked with all checks green | required contexts `test`, `pytest`, `CI` | `BLOCKED`, no failing check | +| Dependency review failed every PR | dependency graph disabled | error naming a feature, not a defect | +| Deleted rule still advertised | About field | no signal at all | +| (pending) | PyPI trusted publisher | fails **after** the tag is spent | + +Verify each before a release. The commands assume `gh` is authenticated. + +### PyPI trusted publisher — **unrecoverable if wrong** + +Required by `release.yml`'s `publish` job. The record on PyPI must match **exactly**: +owner `amic25`, repository `agentguard`, workflow filename `release.yml`, environment +`pypi`. A mismatch fails the OIDC exchange — and it fails *after* the tag has been pushed, +which means the version is spent. PyPI versions can be yanked but never replaced, so the +recovery is a new version number and a permanent gap in the history. + +There is no API to read this back. Verify by eye at +`https://pypi.org/manage/project/agentguard/settings/publishing/`, and rehearse the +mechanism first with the TestPyPI dry run (Actions → Release → Run workflow), which has +its own separate publisher record and validates the shape but not this configuration. + +### GitHub environments `pypi` and `testpypi` + +Referenced by `release.yml`. Verify: + +``` +gh api repos/amic25/agentguard/environments --jq '[.environments[].name]' +``` + +If this returns `[]`, neither exists. GitHub creates an environment implicitly on first +use, so a publish can succeed without one — but then it carries **no protection rules**, +and anyone able to push a tag can publish. If the PyPI publisher record names an +environment, the names must match exactly or the OIDC exchange fails. + +Recoverable: create the environment and re-run. But not if the tag is already spent. + +### Branch ruleset required contexts + +Verify against what actually reports: + +``` +gh api repos/amic25/agentguard/rulesets/19777822 \ + --jq '[.rules[] | select(.type=="required_status_checks") | .parameters.required_status_checks[].context]' +gh pr checks +``` + +Every required context must appear in the second list. **Always pick them from GitHub's +suggestion list, never type them.** `test`, `pytest`, and `CI` were all typed by hand; +none is a check-run name, so none could ever report, and every pull request sat at +`BLOCKED` with all checks green. `ci-ok` exists precisely so that a matrix can change +without touching this setting. Recoverable, but it blocks all merges until noticed. + +### About description and topics + +``` +gh api repos/amic25/agentguard --jq .description +gh api repos/amic25/agentguard/topics --jq '.names' +``` + +The description is a claim surface, and it is the one no sweep of the working tree reaches. +It advertised "vulnerable dependencies" after AG009 was deleted. Any claim here must hold +to the same standard as the README: reproducible by `make bench`, or not stated. +Recoverable at any time, but wrong in public until someone looks. + +### Dependency graph and Dependabot alerts + +``` +gh api repos/amic25/agentguard/vulnerability-alerts -i | head -1 # 204 = enabled +``` + +`dependency-review.yml` fails on every pull request when the graph is disabled, with an +error that reads like a broken workflow rather than a missing setting. Recoverable. + +### Social preview image + +`docs/assets/social-preview.svg` is in the repository; the uploaded preview is a setting +and is not verifiable by any command. Check by eye at Settings → General → Social preview. +Recoverable. + +### Before tagging + +1. `gh api .../environments` lists `pypi`. +2. The PyPI publisher record matches owner, repository, workflow, and environment. +3. A TestPyPI dry run has gone green. +4. `gh api .../description` makes no claim `make bench` cannot reproduce. +5. Required contexts all appear in `gh pr checks` on a live pull request. + +Only item 2 is unrecoverable. Check it last, and check it twice. diff --git a/docs/launch/REDDIT.md b/docs/launch/REDDIT.md index c83cc95..b2c0e74 100644 --- a/docs/launch/REDDIT.md +++ b/docs/launch/REDDIT.md @@ -7,7 +7,7 @@ I’m releasing AgentGuard, a Python CLI that scans Python and JS/TS agent code for leaked credentials, unsafe command/file access, broad tool permissions, prompt-injection paths, SSRF-style outbound calls, missing tool schemas, absent approval gates, and vulnerable dependencies. ```bash -pipx install agentguard-sast +pipx install agentguard agentguard scan . ``` diff --git a/pyproject.toml b/pyproject.toml index b00aca3..e636e38 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ requires = ["hatchling>=1.25"] build-backend = "hatchling.build" [project] -name = "agentguard-sast" +name = "agentguard" version = "0.2.0" description = "Static security scanner for AI agent applications" readme = "README.md" diff --git a/src/agentguard/__init__.py b/src/agentguard/__init__.py index 1f47e98..9147a33 100644 --- a/src/agentguard/__init__.py +++ b/src/agentguard/__init__.py @@ -13,6 +13,6 @@ #: `pyproject.toml` - is what `--version`, the JSON report's `tool.version`, and the #: SARIF driver version all report. Bumping one and not the other would have published #: a package declaring one version while every report it emitted claimed another. - __version__ = version("agentguard-sast") + __version__ = version("agentguard") except PackageNotFoundError: # pragma: no cover - source tree with no install __version__ = "0.0.0+unknown" diff --git a/tests/test_cli.py b/tests/test_cli.py index 5732784..3c870ee 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -62,4 +62,4 @@ def test_version_matches_package_metadata() -> None: result = runner.invoke(app, ["--version"]) assert result.exit_code == 0 - assert version("agentguard-sast") in result.stdout + assert version("agentguard") in result.stdout