Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .ast-grep/rules/component-literals-only-in-rules.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Invariant 5: signer and verifier agree on every rule. The profile's
# component lists are stated once, in rules.py, and both roles read them.
# A component identifier written anywhere else is a second copy of a rule.
id: component-literals-only-in-rules
language: Python
severity: error
message: Component identifiers are stated once, in rules.py; signer, verifier, and directory read them from there.
files:
- "src/iab_agentic_primitives/web_bot_auth/**/*.py"
ignores:
- "src/iab_agentic_primitives/web_bot_auth/rules.py"
# Fixture data: the scenarios list deliberately wrong components, as scenarios.json spells them.
- "src/iab_agentic_primitives/web_bot_auth/conformance/scenarios.py"
rule:
kind: string
any:
# The first argument of component_id("content-digest"), _key("@method"),
# and the like.
- all:
- nthChild: 1
- inside:
kind: argument_list
inside:
kind: call
has:
field: function
regex: '^(component_id|component_key|_key)$'
# A bare derived-component name such as "@target-uri" or '"@method";req'.
# A docstring opens with three quotes, so the regex never matches one.
- regex: '^["'']"?@[a-z-]+'
25 changes: 25 additions & 0 deletions .ast-grep/rules/library-verify-checks-components-and-body.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# The RFC 9421 seam hands the library the components the profile requires and
# the covered body, so the library checks them as well as the profile does
# (invariant 1). The profile's own checks run first, so no behavior test can
# tell the library's checks apart; this rule keeps them in place.
id: library-verify-checks-components-and-body
language: Python
severity: error
message: The seam's call to the library verifier passes require_components= and body=.
files:
- "src/iab_agentic_primitives/web_bot_auth/_sig.py"
rule:
pattern: $V.verify($$$ARGS)
any:
- not:
has:
kind: argument_list
has:
kind: keyword_argument
regex: "^require_components="
- not:
has:
kind: argument_list
has:
kind: keyword_argument
regex: "^body="
21 changes: 21 additions & 0 deletions .ast-grep/rules/no-absolute-self-import.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# The module is self-contained: it reaches its siblings through relative
# imports only, so it can be vendored or moved without edits.
id: no-absolute-self-import
language: Python
severity: error
message: web_bot_auth is self-contained and uses relative imports only.
files:
- "src/iab_agentic_primitives/web_bot_auth/**/*.py"
rule:
any:
# import iab_agentic_primitives, import iab_agentic_primitives.x as y
- kind: import_statement
has:
stopBy: end
kind: dotted_name
regex: '^iab_agentic_primitives(\.|$)'
# from iab_agentic_primitives.x import y
- kind: import_from_statement
has:
field: module_name
regex: '^iab_agentic_primitives(\.|$)'
44 changes: 44 additions & 0 deletions .ast-grep/rules/no-cross-module-private-import.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# A private name belongs to its module. Another module reaches it only
# through a public seam, so the layer order cannot be bypassed by an import,
# and a test reaches the module only through its public surface.
id: no-cross-module-private-import
language: Python
severity: error
message: A private name belongs to its module; another module imports it only through a public seam.
files:
- "src/iab_agentic_primitives/web_bot_auth/**/*.py"
rule:
kind: dotted_name
regex: '^_[a-z]'
inside:
kind: import_from_statement
any:
# from .directory import _checked_max_age
- has:
kind: relative_import
regex: '^\.+[a-z]'
# from iab_agentic_primitives.web_bot_auth.verifier import _select
- has:
field: module_name
regex: '^iab_agentic_primitives\.web_bot_auth(\.[a-z][a-z_]*)+$'
---
id: no-cross-module-private-import-in-tests
language: Python
severity: error
message: A test reaches a private name; test the behavior through the public API instead.
files:
- "tests/web_bot_auth/**/*.py"
rule:
kind: dotted_name
regex: '^_[a-z]'
inside:
kind: import_from_statement
any:
# from .directory import _checked_max_age
- has:
kind: relative_import
regex: '^\.+[a-z]'
# from iab_agentic_primitives.web_bot_auth.verifier import _select
- has:
field: module_name
regex: '^iab_agentic_primitives\.web_bot_auth(\.[a-z][a-z_]*)+$'
17 changes: 17 additions & 0 deletions .ast-grep/rules/no-host-derived-authority.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Invariant 3: the server's own authority comes from configuration, never
# from the request. Reading Host, Forwarded, or the ASGI server tuple to
# decide what the server is would let a client choose the authority a
# signature is checked against.
id: no-host-derived-authority
language: Python
severity: error
message: The server's authority comes from configuration (base_url / authorities), never from Host, Forwarded, or scope["server"].
files:
- "src/iab_agentic_primitives/web_bot_auth/**/*.py"
rule:
any:
- pattern: $S["server"]
- pattern: $S.get("server")
- all:
- kind: string
- regex: '^b?["''](host|x-forwarded-host|forwarded)["'']$'
28 changes: 28 additions & 0 deletions .ast-grep/rules/refusal-minted-in-one-place.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Invariant 4: every refusal is a typed WebBotAuthError with a status from
# the profile's table. A refusal is minted from that table in one place, so
# no call site can pick its own status or reason.
id: refusal-minted-in-one-place
language: Python
severity: error
message: A refusal is minted in errors.py from a Refusal member; elsewhere, raise refuse(Refusal.X, detail).
files:
- "src/iab_agentic_primitives/web_bot_auth/**/*.py"
ignores:
- "src/iab_agentic_primitives/web_bot_auth/errors.py"
rule:
any:
- pattern: WebBotAuthError($$$A)
- pattern: $M.WebBotAuthError($$$A)
---
# A test asserts a refusal by its Refusal member, through the one helper that
# checks the member and its status together.
id: refusal-asserted-through-expect-refusal
language: Python
severity: error
message: Assert a refusal with expect_refusal(call, Refusal.X), not pytest.raises(WebBotAuthError).
files:
- "tests/web_bot_auth/**/*.py"
rule:
any:
- pattern: pytest.raises(WebBotAuthError $$$A)
- pattern: raises(WebBotAuthError $$$A)
30 changes: 30 additions & 0 deletions .ast-grep/rules/signature-fields-parsed-once.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Signature and Signature-Input are parsed by one wire-seam function. Any
# other module that reads them by name is a second parser whose rules can
# drift from the first.
id: signature-fields-parsed-once
language: Python
severity: error
message: Signature / Signature-Input are parsed by one L2 function; nothing else reads them by name.
files:
- "src/iab_agentic_primitives/web_bot_auth/**/*.py"
ignores:
- "src/iab_agentic_primitives/web_bot_auth/_wire.py"
# Fixture data: a scenario appends raw members to these fields after signing.
- "src/iab_agentic_primitives/web_bot_auth/conformance/scenarios.py"
rule:
all:
- kind: string
- regex: '^b?["'']signature(-input)?["'']$'
- inside:
kind: call
stopBy: end
# component_id("signature", {"req": True, ...}) names the covered
# component, not the field; component-literals-only-in-rules owns it.
- not:
inside:
kind: argument_list
inside:
kind: call
has:
field: function
regex: '^component_id$'
31 changes: 31 additions & 0 deletions .ast-grep/rules/verified-types-constructed-by-owner.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# A verified credential is constructed only by the code that verified it, so
# holding one means the check ran. Tests get one by verifying a real message.
id: verified-credentials-constructed-by-owner
language: Python
severity: error
message: VerifiedRequest and VerifiedResponse are constructed only in verifier.py.
files:
- "src/iab_agentic_primitives/web_bot_auth/**/*.py"
- "tests/web_bot_auth/**/*.py"
ignores:
- "src/iab_agentic_primitives/web_bot_auth/verifier.py"
rule:
any:
- pattern: VerifiedRequest($$$A)
- pattern: $M.VerifiedRequest($$$A)
- pattern: VerifiedResponse($$$A)
- pattern: $M.VerifiedResponse($$$A)
---
id: verified-directory-constructed-by-owner
language: Python
severity: error
message: VerifiedDirectory is constructed only in directory.py; tests verify a real signed directory.
files:
- "src/iab_agentic_primitives/web_bot_auth/**/*.py"
- "tests/web_bot_auth/**/*.py"
ignores:
- "src/iab_agentic_primitives/web_bot_auth/directory.py"
rule:
any:
- pattern: VerifiedDirectory($$$A)
- pattern: $M.VerifiedDirectory($$$A)
33 changes: 32 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,39 @@ jobs:
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
python-version: "3.11"
python-version: "3.12"
- name: Run tests
run: uv run pytest
- name: Lint
run: uv run ruff check .

# The Web Bot Auth gates, in order: the ruff format check, the ruff lint,
# the seam boundary, the structural rules, the import contracts, mypy
# strict, and the behavior suite with its coverage floor.
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
python-version: "3.12"
- name: Run the Web Bot Auth quality gates
run: make quality

# The interop tests, which pytest deselects by default: Cloudflare's
# web-bot-auth signs and verifies against this library through Node.
web-bot-auth-interop:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
python-version: "3.12"
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "22"
- name: Run the interop harness against the module's adapter
run: make interop
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ build/
*.egg-info/
.pytest_cache/
.ruff_cache/
# Web Bot Auth quality gates (make quality)
.mypy_cache/
.import_linter_cache/
.coverage

# Secrets hygiene
.env
Expand Down
Loading