diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index e75e04c6..315f6d1c 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,5 @@ blank_issues_enabled: false contact_links: - name: Security report - url: https://github.com/PetrefiedThunder/agent-middleware-api/security/advisories + url: https://github.com/agentmiddleware/agent-middleware-api/security/advisories about: Report sensitive security vulnerabilities privately. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 11d67c63..159aae7e 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,16 +1,96 @@ version: 2 + +# Every Dependabot pull request runs the full CI matrix (about 75 billable +# Actions minutes per run on a private repository). Updates are therefore +# grouped: one pull request per ecosystem per interval instead of one per +# package. Six separate pip pull requests in one week (#783 to #788) cost +# roughly 450 minutes of a 2,000 minute monthly allowance. +# +# requirements.lock is produced by `make lock` (uv), not by Dependabot. The +# required lint job fails a pull request whose requirements.txt no longer +# matches the lock, and admins cannot bypass required checks, so a grouped +# pip update cannot merge until the lock is regenerated on its own branch: +# git fetch origin && git checkout +# make lock && git commit -am "build(deps): regenerate requirements.lock" +# git push +# Dependabot keeps that commit unless the branch is rebased. updates: - package-ecosystem: "pip" directory: "/" schedule: interval: "weekly" - open-pull-requests-limit: 10 + day: "monday" + time: "06:00" + timezone: "America/Los_Angeles" + open-pull-requests-limit: 5 labels: - dependencies + groups: + python-dependencies: + applies-to: version-updates + patterns: + - "*" + python-security: + applies-to: security-updates + patterns: + - "*" + + # Published packages with their own manifests. Grouped per directory: at + # most one pull request each for the SDK and the four wrappers, instead of + # one per package. (A group spanning directories exists only as + # `group-by: dependency-name`, which is one pull request per dependency, + # worse for this purpose.) + - package-ecosystem: "pip" + directories: + - "/b2a_sdk" + - "/wrappers/*" + schedule: + interval: "weekly" + day: "monday" + time: "06:00" + timezone: "America/Los_Angeles" + open-pull-requests-limit: 2 + labels: + - dependencies + groups: + sdk-and-wrappers: + patterns: + - "*" + - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" + day: "monday" + time: "06:00" + timezone: "America/Los_Angeles" labels: - dependencies - ci + groups: + actions: + patterns: + - "*" + + # Dockerfile and Dockerfile.dev pin python:3.12-slim by digest + # (tests/test_ci_supply_chain_pins.py requires the digest form). Dependabot + # keeps the digest form and moves it to the current image. + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "monthly" + labels: + - dependencies + + # Railway infrastructure-as-code checked by scripts/trust_release_gate.sh + # (make check-railway-iac). + - package-ecosystem: "npm" + directory: "/.railway" + schedule: + interval: "monthly" + labels: + - dependencies + groups: + railway-iac: + patterns: + - "*" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 75db6679..334b5274 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -606,9 +606,9 @@ jobs: # Commit history of this push/PR: catches a secret that was added in one # commit and deleted in a later one, which the working-tree scan misses. - # Only the new commits are scanned — older history was verified - # separately (see the scope note in .gitleaks.toml) and still contains - # the rotated 2026-08-06 key, so a full-history scan would always fail. + # Only the new commits are scanned; older history is covered by the + # full-history scans recorded in the scope note of .gitleaks.toml (most + # recently 2026-10-22, after the 2026-10-20 history rewrite). - name: Scan commit range for committed secrets env: EVENT_NAME: ${{ github.event_name }} diff --git a/.gitleaks.toml b/.gitleaks.toml index fa5297da..1d5d1fd6 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -1,19 +1,25 @@ # Secret-scanning config for CI. # # Scope note: CI scans the working tree plus the commits in each push/PR -# range (.github/workflows/ci.yml secret_scan). Older history was verified -# separately with a full-history `gitleaks detect --log-opts=--all` under this -# config, most recently on 2026-09-19. The only real credential ever committed -# is the 2026-08-06 leaked key (see docs/api-key-rotation.md), and it is -# reachable from main's own history — the 2026-08-24 note here previously -# claimed the flattening had removed it from main and that pruning stale -# branches would remove the reachable copies. Both were wrong: commit 9e45009 -# is an ancestor of origin/main, and `git log origin/main -S` finds the value -# in three commits. The key was rotated dead at the provider on 2026-08-07, -# which was always the only real remediation, and the incident record -# deliberately publishes the retired value anyway. Do not restate the old -# claim. Every other historical finding is synthetic test material or a -# content hash, allowlisted below. +# range (.github/workflows/ci.yml secret_scan). Older history is covered by +# full-history scans (`gitleaks detect --log-opts=--all` under this config), +# most recently on 2026-10-22 against a fresh clone taken after the history +# rewrite of 2026-10-20: 1,410 commits across every branch, nothing outside +# the allowlist below. The only real credential ever committed is the key +# from the 2026-08-06 incident (committed 2026-08-04 in +# scripts/stress_test_live.py, removed from HEAD by #201 on 2026-08-07; see +# docs/api-key-rotation.md). It was rotated dead at +# the provider on 2026-08-07, which was always the only real remediation. It +# then stayed reachable from main's history (the 2026-08-24 note here claimed +# otherwise and was corrected on 2026-09-19) until the 2026-10-20 rewrite +# replaced its value with the literal ***REMOVED*** in every commit, so +# neither the value nor the commit that introduced it (formerly 9e45009) is +# reachable from any branch or tag any more. GitHub still serves that +# pre-rewrite commit by its full SHA until it is garbage-collected or purged +# by GitHub Support, and clones taken before 2026-10-20 still carry both (the +# repository has no forks as of 2026-10-22). Every other historical finding +# is synthetic test material, a content hash, or the ***REMOVED*** marker the +# rewrite left behind, all allowlisted below. [extend] useDefault = true @@ -47,6 +53,9 @@ paths = [ # So do the framework wrappers' suites — same reason, different tree. Without # this a full-history scan reports the wrappers' fake api_key= kwargs. '''^wrappers/[^/]+/tests/''', + # The static site's Node test files mock the demo-key endpoint with + # synthetic keys, for the same reason as tests/ above. + '''^site/test_[^/]+\.(c|m)?js$''', # Demo transcripts and copy-paste docs use placeholder keys. '''^docs/demo-.*\.md$''', # Research manifests map source paths to SHA-256 content hashes. The generic @@ -95,4 +104,9 @@ regexes = [ # The public AWS KMS KeySpec enum value for Ed25519 keys (kms_signer.py); it # names a key type, it is not key material. '''^ECC_NIST_EDWARDS25519$''', + # The marker the 2026-10-20 history rewrite left in place of the retired + # 2026-08-06 key's value (scripts/stress_test_live.py at its 2026-08-04 + # commit). It is not a credential; the key it replaced was rotated dead on + # 2026-08-07. + '''^\*\*\*REMOVED\*\*\*$''', ] diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index da0a5682..5205400a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,6 +6,15 @@ repos: args: [--fix] - id: ruff-format + # Local stand-in for GitHub push protection, which is not available on a + # private repository without the paid Secret Protection add-on. Scans the + # staged diff with the same .gitleaks.toml ruleset and the same gitleaks + # release as CI's secret_scan job, so a local pass predicts the CI verdict. + - repo: https://github.com/gitleaks/gitleaks + rev: v8.21.2 + hooks: + - id: gitleaks + # mypy runs from the project environment (language: system) via `uv run`, # matching how the Makefile invokes tools. The previous mirrors-mypy setup # used an isolated env + the unbuildable `types-all` dep, which broke the diff --git a/FINAL_SUMMARY.md b/FINAL_SUMMARY.md index e11a16d2..6b12da79 100644 --- a/FINAL_SUMMARY.md +++ b/FINAL_SUMMARY.md @@ -187,7 +187,7 @@ Per task requirements: ## Pull Request -**URL:** https://github.com/PetrefiedThunder/agent-middleware-api/pull/331 +**URL:** https://github.com/agentmiddleware/agent-middleware-api/pull/331 **Title:** Verify quote-locked pricing vertical slice (already complete on main) diff --git a/README.md b/README.md index b30e6e99..c2f4b0a6 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ # Agent Middleware API -[![CI](https://github.com/PetrefiedThunder/agent-middleware-api/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/PetrefiedThunder/agent-middleware-api/actions/workflows/ci.yml) +[![CI](https://github.com/agentmiddleware/agent-middleware-api/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/agentmiddleware/agent-middleware-api/actions/workflows/ci.yml) ![Version](https://img.shields.io/badge/version-v1.3.0--unreleased-lightgrey) ![Python](https://img.shields.io/badge/python-3.11%2B-blue) ![License](https://img.shields.io/badge/license-Apache%202.0%20(gateway%20is%20Fair%20Source%2C%20FSL--1.1)-blue) @@ -97,7 +97,7 @@ Per-release detail: [CHANGELOG.md](CHANGELOG.md). Prerequisites: Python 3.11+, [`uv`](https://docs.astral.sh/uv/), and `make`. ```bash -git clone https://github.com/PetrefiedThunder/agent-middleware-api.git +git clone https://github.com/agentmiddleware/agent-middleware-api.git cd agent-middleware-api make demo-ambiguous-retry ``` diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md index 8da2de95..36ee1ada 100644 --- a/TROUBLESHOOTING.md +++ b/TROUBLESHOOTING.md @@ -146,4 +146,4 @@ SQLite-only tests will still pass; PostgreSQL-specific tests skip if unavailable - Check [docs/PROOF_MATRIX.md](docs/PROOF_MATRIX.md) for what each proof command asserts (and what it does not). - Review [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and `make` targets. -If you believe you've found a bug, use the [bug report template](https://github.com/PetrefiedThunder/agent-middleware-api/issues/new?template=bug_report.yml). For security issues, see [SECURITY.md](SECURITY.md). +If you believe you've found a bug, use the [bug report template](https://github.com/agentmiddleware/agent-middleware-api/issues/new?template=bug_report.yml). For security issues, see [SECURITY.md](SECURITY.md). diff --git a/app/db/database.py b/app/db/database.py index 30df07bb..c1c705d0 100644 --- a/app/db/database.py +++ b/app/db/database.py @@ -16,6 +16,7 @@ import logging import os +import sqlite3 from contextlib import asynccontextmanager from datetime import datetime from typing import Any, AsyncGenerator @@ -126,6 +127,59 @@ def _get_database_url() -> str | None: return as_sqlalchemy_url(raw) +class _StatementReleasingCursor(sqlite3.Cursor): + """Release a failed statement on the aiosqlite worker thread that ran it. + + Through CPython 3.11, ``sqlite3.Cursor.execute``/``executemany`` leave the + prepared statement attached to the cursor when the step fails (unique + violation, "database is locked", ...); 3.12 resets and drops it in the + error path of the C-level query executor instead. A cursor that still owns + a statement resets it again when it is destroyed, and that sqlite3_reset() + call takes the connection mutex. The destruction happens wherever the + cyclic GC runs -- in practice the event-loop thread, since the failed + cursor survives in the traceback of the exception the caller handled -- + while aiosqlite executes statements on a per-connection worker thread. If + that worker is inside sqlite3_step() waiting in SQLite's busy handler (it + holds the mutex for the whole wait), the event loop blocks until the busy + timeout expires; the writer being waited for can only commit from the + event loop, so the wait always runs to the full timeout. Under concurrent + governed MCP writes this surfaced as a 30 s stall plus "database is + locked", on the Python 3.11 CI leg only. Closing the cursor here, on the + worker thread that owns the connection, resets and detaches the statement + immediately, so its later destruction never enters SQLite. On 3.12+ the + close is a no-op for the statement and remains harmless. + """ + + def execute(self, sql, parameters=(), /): # noqa: ANN001, ANN201 + try: + return super().execute(sql, parameters) + except BaseException: + self._release_statement() + raise + + def executemany(self, sql, seq_of_parameters, /): # noqa: ANN001, ANN201 + try: + return super().executemany(sql, seq_of_parameters) + except BaseException: + self._release_statement() + raise + + def _release_statement(self) -> None: + try: + self.close() + except Exception: + # The connection is already closed or otherwise unusable, so the + # statement cannot be reset; the original error is what matters. + pass + + +class _StatementReleasingConnection(sqlite3.Connection): + """sqlite3 connection whose cursors are ``_StatementReleasingCursor``.""" + + def cursor(self, factory=_StatementReleasingCursor): # noqa: ANN001, ANN201 + return super().cursor(factory) + + def get_engine() -> AsyncEngine | None: """Get or create the async database engine. @@ -153,7 +207,12 @@ def get_engine() -> AsyncEngine | None: # choking on concurrent writes manifests as OperationalError # "database is locked". connect_args + WAL mode below solve it # for dev/test; production runs on PostgreSQL. - engine_kwargs["connect_args"] = {"timeout": 30} + engine_kwargs["connect_args"] = { + "timeout": 30, + # Releases a failed statement on the worker thread; see + # _StatementReleasingCursor (Python 3.11 event-loop stall). + "factory": _StatementReleasingConnection, + } else: engine_kwargs["pool_size"] = get_settings().DB_POOL_SIZE engine_kwargs["max_overflow"] = get_settings().DB_MAX_OVERFLOW diff --git a/app/main.py b/app/main.py index 4fe666a6..3393374d 100644 --- a/app/main.py +++ b/app/main.py @@ -641,7 +641,7 @@ async def periodic_cleanup(): contact=public_contact or None, license_info={ "name": "Business Source License 1.1", - "url": "https://github.com/PetrefiedThunder/agent-middleware-api/blob/main/LICENSE", + "url": "https://github.com/agentmiddleware/agent-middleware-api/blob/main/LICENSE", }, servers=[ { diff --git a/b2a_sdk/README.md b/b2a_sdk/README.md index f66a4fa1..403c2d48 100644 --- a/b2a_sdk/README.md +++ b/b2a_sdk/README.md @@ -37,9 +37,9 @@ python -m pip install -e './b2a_sdk[mcp]' ### Released artifact The source in this checkout is `0.5.0`; the currently released artifact is -[`python-sdk-v0.4.0`](https://github.com/PetrefiedThunder/agent-middleware-api/releases/tag/python-sdk-v0.4.0). +[`python-sdk-v0.4.0`](https://github.com/agentmiddleware/agent-middleware-api/releases/tag/python-sdk-v0.4.0). Download -[`b2a_sdk-0.4.0-py3-none-any.whl`](https://github.com/PetrefiedThunder/agent-middleware-api/releases/download/python-sdk-v0.4.0/b2a_sdk-0.4.0-py3-none-any.whl) +[`b2a_sdk-0.4.0-py3-none-any.whl`](https://github.com/agentmiddleware/agent-middleware-api/releases/download/python-sdk-v0.4.0/b2a_sdk-0.4.0-py3-none-any.whl) and install that downloaded file: ```bash diff --git a/docs/GAP_CLOSURE_PLAN.md b/docs/GAP_CLOSURE_PLAN.md index ab505a4f..435eeb30 100644 --- a/docs/GAP_CLOSURE_PLAN.md +++ b/docs/GAP_CLOSURE_PLAN.md @@ -59,7 +59,7 @@ class RateLimiter: **Solution:** Audit and patch. **Deliverables:** -- [ ] Review https://github.com/PetrefiedThunder/agent-middleware-api/security/dependabot +- [ ] Review https://github.com/agentmiddleware/agent-middleware-api/security/dependabot - [ ] Update `requirements.txt` for critical + high severity - [ ] Run full test suite post-update - [ ] Document any breaking changes diff --git a/docs/README.md b/docs/README.md index 06fa2f4a..a5190ded 100644 --- a/docs/README.md +++ b/docs/README.md @@ -20,6 +20,7 @@ consequential MCP tool behind a governed boundary. Start with the | Confirm which commit production is actually running | [Deployment verification checklist](deployment-verification-checklist.md) | | Plan a design-partner evaluation | [Design partner guide](../DESIGN_PARTNER_GUIDE.md) | | Contribute to the repository | [Contributing guide](../CONTRIBUTING.md) | +| Configure the GitHub organization, branch protection, and Actions budget | [GitHub organization configuration](github-org-configuration.md) | ## Agent and API discovery diff --git a/docs/agentmarket-listing.md b/docs/agentmarket-listing.md index d547058d..4ed3e0ed 100644 --- a/docs/agentmarket-listing.md +++ b/docs/agentmarket-listing.md @@ -55,7 +55,7 @@ IAM replacement, or broad agent-framework governance. | Product | Replay-safe gateway debit + dispatch evidence + signed receipts | | Mechanism | Permit → governed invoke → wallet charge → receipt → audit | | Pricing | Controlled design-partner pilot; no public tier or SLA committed | -| Repo | https://github.com/PetrefiedThunder/agent-middleware-api | +| Repo | https://github.com/agentmiddleware/agent-middleware-api | --- diff --git a/docs/agentmarket-submission.md b/docs/agentmarket-submission.md index 0a8f0d76..155f287f 100644 --- a/docs/agentmarket-submission.md +++ b/docs/agentmarket-submission.md @@ -75,7 +75,7 @@ pilot. ## Repository -https://github.com/PetrefiedThunder/agent-middleware-api +https://github.com/agentmiddleware/agent-middleware-api ## Demo @@ -100,7 +100,7 @@ agent-governance ## Contact -- GitHub Issues: https://github.com/PetrefiedThunder/agent-middleware-api/issues +- GitHub Issues: https://github.com/agentmiddleware/agent-middleware-api/issues - Public operator email: intentionally omitted until a monitored address and accountable identity pass the launch contact gate. Do not submit this listing before those values are configured. diff --git a/docs/api-key-rotation.md b/docs/api-key-rotation.md index ddc07edf..b62b63ea 100644 --- a/docs/api-key-rotation.md +++ b/docs/api-key-rotation.md @@ -24,10 +24,15 @@ removes it from neither git history nor existing clones. | Date | Key | Exposure | Action | | --- | --- | --- | --- | -| 2026-08-06 | `***REMOVED***` | Hardcoded in `scripts/stress_test_live.py`, reachable on public `main`. Removed from HEAD in #201. **Still reachable from `main`'s own history** — see the correction below | 2026-08-07: `VALID_API_KEYS` fully replaced on Railway and cutover completed via dashboard Redeploy of the last good deployment (variable-triggered rebuilds were crash-looping on the stale `master` trigger — see warning below). Verified with `rotate_api_keys.py verify`: retired key rejected (403), replacement accepted (200) | +| 2026-08-06 (Pacific; committed 2026-08-04, discovered and removed 2026-08-07 05:45 to 06:04 UTC) | `***REMOVED***` | Hardcoded in `scripts/stress_test_live.py`, reachable on `main`, which was public at the time. Removed from HEAD in #201; still reachable from `main`'s history until the 2026-10-20 rewrite (see the two notes below). The Key column shows `***REMOVED***` because the rewrite replaced the value everywhere in the repository, this table included | 2026-08-07: `VALID_API_KEYS` fully replaced on Railway and cutover completed via dashboard Redeploy of the last good deployment (variable-triggered rebuilds were crash-looping on the stale `master` trigger; the note under step 2 of the rotation procedure records that workaround as historical). Verified with `rotate_api_keys.py verify`: retired key rejected (403), replacement accepted (200) | ### Correction (2026-09-19): the key is still reachable from `main` +> Historical. The claims in this section were true on 2026-09-19 and stopped +> being true with the 2026-10-20 rewrite described in the next section. The +> section is kept unchanged as the record, except that the rewrite replaced +> the key's value inside it with `***REMOVED***`. + An earlier version of this record, and the scope note in `.gitleaks.toml`, said `main`'s flattened history no longer contained the retired key and that pruning stale unmerged branches would remove the reachable copies. **Both @@ -68,6 +73,47 @@ public, for signal rather than secrecy: 200 branches, most of them abandoned agent sessions, is not what a reader should find in a project whose credibility is the product. +### Update (2026-10-20): the history was rewritten after all + +On 2026-10-20 (22:19 UTC) the maintainer force-pushed a rewrite of the whole +repository, every branch included, in which every commit SHA changed except +the first three commits of 2026-02-23, which were already identical. It +replaces the retired key's value with the literal `***REMOVED***` wherever it +appeared, including in this table and in the console transcript above. Verified on 2026-10-22 on +a fresh clone: + +- `git cat-file -t 9e45009` reports "Not a valid object name": the commit the + correction cites is no longer reachable from any branch or tag. GitHub + still serves it by its full SHA (the commits API returned + `9e45009ce010f6d9d4449bc94445a6bfdd5ec06b`, original value included, on + 2026-10-22) until it is garbage-collected or GitHub Support purges it; ask + Support for the purge if that matters. +- A full-history gitleaks scan (`gitleaks detect --log-opts=--all` under + `.gitleaks.toml`, every branch, 1,410 commits) reports nothing outside the + allowlist. Under the previous allowlist it reported three findings of two + kinds: the `***REMOVED***` marker itself, in the 2026-08-04 commit of + `scripts/stress_test_live.py`, and (twice) a synthetic + `amw_demo_secret_value` in `site/test_demo_key.mjs` on the unmerged + `devops/demo-key-utm` branch; both kinds are allowlisted as non-secrets. +- `scripts/secret_scan_range.sh` now pins the `main` tip that scan covered + (`9289867`) as its verified-clean base; the previous pin (`dd16051`) is no + longer in the repository's history. + +What this changes in what to claim: the value is no longer reachable from +this repository's branches or tags. It is still served by GitHub by commit +SHA until garbage collection or a Support purge, it is quoted verbatim in the +description of pull request 448, it is still in every clone taken before +2026-10-20 (the repository has no forks as of 2026-10-22), and it was +published in plaintext in this file from 2026-08-07 (06:42 UTC, #202) until +the rewrite, so treat it as public knowledge. The remediation is unchanged: +the key was rotated on 2026-08-07, is dead at the provider, and +`rotate_api_keys.py verify` confirms it is refused. The honest statement is +now: leaked (on `main` from 2026-08-04), rotated within a day of discovery +(2026-08-07), verified dead, removed from the repository's reachable history +on 2026-10-20, and still present in pre-rewrite copies, in GitHub's dangling +pre-rewrite objects until those are purged, and in the text of pull request +448. + ## Rotation procedure (Railway) Before changing keys, qualify the exact-SHA release and a schema-compatible diff --git a/docs/awi-adoption-guide.md b/docs/awi-adoption-guide.md index 3c003b8a..34ecdd5a 100644 --- a/docs/awi-adoption-guide.md +++ b/docs/awi-adoption-guide.md @@ -35,7 +35,7 @@ To use the Python client today, work from a checkout and put the SDK on your path: ```bash -git clone https://github.com/PetrefiedThunder/agent-middleware-api.git +git clone https://github.com/agentmiddleware/agent-middleware-api.git cd agent-middleware-api python -m pip install -r requirements.txt export PYTHONPATH="$PWD/awi_sdk/python:$PYTHONPATH" @@ -218,6 +218,6 @@ adapter = AWIFallbackAdapter( ## Support -- GitHub Issues: https://github.com/PetrefiedThunder/agent-middleware-api/issues -- Documentation: https://github.com/PetrefiedThunder/agent-middleware-api#readme +- GitHub Issues: https://github.com/agentmiddleware/agent-middleware-api/issues +- Documentation: https://github.com/agentmiddleware/agent-middleware-api#readme - arXiv Paper: https://arxiv.org/abs/2506.10953 diff --git a/docs/constant-test-loop.md b/docs/constant-test-loop.md index cda1be21..175120d4 100644 --- a/docs/constant-test-loop.md +++ b/docs/constant-test-loop.md @@ -124,7 +124,7 @@ BOOTSTRAP_KEY="..." python scripts/partner_api_key_bootstrap.py \ --key-name constant-test-loop \ --budget-credits 5000 \ --json | jq -r .api_key | \ - gh secret set CI_SMOKE_AGENT_KEY --repo PetrefiedThunder/agent-middleware-api + gh secret set CI_SMOKE_AGENT_KEY --repo agentmiddleware/agent-middleware-api ``` ### Security Properties diff --git a/docs/deploy-railway.md b/docs/deploy-railway.md index 08e53d79..b011ac3e 100644 --- a/docs/deploy-railway.md +++ b/docs/deploy-railway.md @@ -586,7 +586,7 @@ git merge-base --is-ancestor "$DEPLOY_SHA" origin/main # Exact-SHA CI is a hard gate. Do not substitute a green PR-head run. ci_conclusion="$(gh api \ - "repos/PetrefiedThunder/agent-middleware-api/actions/workflows/ci.yml/runs?head_sha=$DEPLOY_SHA&status=completed" \ + "repos/agentmiddleware/agent-middleware-api/actions/workflows/ci.yml/runs?head_sha=$DEPLOY_SHA&status=completed" \ --jq '[.workflow_runs[] | select(.event == "push" and .conclusion != null)][0].conclusion // "none"')" test "$ci_conclusion" = "success" diff --git a/docs/github-org-configuration.md b/docs/github-org-configuration.md new file mode 100644 index 00000000..b1b276d6 --- /dev/null +++ b/docs/github-org-configuration.md @@ -0,0 +1,417 @@ +# GitHub organization configuration + +How the `agentmiddleware` GitHub organization and its repositories are meant to +be configured, what was actually configured when this was audited, and the +exact clicks or commands that close each gap. Written for one maintainer with +admin on everything and no second reviewer. + +Audit date: 2026-10-19, updated 2026-10-20 after the repositories were made +public and 2026-10-22 after this repository was unarchived. Re-run the two +scripts below to refresh the repository rows; the organization rows can only +be read in the GitHub UI. + +State on 2026-10-22: the 2026-10-20 update of this document recorded this +repository as archived. It was writable again by 2026-10-21 19:15 UTC, when +pull request 842 was opened, and `main` merged ten pull requests between +2026-10-21 22:08 and 2026-10-22 07:04 UTC (842 to 851: site, README and test +work, plus a one-line repository-URL change in `app/routers/well_known.py` +in 842 and a CI step for the site's JavaScript tests in 845). Pull request +850 (merged 06:02 UTC) added the README banner saying the repository is +archived and that development continues in `agent-middleware`; its +description says the repository "stays unarchived for now because site PRs +are still open". After the last merge it was read-only again until about +21:35 UTC on 2026-10-22, when the repository object was last updated and +pushes started succeeding; GitHub records no archive times. +The organization also gained a `.github` repository on 2026-10-20 (profile +README, `CODE_OF_CONDUCT.md`, `CONTRIBUTING.md`, `SECURITY.md`, and a weekly +Dependabot entry for the `github-actions` ecosystem, though it has no +workflows yet) and an organization ruleset, `main-protect`, that requires a +pull request on the default branch (see the audit table). Everything below +treats this repository as live until the maintainer archives it for good. + +## Do these first, in this order + +1. **Done 2026-10-20: six of the seven repositories are public** + (`agent-middleware-public` is still private). That removes the Actions + minutes bill on standard runners and switched on secret scanning, push + protection and private vulnerability reporting by itself. The first + follow-up is settled: every `main` push since 2026-10-20 09:23 UTC (17 + through `9289867` on 2026-10-22) started a named workflow run, and the + no-name `startup_failure` in [Actions budget](#actions-budget) last fired + at 2026-10-19 21:28 UTC, so billing was the cause. The second remains: + this repository's description is fixed; re-read the others (see + [Repositories](#repositories)). +2. **Apply the repository settings** from a machine where `gh auth status` + shows your account: + + ```bash + python scripts/github_repo_settings.py # dry run, shows drift + python scripts/github_repo_settings.py --apply + ``` + + "Delete branch on merge" was switched on by hand by 2026-10-22, and a + pull request (0 approvals) is already required on `main` by the + organization ruleset `main-protect`, created 2026-10-20 22:09 UTC. What + the script still reports as drift is the same requirement on the classic + branch-protection rule, which also carries the required checks; applying + it is belt and braces, not a new control. Secret scanning is already on. +3. **Clean up the remaining branches** now that "delete branch on merge" is + on, so they stop accumulating: + + ```bash + python scripts/github_branch_cleanup.py # lists what is fully merged + python scripts/github_branch_cleanup.py --apply # deletes only those + python scripts/github_branch_cleanup.py --stale-days 90 # review the rest by hand + ``` + + Dry run on 2026-10-19: 550 branches, 103 fully merged into `main`, 26 + unmerged with no commit in 90 days, 45 `backup/local-2026-10-07/*` kept + by prefix. Dry run on 2026-10-20, after about 240 branches had been + deleted by hand in between: 310 branches (284 by 2026-10-22); 4 fully + merged (the `--apply` set); 3 unmerged with no commit in 90 days + (`codex/regengine-governed-mcp-bridge`, `feat/trust-primitives`, + `codex/trust-readiness-gap-map`); 45 `backup/` kept; the remaining 256 + (the script also skips the protected `master`) are unmerged branches with + a commit in the last 90 days, nearly all agent-generated. Those need a decision, not a script: either open the + pull request or delete the branch. + +4. **Work through the [organization settings](#organization-settings) + table.** None of it can be scripted from a repository-scoped token. + +## Audit snapshot + +### Organization + +| Item | Found | Target | +|---|---|---| +| Members | 1 (`PetrefiedThunder`, owner) | Same, until there is a second maintainer | +| Teams | none | A `maintainers` team as soon as a second person joins, so `CODEOWNERS` can name a team instead of a person | +| Repositories | 7 on 2026-10-22: six public (`agent-middleware-api`, `agent-middleware`, `agent-middleware-spec`, `amw-demo`, `amw-paypal-agent-demo`, `.github`) and `agent-middleware-public` still private. This repository's description was fixed; the others were not re-read on 2026-10-22 | `agent-middleware` is where development continues, per the README banner added here by #850; see [Repositories](#repositories) | +| Actions runs on `main` pushes | `startup_failure`, no workflow name, on every `main` push from 2026-10-17 to 2026-10-19, while the repository was private. Since it went public every push run has started: the merges of 2026-10-21 and 2026-10-22 ran green (for example run 39733461851 for `b6f006a`) except two runs that were cancelled, not failed: `7408143` (#847, run 39719715065, where only `test (3.11)` was cancelled 25 minutes in and the other 18 jobs passed) and `9289867` (#848, run 39737949702); both commits' Docker publish runs succeeded | Green push run on every `main` commit | +| Two-factor requirement, base permission, Actions policies, security configuration | Not readable from here | See [Organization settings](#organization-settings) | + +### `agent-middleware-api` + +| Setting | Found | Target | How | +|---|---|---|---| +| `main` required status checks | 11 contexts, exactly the list in [trust-release-gate-branch-protection.md](trust-release-gate-branch-protection.md); strict (up to date) on | Keep | Already correct | +| `main` admins exempt | No (`enforce_admins` on) | Keep | Already correct | +| `main` force push / deletion | Both blocked | Keep | Already correct | +| `main` conversation resolution | Required | Keep | Already correct | +| `main` requires a pull request | **Off** on the classic rule (still, on 2026-10-22); **On** through the organization ruleset `main-protect` since 2026-10-20 22:09 UTC, 0 approvals | On | Effective already; `github_repo_settings.py --apply` mirrors it onto the classic rule | +| Delete branch on merge | **Off** on 2026-10-19 (why 550 branches had accumulated); **On** by 2026-10-22 | On | Done by hand | +| Auto-merge, update branch | On | Keep | Already correct | +| Web commit sign-off (DCO) | On | Keep | Already correct | +| Secret scanning, push protection | Disabled while private; **enabled** (with validity checks and non-provider patterns) the moment the repository went public on 2026-10-20 | Keep | Already correct; see [Secret scanning](#secret-scanning) | +| Private vulnerability reporting | Enabled | Keep; it is what `SECURITY.md` asks reporters to use | Already correct | +| Dependabot security updates | Enabled | Keep | Already correct | +| Dependabot version updates | One pull request per package, pip and Actions only | Grouped pull requests: weekly for pip (root, SDK and wrappers) and Actions, monthly for the Docker digest and Railway IaC | Done in `.github/dependabot.yml` on this branch | +| `test (3.11)` stalls | `main`'s run for `7408143` (#847) and this branch's run for `fd1c42c` each had `test (3.11)` alone cancelled by the 25-minute job timeout after the suite stopped producing output mid-run. Through CPython 3.11, a sqlite3 cursor whose statement failed keeps that statement attached; destroying the cursor on the event-loop thread while an aiosqlite worker holds the connection mutex in a busy wait freezes the loop | No stall | Fixed on this branch: `app/db/database.py` passes a connection factory whose cursors release a failed statement on the worker thread (a no-op on 3.12+); `tests/test_sqlite_failed_cursor_release.py` reproduces the mechanism | +| Rulesets | None on 2026-10-19. Since 2026-10-20 22:09 UTC the organization ruleset `main-protect` (id 25509458, active, no bypass actors) applies to the default branch: pull request required with 0 approvals, no deletion, no force push. Whether it targets the other repositories too is visible only in Organization → Settings → Rulesets | Keep | – | +| `master` branch | Exists, protected, 9 commits ahead and 1,239 behind `main` on 2026-10-19, last commit 2026-05-22; still present on 2026-10-22 | Deleted | See [Remove master](#remove-master) | +| Security advisory link in issue chooser | Pointed at the old `PetrefiedThunder/` namespace | Organization namespace | Done in `.github/ISSUE_TEMPLATE/config.yml` on this branch | + +Why 0 approvals: GitHub does not let an author approve their own pull request. +With one maintainer, requiring 1 approval blocks every merge. Requiring a pull +request with 0 approvals still forces every change through the 11 required +checks, which is the control this repository relies on +([GOVERNANCE.md](../GOVERNANCE.md), "Merge requirements"). Raise the count to 1 +and turn on code-owner review the day a second maintainer has commit access. + +## Actions budget + +### What happened + +- Open issue 805 (2026-10-14) reports `startup_failure` on this repository and + attributes it to an organization billing or policy setting. +- Pull request 816 was opened on 2026-10-15 from a branch named + `ci/retrigger-main-1014` to retrigger checks after the lock lifted, and its + title, "ci: refresh requirements.lock and fix PT011 after billing lock" + (merged 2026-10-17 20:35 UTC), names the lock. So the lock has happened at + least twice this month. +- The workflow files did not change between the last green push run before + the lock (`44cd144`, 2026-10-16) and any of the heads that failed to start + (through `0c3300a` on 2026-10-19), which rules out a broken workflow file. +- The block comes and goes in windows. The same no-name `startup_failure` + run (GitHub's placeholder when it refuses to start any workflow for an + event) fired 139 times since 2026-09-15: 27 times between 2026-09-15 22:01 + and 2026-09-16 09:30, 7 times on 2026-10-13, 90 times on 2026-10-14 + (04:45, 09:29 and 18:30 to 21:26), 4 times on 2026-10-17 20:19 to 20:35, 6 + times on 2026-10-19 11:23 to 11:28 and 6 times on 2026-10-19 21:26 to + 21:28 UTC, on push and pull-request events alike, including the push of + every `main` commit from 2026-10-17 through 2026-10-19 (`7190d17`, + `50df44a`, `bda6197`, `0c3300a`). Between those windows (for example this + pull request's runs at 07:33 UTC on 2026-10-19 and from 08:52 UTC on + 2026-10-20) everything starts normally. The API does not expose the + reason; the run page does, as a banner above the empty job list. Open the + latest `main` push one and read it: + . + A spending-limit message confirms the billing explanation; anything else + changes the fix. +- Until that is settled, every merge into `main` that lands inside a window + has no push run, and `railway-deploy.yml` will refuse to release it. Check + the Actions tab after each merge; if the push run is the no-name failure, + the commit cannot be released until CI has a manual trigger (`ci.yml` has + no `workflow_dispatch`) or the release gate accepts a re-run. + +### Why it kept happening (while the repository was private) + +Private repositories pay for Actions minutes. This repository's CI is heavy by +design (every push runs the full release gate), and every Dependabot pull +request runs it too. The numbers stay here because they are also the +wall-clock cost of every pull request. + +| Measured on run 2179 (2026-10-16, `main`) | Minutes | +|---|---| +| `test (3.11)`, `test (3.12)`, `test (3.13)` | 12.6 + 17.3 + 10.3 | +| `trust_release_gate`, `postgres_permit_concurrency` | 3.7 + 3.8 | +| `postgres_trust` | 1.8 | +| `lint`, `invariant_attack_proof`, `production_trust`, `secret_scan`, `constant_test_loop` | 0.5 to 1 each | +| `python_sdk` x4, `wrapper_unit` x4 | about 0.5 each | +| Job time per CI run | about 60 | +| Billed per run (each job rounds up to a whole minute) | about 65 | +| Plus `Failure Lab` (fast tier on pull requests, fast and slow tiers on `main`) and `docker-publish` | 3 to 10 | + +Between 2026-10-15 and 2026-10-17 there were 80 CI runs (69 ran to +completion, 11 were superseded and cancelled), about 5,000 billed minutes in +three days at the per-job ceilings. The Free plan includes 2,000 minutes a +month for private repositories; Team includes 3,000. Overage on standard +Linux runners is about $0.008 per minute, so a month at the recent cadence +(709 CI runs in the 30 days to 2026-10-19) is roughly 40,000 to 50,000 +minutes, or $300 to $400 of overage. + +### The decision + +**Taken on 2026-10-20: the repository is public.** Public repositories get +unlimited Actions minutes on standard runners, free secret scanning and push +protection, and private vulnerability reporting, and GitHub switched all of +those on by itself at the flip. The repository already carried a +`SECURITY.md` and tests asserting that public surfaces link to +`github.com/agentmiddleware/agent-middleware-api`. Later the same day the +maintainer rewrote the entire history (every commit SHA changed except the +first three commits of 2026-02-23, already identical) to replace +the value of the key removed on 2026-08-06 (#201) and rotated on 2026-08-07 +with `***REMOVED***`, replaced `LICENSE` (Business Source License 1.1) with +the Apache License 2.0 in the one new commit on top, `861d46c`, and, per the +2026-10-20 update of this document, archived the repository. It was writable +again by 2026-10-21 19:15 UTC, read-only again after the last merge on +2026-10-22, and writable from about 21:35 UTC that day (pull request 850 +says the archive waits for the open site pull requests). The fallout is +listed under [After the history rewrite](#after-the-history-rewrite). + +The two options not taken, kept for the record: + +- A spending limit on a private repository (Organization Settings → Billing + and licensing → Budgets and alerts). A $0 limit, the default, is what + produces the lock. +- Reducing the bill per push: the non-required legs `test (3.13)`, + `python_sdk (3.13)` and the four `wrapper_unit` jobs are about 15 minutes of + each run and could be gated on `github.event_name == 'push'`. No longer a + cost question; still a wall-clock one if pull-request feedback feels slow. + +`main` had green push runs again on 2026-10-20 from 09:23 UTC (five of six; +the 11:05 run failed on `test (3.11)`, a real test failure), then the +force-push of the rewritten history at 22:19 UTC produced a run that is red +on `secret_scan` (see below), with the six long jobs cancelled and the other +twelve green. The merges of +2026-10-21 and 2026-10-22 ran green again (a normal merge carries a known +`before` SHA, so its `secret_scan` range is well defined); two runs on +2026-10-22 were cancelled rather than failed: `7408143`, where `test (3.11)` +alone was cancelled 25 minutes after it started, and `9289867`. `ci.yml` +still has no manual trigger, so a cancelled push run is replaced either by +re-running it from the Actions page (the release gate reads the run's current +conclusion) or by the next merge; a `startup_failure` run has no jobs to +re-run. + +## After the history rewrite + +The 2026-10-20 22:19 UTC force-push replaced every commit on `main` (1,403 +of them), added one new commit, `861d46c`, on top, and, as it turned out, +rewrote every other branch too. These were the +consequences; the state on 2026-10-22 is given for each. + +- **`secret_scan` failed on `main`'s rewrite push, and its fallback base was + stale. Fixed on this branch.** The rewrite push carried the old tip + `2d62e2b` as its `before` SHA and GitHub still served that commit, so + `scripts/secret_scan_range.sh` emitted `2d62e2b..861d46c`, a range with no + common ancestor that covered the whole rewritten history (984 commits); + gitleaks flagged the `***REMOVED***` marker the rewrite had left in the + 2026-08-04 commit of `scripts/stress_test_live.py`, and the "Scan commit + range for committed secrets" step failed on run 39507256676. Separately, + the script's pinned verified-clean base for pushes with no usable `before` + SHA, `dd16051` (#811), stopped existing, so that fallback would have failed + closed and `test_pinned_verified_base_is_on_this_history` silently skipped + (it treats a missing pin as a shallow checkout). Normal pushes were + unaffected: their `before` SHA is an ancestor of the new tip, so the range + covers only the new commits. On 2026-10-22 a full-history scan of a fresh + clone (gitleaks 8.21.2, the release CI pins, `--log-opts=--all`, 283 + branches at the time, 1,410 commits scanned) found three findings in two + places, both non-secrets and now allowlisted in `.gitleaks.toml`: the + `***REMOVED***` marker above, and a synthetic `amw_demo_secret_value` at + commit `bd812bab` in the history of the unmerged `devops/demo-key-utm` + branch (its tip has already replaced the value). `site/hero-receipt-fixes`, + pushed at 21:51 UTC after the clone, adds one clean commit. `VERIFIED_BASE` + is re-pinned to `9289867`, the `main` tip that scan covered, and the test + runs again. +- **Two documents described the old history. Fixed on this branch.** The + scope note in `.gitleaks.toml` and the "Correction (2026-09-19)" section of + [api-key-rotation.md](api-key-rotation.md) said the retired key was + reachable from `main` through commit `9e45009`. Neither is reachable from a + branch or tag any more (GitHub still serves the old commit by SHA until it + is garbage-collected or purged). The scope note now records the 2026-10-22 + scan, the + rotation record keeps the 2026-09-19 section as history and adds a dated + update, and the comment in `ci.yml`'s `secret_scan` job no longer says a + full-history scan would always fail. +- **The license documents disagree with `LICENSE`. Still open.** `LICENSE` + is Apache License 2.0 since `861d46c`; #851 fixed the README badge and the + #850 banner says "Apache 2.0 (gateway is Fair Source, FSL-1.1)". But + `LICENSING.md` (the directory table and "What the Business Source License + allows"), `CONTRIBUTING.md` ("Licensing of contributions"), `GOVERNANCE.md` + (two places) and the README's own licensing paragraph still say Business + Source License 1.1 for the core, and "gateway is Fair Source" describes the + `gateway/` directory of `agent-middleware`, which this repository does not + have. Decide what `app/` is licensed under and say it in all four files in + one commit; it decides whether a competitor may host the service. +- **Clones are on dead history; branches are not.** Every one of the 284 + branches on GitHub shares history with the rewritten `main` (checked + 2026-10-22), so pull requests compare normally again; this one showed + 94,797 changed lines only until its branch was rewritten too. Local clones + and forks taken before 2026-10-20 do not share history and must be + re-cloned. `github_branch_cleanup.py` was never at risk: a branch with no + merge base compares as "ahead", so `--apply` cannot delete it. + +## Secret scanning + +GitHub's secret scanning, push protection, validity checks and non-provider +patterns have been on since the repository went public on 2026-10-20 (they +were off while it was private, where they need the paid Secret Protection +add-on). `scripts/github_repo_settings.py` reports secret scanning and push +protection and would re-enable those two if they were ever switched off; +validity checks and non-provider patterns stay set by hand under the +repository's Code security settings. + +Three layers now cover the same risk, and all three stay: + +- GitHub push protection rejects a push that contains a known provider + secret pattern before it lands. +- `.pre-commit-config.yaml` runs gitleaks on the staged diff with the same + `.gitleaks.toml` and the same gitleaks release as CI, which also catches the + repository's own low-entropy patterns that GitHub does not know. Install + once with `pre-commit install`. +- The CI `secret_scan` job scans the working tree and the pushed commit range + and is a required check on `main`. + +Full-history scans are run by hand and recorded in the scope note of +`.gitleaks.toml`; the latest, on 2026-10-22, covered every branch of the +rewritten history and is what `scripts/secret_scan_range.sh` pins its +fallback base to. + +Alerts from GitHub's scanner appear under the repository's Security tab. The +2026-08-06 retired key was scrubbed from history on 2026-10-20, so it should +no longer be flagged; if an alert for it was raised before the rewrite, close +it as "revoked" with a link to [api-key-rotation.md](api-key-rotation.md). + +## Remove master + +`master` is a protected leftover from before the default branch was `main`, +still present on 2026-10-22. On 2026-10-19 it was 1,239 commits behind +`main` (1,258 on 2026-10-22) and carried 9 commits that never reached +`main`; the first command below lists those commits, and +`gh api repos/agentmiddleware/agent-middleware-api/compare/main...master --jq '{ahead_by, behind_by}'` +gives the current counts. +Protected branches cannot be deleted through the branch list, so: + +```bash +gh api repos/agentmiddleware/agent-middleware-api/compare/main...master \ + --jq '.commits[] | "\(.sha[0:8]) \(.commit.message | split("\n")[0])"' # see the 9 commits +gh api -X DELETE repos/agentmiddleware/agent-middleware-api/branches/master/protection +gh api -X DELETE repos/agentmiddleware/agent-middleware-api/git/refs/heads/master +``` + +`failure-lab.yml` still lists `master` in its triggers; remove it in the same +change that deletes the branch so the workflow does not reference a branch +that no longer exists. + +## Organization settings + +Everything here is Organization → Settings. The organization-level pages +are not readable or writable through the repository-scoped token this +document was produced with, so it is a checklist, not a script; the +`main-protect` ruleset's rules are the one item visible from the repository +side. + +| Page | Setting | Target | Why | +|---|---|---|---| +| Authentication security | Require two-factor authentication for everyone | On | One compromised password is the whole organization | +| Member privileges | Base permissions | Read | New members should not get write everywhere by default | +| Member privileges | Repository creation | Owners only (uncheck public and private for members) | Keeps the repository list deliberate | +| Member privileges | Repository forking | Off for private repositories | Only `agent-middleware-public` is still private; public repositories can always be forked | +| Actions → General | Policies | "Allow agentmiddleware, and select non-agentmiddleware, actions and reusable workflows", with "Allow actions created by GitHub" and "Allow actions by Marketplace verified creators" | Every workflow already pins actions to a commit SHA and the nine actions in use are all `actions/*` or `docker/*`, so nothing in use would be blocked; this stops a future workflow from pulling an action from an unvetted third party (pinning itself is a review rule, not something this policy enforces) | +| Actions → General | Workflow permissions | "Read repository contents and packages permissions"; uncheck "Allow GitHub Actions to create and approve pull requests" | Workflows that need more declare it in their `permissions:` block, as they already do | +| Actions → General | Fork pull request workflows | "Require approval for all external contributors" | Now live: every public repository accepts fork pull requests, and under GitHub's default (approval only for first-time contributors) a fork's workflow run from anyone with a previously merged pull request would start on its own | +| Code security → Configurations | Apply "GitHub recommended" to all repositories, and set it as the default for new repositories | Dependabot alerts, security updates, secret scanning and push protection | One place instead of seven repository pages; also covers the one repository still private | +| Code security → Global settings | Private vulnerability reporting for new public repositories | On | `SECURITY.md` tells reporters to open a private advisory; that only works for non-collaborators when this is on. Already on for `agent-middleware-api` | +| Billing and licensing | Actions budget and alert | A small budget with an alert, no hard stop | No longer the block for standard runners on public repositories; still what would bite if a private repository or larger runner is used later | +| Repositories → Rulesets | `main-protect` (exists since 2026-10-20) | Keep. If it is meant to cover every repository, say so in its repository targets; the required status checks can move there from the classic rule later | One rule for every default branch beats a classic rule per repository | +| Profile | Verified domain | Add `agentmiddleware.dev` (the repository homepage and product site; `api.thisisatest.tech` is only the API host) | Shows the "verified" badge next to the organization | + +## Repositories + +Run `python scripts/github_repo_settings.py --repo agentmiddleware/` on +each. For a repository without a branch rule, pass `--required-check` once +per check its CI reports, or leave it empty to create the rule with no +required status checks (a pull request with 0 approvals, conversation +resolution, admins included, no force push or deletion). + +| Repository | Visibility (2026-10-22) | Notes | +|---|---|---| +| `agent-middleware-api` | Public | This document. Recorded archived on 2026-10-20, writable again by 2026-10-21, read-only again after the last merge on 2026-10-22 and writable from about 21:35 UTC that day; the #850 banner announces the archive the maintainer intends once the open site pull requests land (see the top of this document). Description fixed | +| `agent-middleware` | Public | Where development continues, per this repository's README banner. 45 open issues on 2026-10-20 | +| `.github` | Public | Organization profile README, `CODE_OF_CONDUCT.md`, `CONTRIBUTING.md`, `SECURITY.md`, and a `dependabot.yml` with a weekly `github-actions` entry (no workflows yet, so nothing to pin) | +| `agent-middleware-public` | **Private** | Still the only private one, and the one whose name says public. Description said "private staging repo" on 2026-10-20. Flip it, rename it or archive it | +| `agent-middleware-spec` | Public | Its README gained an archive banner pointing at `agent-middleware` on 2026-10-22 06:11 UTC (its pull request 4), so it was writable until then; whether it is archived now is not readable from here. `agent-middleware/spec` carries a diverged copy of its `spec/` directory (of the seven shared files only `test-vectors/keys.json` is identical) | +| `amw-demo` | Public | Description read "private until approved" on 2026-10-20 and has not been re-read from here. Security-tab check once | +| `amw-paypal-agent-demo` | Public | Same Security-tab check | + +## Stale references still in the tree + +Links to the old personal namespace are fixed on this branch: +`.github/ISSUE_TEMPLATE/config.yml` (security-advisory link), `README.md` +(CI badge and clone URL), `TROUBLESHOOTING.md` (bug-report link), +`FINAL_SUMMARY.md` (a pull-request URL; a quoted task prompt on its line 21 +still spells the old slug), `app/main.py` (the OpenAPI license URL, with +`docs/openapi.json` regenerated), `b2a_sdk/README.md`, the four framework +READMEs and the three wrapper READMEs that carried the old clone URL (the +CrewAI wrapper's did not), and `docs/GAP_CLOSURE_PLAN.md`, +`docs/agentmarket-listing.md`, `docs/agentmarket-submission.md`, +`docs/awi-adoption-guide.md`, `docs/constant-test-loop.md` and +`docs/deploy-railway.md`. That is 24 `github.com/PetrefiedThunder` URLs in +18 files, plus bare slugs in the last two. GitHub redirects the old URLs, so +nothing was broken; they were wrong in the way a security questionnaire +notices. 661 old-namespace URLs in 49 files remain at this branch's head, +nearly all inside dated records kept as written (`docs/qa/`, +`docs/issue-resolution-2026-10-02/`), plus one each in `LICENSING.md` (part +of the license-document drift above), `tests/test_site_agent_interface.py` +and `server.json`. One is deliberately left: + +- `server.json` names the MCP Registry entry `io.github.PetrefiedThunder/...` + and gives `https://github.com/PetrefiedThunder/agent-middleware-api` as its + `repository.url`. That is the registry identity the publisher authenticated + as; changing it is a registry re-submission, not a link fix. + +## What this document does not do + +- It never deletes anything by itself. Both scripts are dry runs unless + `--apply` is passed, and the branch cleanup only ever deletes branches with + zero commits that are not already on `main`. +- It does not change the required-check list. That list is owned by + [trust-release-gate-branch-protection.md](trust-release-gate-branch-protection.md) + and the script copies whatever is live. +- It changes `ci.yml` only in a comment (the note above the `secret_scan` + job's "Scan commit range" step); no job, step or trigger changes. The + per-push cost reduction above is a separate, reviewable change. +- The one runtime change on this branch is the SQLite connection factory in + `app/db/database.py` (see the `test (3.11)` row of the audit table). It + applies to SQLite URLs only, which this project uses for local and test + databases; PostgreSQL is untouched. diff --git a/docs/openapi.json b/docs/openapi.json index 1c52268a..8556ce0a 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -5,7 +5,7 @@ "description": "## Agent Transaction Integrity\n\n**Make consequential agent actions transactional.**\n\nTransaction-integrity boundary for consequential autonomous actions on the configured upstream-MCP path: one logical action binds scoped authority and configured consumption to at most one gateway dispatch and debit, records delivery_uncertain instead of automatically redispatching, and links gateway evidence for required external reconciliation.\n\nIt is not a full agent middleware platform. This is a durable gateway state machine, not one distributed ACID transaction and not proof of the downstream effect. A remote side effect is exactly once only when the upstream honors the forwarded idempotency key. See `/WEDGE.md` and `/SECURITY_LIMITATIONS.md`.\n\n### Canonical Loop\n\n`logical action -> authorize -> reserve allowance -> debit -> claim dispatch -> confirmed outcome | delivery_uncertain -> receipt/audit -> authoritative external reconciliation required`\n\n### Product Surface\n\n- **Logical action identity** — accepted key bound to one payload\n- **Bounded authority consumption** — scoped permit and configured allowance\n- **One-shot gateway dispatch/debit** — at most once per accepted action\n- **Explicit uncertainty** — `delivery_uncertain` is not auto-redispatched\n- **Linked gateway evidence** — receipt and audit data for reconciliation\n- **Discovery** — `.well-known/agent.json`, `/mcp/tools.json`, `/llms.txt`, `/v1/discover`\n\n### Proof Surfaces\n\nAgentic-web, browser, content, oracle, sandbox, media, IoT, red-team, and telemetry demos are labeled scaffolding. They mount only when `ENABLE_PROOF_SURFACES=true` and do not define the product.\n\n### Authentication\n\nProtected endpoints require an API key via the `X-API-Key` header.\n\n### For Agents\n\nStart at `/.well-known/agent.json`, then `/llms.txt` and `/mcp/tools.json`. Prefer `PUBLIC_URL` over any localhost server entry.", "license": { "name": "Business Source License 1.1", - "url": "https://github.com/PetrefiedThunder/agent-middleware-api/blob/main/LICENSE" + "url": "https://github.com/agentmiddleware/agent-middleware-api/blob/main/LICENSE" }, "version": "1.3.0" }, diff --git a/docs/trust-release-gate-branch-protection.md b/docs/trust-release-gate-branch-protection.md index 5c29f440..a1abdf0d 100644 --- a/docs/trust-release-gate-branch-protection.md +++ b/docs/trust-release-gate-branch-protection.md @@ -51,9 +51,14 @@ the one that fails loudly and specifically when a claim regresses. Branch protection rule, `main`: - **Require a pull request before merging** — on. - - Require approvals: 1 (raise as the team grows). + - Require approvals: 0 while the project has one maintainer. GitHub does + not let an author approve their own pull request, so 1 would block every + merge; the required checks below are the review. Raise to 1 the day a + second maintainer has commit access. - Dismiss stale approvals when new commits are pushed — on. - - Require review from Code Owners — on (`CODEOWNERS` is present). + - Require review from Code Owners — off until there is a second maintainer. + `CODEOWNERS` assigns every path to the single maintainer, so it would + also block self-merge. Turn it on together with the approval count. - **Require status checks to pass before merging** — on. - Require branches to be up to date before merging — on. (Forces the gate to run against the actual merge result, not a stale base.) @@ -66,8 +71,11 @@ Branch protection rule, `main`: ### Via the REST API -Set once, from an account with admin on the repo (requires a token with -`repo` / branch-protection admin scope): +`python scripts/github_repo_settings.py --apply` sends the body below, with +the `contexts` list read from the live rule so a hand-edited copy cannot drift +from what CI reports. It is a dry run without `--apply`. The manual form, from +an account with admin on the repo (requires a token with `repo` / +branch-protection admin scope): ```bash # OWNER/REPO and the required check contexts are the only things to edit. @@ -89,8 +97,8 @@ curl -sS -X PUT \ "enforce_admins": true, "required_pull_request_reviews": { "dismiss_stale_reviews": true, - "require_code_owner_reviews": true, - "required_approving_review_count": 1 + "require_code_owner_reviews": false, + "required_approving_review_count": 0 }, "required_conversation_resolution": true, "restrictions": null, diff --git a/framework_integrations/README.autogen.md b/framework_integrations/README.autogen.md index bc8d08ae..636b746f 100644 --- a/framework_integrations/README.autogen.md +++ b/framework_integrations/README.autogen.md @@ -7,7 +7,7 @@ Use Agent Middleware API tools with Microsoft AutoGen agents. No PyPI package is published. Work from a checkout of this repository: ```bash -git clone https://github.com/PetrefiedThunder/agent-middleware-api.git +git clone https://github.com/agentmiddleware/agent-middleware-api.git cd agent-middleware-api python -m pip install -r requirements.txt python -m pip install autogen diff --git a/framework_integrations/README.crewai.md b/framework_integrations/README.crewai.md index ae5a8921..1376066b 100644 --- a/framework_integrations/README.crewai.md +++ b/framework_integrations/README.crewai.md @@ -24,7 +24,7 @@ Use the governed CrewAI wrapper instead. Every call goes through the No PyPI package is published. Work from a checkout of this repository: ```bash -git clone https://github.com/PetrefiedThunder/agent-middleware-api.git +git clone https://github.com/agentmiddleware/agent-middleware-api.git cd agent-middleware-api python -m pip install -e ./b2a_sdk python -m pip install -e wrappers/crewai-agent-middleware diff --git a/framework_integrations/README.langgraph.md b/framework_integrations/README.langgraph.md index 94bf3f8d..97d244a0 100644 --- a/framework_integrations/README.langgraph.md +++ b/framework_integrations/README.langgraph.md @@ -7,7 +7,7 @@ Use Agent Middleware API tools directly in LangGraph agents. No PyPI package is published. Work from a checkout of this repository: ```bash -git clone https://github.com/PetrefiedThunder/agent-middleware-api.git +git clone https://github.com/agentmiddleware/agent-middleware-api.git cd agent-middleware-api python -m pip install -r requirements.txt python -m pip install langgraph langchain-core diff --git a/framework_integrations/README.llamaindex.md b/framework_integrations/README.llamaindex.md index 987d0265..ee0c2e75 100644 --- a/framework_integrations/README.llamaindex.md +++ b/framework_integrations/README.llamaindex.md @@ -7,7 +7,7 @@ Use Agent Middleware API tools with LlamaIndex agents. No PyPI package is published. Work from a checkout of this repository: ```bash -git clone https://github.com/PetrefiedThunder/agent-middleware-api.git +git clone https://github.com/agentmiddleware/agent-middleware-api.git cd agent-middleware-api python -m pip install -r requirements.txt python -m pip install llama-index diff --git a/scripts/github_branch_cleanup.py b/scripts/github_branch_cleanup.py new file mode 100644 index 00000000..f2b3b1a8 --- /dev/null +++ b/scripts/github_branch_cleanup.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +"""List, and optionally delete, remote branches already merged into main. + +Dry run by default: nothing is deleted unless --apply is given, and even then +only branches whose every commit is already reachable from the base branch +(GitHub reports them as 0 commits ahead). Protected branches, the base branch, +and anything under a --keep-prefix are never touched. Unmerged branches are +never deleted by this script; with --stale-days they are listed separately +so you can review them by hand. + +Usage: + python scripts/github_branch_cleanup.py # list merged branches + python scripts/github_branch_cleanup.py --apply # delete them + python scripts/github_branch_cleanup.py --stale-days 90 # also list old unmerged ones + +Runs through the gh CLI as the logged-in account; deleting needs write +access to the repository. One compare request per branch, so a repository +with 500 branches takes a few minutes. +""" + +from __future__ import annotations + +import argparse +import json +import subprocess +import sys +from datetime import datetime, timedelta, timezone +from typing import Any +from urllib.parse import quote + +DEFAULT_REPO = "agentmiddleware/agent-middleware-api" +DEFAULT_KEEP_PREFIXES = ("backup/",) + + +def gh_api(method: str, path: str) -> Any: + proc = subprocess.run( + ["gh", "api", "-X", method, path], capture_output=True, text=True + ) + if proc.returncode != 0: + raise RuntimeError(proc.stderr.strip() or proc.stdout.strip()) + return json.loads(proc.stdout) if proc.stdout.strip() else None + + +def list_branches(repo: str) -> list[dict[str, Any]]: + branches: list[dict[str, Any]] = [] + page = 1 + while True: + chunk = gh_api("GET", f"repos/{repo}/branches?per_page=100&page={page}") + branches.extend(chunk) + if len(chunk) < 100: + return branches + page += 1 + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0]) + parser.add_argument("--repo", default=DEFAULT_REPO, help="owner/name") + parser.add_argument("--base", default="main", help="branch merged into") + parser.add_argument( + "--keep-prefix", + action="append", + default=None, + help=f"never touch branches starting with this (default: {DEFAULT_KEEP_PREFIXES})", + ) + parser.add_argument( + "--stale-days", + type=int, + default=None, + help="also list unmerged branches whose last commit is older than this", + ) + parser.add_argument("--apply", action="store_true", help="delete merged branches") + args = parser.parse_args(argv) + keep_prefixes = tuple(args.keep_prefix or DEFAULT_KEEP_PREFIXES) + + try: + branches = list_branches(args.repo) + except RuntimeError as exc: + print(f"gh api failed: {exc}", file=sys.stderr) + return 2 + + merged: list[tuple[str, str]] = [] # (name, sha seen during the scan) + stale: list[tuple[str, str, int]] = [] + skipped = {"protected": 0, "kept": 0, "base": 0} + cutoff = None + if args.stale_days is not None: + cutoff = datetime.now(timezone.utc) - timedelta(days=args.stale_days) + + print(f"{len(branches)} branches on {args.repo}; comparing against {args.base} ...") + for index, branch in enumerate(branches, start=1): + name = branch["name"] + if name == args.base: + skipped["base"] += 1 + continue + if branch.get("protected"): + skipped["protected"] += 1 + continue + if name.startswith(keep_prefixes): + skipped["kept"] += 1 + continue + try: + compare = gh_api( + "GET", + f"repos/{args.repo}/compare/{quote(args.base, safe='')}...{quote(name, safe='')}", + ) + except RuntimeError as exc: + print(f" ? {name}: compare failed ({exc})") + continue + if compare.get("ahead_by") == 0: + merged.append((name, branch["commit"]["sha"])) + elif cutoff is not None: + # The compare response lists at most one page of commits, oldest + # first, so its last entry is not the tip of a long branch. Read + # the tip commit itself. + tip = gh_api("GET", f"repos/{args.repo}/commits/{branch['commit']['sha']}") + last = tip["commit"]["committer"]["date"] + if datetime.fromisoformat(last.replace("Z", "+00:00")) < cutoff: + stale.append((name, last[:10], int(compare.get("ahead_by", 0)))) + if index % 50 == 0: + print(f" ... {index}/{len(branches)}") + + print( + f"\nSkipped: {skipped['protected']} protected, {skipped['kept']} kept by prefix, " + f"{skipped['base']} base." + ) + print(f"\nMerged into {args.base} (safe to delete): {len(merged)}") + for name, _sha in merged: + print(f" {name}") + if cutoff is not None: + print( + f"\nUnmerged and older than {args.stale_days} days (review by hand): {len(stale)}" + ) + for name, date, ahead in sorted(stale, key=lambda item: item[1]): + print(f" {date} {ahead:4d} ahead {name}") + + if not args.apply: + if merged: + print("\nDry run. Re-run with --apply to delete the merged branches.") + return 0 + + # The scan above takes minutes on a large repository. A branch that moved + # in the meantime is re-checked right before its ref is deleted: same tip + # as during the scan, and still zero commits ahead of the base. + failures = 0 + deleted = 0 + for name, scanned_sha in merged: + encoded = quote(name, safe="") + try: + tip = gh_api("GET", f"repos/{args.repo}/branches/{encoded}")["commit"][ + "sha" + ] + if tip != scanned_sha: + print(f" skipped {name}: moved since the scan ({tip[:8]})") + continue + recheck = gh_api( + "GET", + f"repos/{args.repo}/compare/{quote(args.base, safe='')}...{encoded}", + ) + if recheck.get("ahead_by") != 0: + print(f" skipped {name}: now {recheck.get('ahead_by')} ahead") + continue + gh_api("DELETE", f"repos/{args.repo}/git/refs/heads/{encoded}") + deleted += 1 + print(f" deleted {name}") + except RuntimeError as exc: + failures += 1 + print(f" failed {name}: {exc}") + print(f"\nDeleted {deleted} of {len(merged)} merged branches.") + return 1 if failures else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/github_repo_settings.py b/scripts/github_repo_settings.py new file mode 100644 index 00000000..49d1f792 --- /dev/null +++ b/scripts/github_repo_settings.py @@ -0,0 +1,279 @@ +#!/usr/bin/env python3 +"""Apply the repository settings in docs/github-org-configuration.md. + +Dry run by default: prints each setting as current -> target and changes +nothing. Pass --apply to write. Every call goes through the gh CLI, so it +runs as whatever GitHub account gh is logged in to; that account needs admin +on the repository. + +Usage: + python scripts/github_repo_settings.py # show drift + python scripts/github_repo_settings.py --apply # fix drift + python scripts/github_repo_settings.py --repo agentmiddleware/amw-demo + +What it manages: + 1. Repository flags: delete merged branches, allow auto-merge, allow + "update branch", require web commit sign-off. + 2. Branch protection on the default branch. Required status checks are + kept exactly as they are on the live rule (or taken from + --required-check when the branch has no rule yet), and the rule is + rewritten only when one of the other fields drifts: pull request + required (0 approvals while there is one maintainer), stale approvals + dismissed, admins not exempt, conversations resolved, no force push, + no deletion. + 3. Secret scanning and push protection. GitHub refuses these on a private + repository without the paid Secret Protection add-on; the script + reports that and moves on instead of failing. + +Exit status is 0 when nothing drifts (or everything was applied), 1 when a +dry run found drift, 2 on a gh failure. +""" + +from __future__ import annotations + +import argparse +import json +import subprocess +import sys +from typing import Any + +DEFAULT_REPO = "agentmiddleware/agent-middleware-api" + +REPO_TARGET: dict[str, bool] = { + # Merged branches pile up otherwise; the api repo reached 500+. + "delete_branch_on_merge": True, + "allow_auto_merge": True, + "allow_update_branch": True, + # DCO sign-off on web commits, matching CONTRIBUTING.md. + "web_commit_signoff_required": True, +} + +# Branch protection fields other than the required-check list. Approvals stay +# at 0 and code-owner review off while the project has one maintainer: GitHub +# does not let an author approve their own pull request, so 1 would block +# every merge. See docs/trust-release-gate-branch-protection.md. +PROTECTION_TARGET: dict[str, Any] = { + "enforce_admins": True, + "required_conversation_resolution": True, + "allow_force_pushes": False, + "allow_deletions": False, + "required_pull_request_reviews": { + "dismiss_stale_reviews": True, + "require_code_owner_reviews": False, + "required_approving_review_count": 0, + }, +} + + +class GhError(RuntimeError): + def __init__(self, status: int, message: str) -> None: + super().__init__(message) + self.status = status + + +def gh_api(method: str, path: str, body: dict[str, Any] | None = None) -> Any: + """Call the GitHub REST API through gh and return the parsed JSON body.""" + cmd = ["gh", "api", "-X", method, path] + stdin = None + if body is not None: + cmd.extend(["--input", "-"]) + stdin = json.dumps(body) + proc = subprocess.run(cmd, input=stdin, capture_output=True, text=True) + if proc.returncode != 0: + detail = proc.stderr.strip() or proc.stdout.strip() + status = 0 + try: + status = int(json.loads(proc.stdout or "{}").get("status", 0)) + except (ValueError, AttributeError): + pass + raise GhError(status, detail) + return json.loads(proc.stdout) if proc.stdout.strip() else None + + +def fmt(value: Any) -> str: + return json.dumps(value, sort_keys=True) + + +def check_repo_flags(repo: str, apply: bool) -> int: + repo_info = gh_api("GET", f"repos/{repo}") + drift = {k: v for k, v in REPO_TARGET.items() if repo_info.get(k) != v} + print(f"\n== Repository flags: {repo}") + for key, target in REPO_TARGET.items(): + mark = "ok " if key not in drift else "DRIFT" + print(f" {mark} {key}: {fmt(repo_info.get(key))} -> {fmt(target)}") + if not drift: + return 0 + if apply: + gh_api("PATCH", f"repos/{repo}", drift) + print(f" applied {len(drift)} change(s)") + return 0 + return 1 + + +def _protection_view(rule: dict[str, Any]) -> dict[str, Any]: + """Project the live protection rule onto the PROTECTION_TARGET shape.""" + reviews = rule.get("required_pull_request_reviews") + view: dict[str, Any] = { + "enforce_admins": bool(rule.get("enforce_admins", {}).get("enabled")), + "required_conversation_resolution": bool( + rule.get("required_conversation_resolution", {}).get("enabled") + ), + "allow_force_pushes": bool(rule.get("allow_force_pushes", {}).get("enabled")), + "allow_deletions": bool(rule.get("allow_deletions", {}).get("enabled")), + "required_pull_request_reviews": None, + } + if reviews is not None: + view["required_pull_request_reviews"] = { + "dismiss_stale_reviews": bool(reviews.get("dismiss_stale_reviews")), + "require_code_owner_reviews": bool( + reviews.get("require_code_owner_reviews") + ), + "required_approving_review_count": int( + reviews.get("required_approving_review_count", 0) + ), + } + return view + + +def check_branch_protection( + repo: str, branch: str, apply: bool, fallback_checks: list[str] +) -> int: + print(f"\n== Branch protection: {repo}@{branch}") + try: + rule = gh_api("GET", f"repos/{repo}/branches/{branch}/protection") + except GhError as exc: + if exc.status != 404 and "not protected" not in str(exc).lower(): + raise + rule = None + + # Fields this script does not manage but the PUT would reset to false if + # left out; replayed from the live rule so an unrelated fix cannot + # silently switch them off. + passthrough = { + "required_linear_history": False, + "block_creations": False, + "lock_branch": False, + "allow_fork_syncing": False, + } + if rule is None: + contexts = sorted(fallback_checks) + strict = True + current = {key: None for key in PROTECTION_TARGET} + print(" no protection rule exists yet") + else: + checks = rule.get("required_status_checks") or {} + contexts = sorted(checks.get("contexts") or []) + strict = bool(checks.get("strict", True)) + current = _protection_view(rule) + for key in passthrough: + passthrough[key] = bool((rule.get(key) or {}).get("enabled")) + + print( + f" required checks kept as-is ({len(contexts)}): {', '.join(contexts) or '-'}" + ) + drift = False + for key, target in PROTECTION_TARGET.items(): + same = current.get(key) == target + drift = drift or not same + mark = "ok " if same else "DRIFT" + print(f" {mark} {key}: {fmt(current.get(key))} -> {fmt(target)}") + if not drift: + return 0 + + body = { + "required_status_checks": {"strict": strict, "contexts": contexts}, + "enforce_admins": PROTECTION_TARGET["enforce_admins"], + "required_pull_request_reviews": PROTECTION_TARGET[ + "required_pull_request_reviews" + ], + "required_conversation_resolution": PROTECTION_TARGET[ + "required_conversation_resolution" + ], + "restrictions": None, + "allow_force_pushes": PROTECTION_TARGET["allow_force_pushes"], + "allow_deletions": PROTECTION_TARGET["allow_deletions"], + **passthrough, + } + if apply: + gh_api("PUT", f"repos/{repo}/branches/{branch}/protection", body) + print(" applied") + return 0 + print(" would PUT:") + print(" " + json.dumps(body, indent=2).replace("\n", "\n ")) + return 1 + + +def check_secret_scanning(repo: str, apply: bool) -> int: + print(f"\n== Secret scanning: {repo}") + info = gh_api("GET", f"repos/{repo}") + analysis = info.get("security_and_analysis") or {} + wanted = ("secret_scanning", "secret_scanning_push_protection") + drift = [ + key for key in wanted if (analysis.get(key) or {}).get("status") != "enabled" + ] + for key in wanted: + status = (analysis.get(key) or {}).get("status", "unavailable") + mark = "ok " if key not in drift else "DRIFT" + print(f" {mark} {key}: {status} -> enabled") + if not drift: + return 0 + if not apply: + return 1 + body = {"security_and_analysis": {key: {"status": "enabled"} for key in drift}} + try: + gh_api("PATCH", f"repos/{repo}", body) + except GhError as exc: + print( + " GitHub refused to enable secret scanning here. On a private\n" + " repository this needs the Secret Protection add-on (free on a\n" + " public repository). The gitleaks pre-commit hook and CI\n" + " secret_scan job remain the controls until then.\n" + f" gh said: {exc}" + ) + return 1 + print(" applied") + return 0 + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0]) + parser.add_argument("--repo", default=DEFAULT_REPO, help="owner/name") + parser.add_argument( + "--branch", + default=None, + help="branch to protect (default: the repository's default branch)", + ) + parser.add_argument( + "--required-check", + action="append", + default=[], + help=( + "status check context to require when creating a rule from scratch; " + "ignored when a rule already exists (repeatable)" + ), + ) + parser.add_argument("--apply", action="store_true", help="write changes") + args = parser.parse_args(argv) + + try: + branch = args.branch or gh_api("GET", f"repos/{args.repo}")["default_branch"] + results = [ + check_repo_flags(args.repo, args.apply), + check_branch_protection(args.repo, branch, args.apply, args.required_check), + check_secret_scanning(args.repo, args.apply), + ] + except GhError as exc: + print(f"gh api failed: {exc}", file=sys.stderr) + return 2 + + if any(results): + print( + "\nDrift remains." + ("" if args.apply else " Re-run with --apply to fix.") + ) + return 1 + print("\nNo drift.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/secret_scan_range.sh b/scripts/secret_scan_range.sh index 92ee17d4..006a98db 100644 --- a/scripts/secret_scan_range.sh +++ b/scripts/secret_scan_range.sh @@ -8,12 +8,13 @@ # pinned fallback base (tests use it; CI must not). set -euo pipefail -# Last main commit whose full ancestry is known clean: #811, the commit that -# introduced the commit-range scan. Everything after it on main was merged -# through a PR whose range was scanned; everything before it was covered by -# the full-history verification recorded in .gitleaks.toml. Move this forward -# only after a full-history scan of the new commit passes. -VERIFIED_BASE="${SECRET_SCAN_VERIFIED_BASE:-dd16051192acd72625ea9472c2a6802a497c5b4a}" +# Last main commit whose full ancestry is known clean: the tip of main on +# 2026-10-22, when a full-history gitleaks scan of a fresh clone (every ref, +# --log-opts=--all, this config) passed after the 2026-10-20 history rewrite +# had changed every SHA; see the scope note in .gitleaks.toml. Everything +# after it on main is merged through a PR whose range was scanned. Move this +# forward only after a full-history scan of the new commit passes. +VERIFIED_BASE="${SECRET_SCAN_VERIFIED_BASE:-9289867d50c6cbc184db3efc5ef630378fb5f973}" ZERO_SHA="0000000000000000000000000000000000000000" has_commit() { diff --git a/tests/test_sqlite_failed_cursor_release.py b/tests/test_sqlite_failed_cursor_release.py new file mode 100644 index 00000000..c558bca1 --- /dev/null +++ b/tests/test_sqlite_failed_cursor_release.py @@ -0,0 +1,117 @@ +"""The SQLite connection factory keeps failed statements off the event loop. + +Through CPython 3.11, a sqlite3 cursor whose execute() raised still owns its +prepared statement, and destroying that cursor later calls sqlite3_reset(), +which takes the connection mutex. aiosqlite runs statements on a worker thread +that holds that mutex for the whole busy-handler wait, so when the cyclic GC +destroys such a cursor on the event-loop thread while a sibling statement is +waiting for the WAL write lock, the loop freezes for the full busy timeout and +the lock holder, which can only commit from the loop, never does. Under +concurrent governed calls this surfaced as a 30 s stall plus "database is +locked" on the Python 3.11 CI leg only. + +This test is the mechanism in miniature, with a 10 s busy timeout: connection +B holds the write lock, connection A has a cursor from a failed INSERT and +then starts a write that waits for B, the main thread destroys the failed +cursor, and B is scheduled to commit 0.2 s later. With the factory, the main +thread is never blocked on the mutex, B commits, and A's write succeeds. +Without it, on 3.11, the main thread blocks for about 10 s and A's write +fails with "database is locked"; on 3.12+ the interpreter already detaches +the statement, so the factory is a no-op there and the test passes either +way. + +The timing bound is deliberately loose. Destroying the cursor runs a full +cyclic collection, which late in a long test session is slow on its own +(about 2 s was observed in a loaded xdist worker); what the fix removes is a +wait as long as the busy timeout, so the assertion is against half of it, +and a weak reference proves the cursor really was destroyed on this thread. +""" + +from __future__ import annotations + +import asyncio +import gc +import sqlite3 +import time +import weakref +from pathlib import Path + +import aiosqlite +import pytest + +from app.db.database import _StatementReleasingConnection + +BUSY_TIMEOUT_SECONDS = 10.0 + + +async def _stall_seconds( + db_path: Path, factory: type[sqlite3.Connection] | None +) -> tuple[float, str]: + """Return (seconds the main thread was blocked, outcome of A's write).""" + kwargs: dict[str, object] = {"timeout": BUSY_TIMEOUT_SECONDS} + if factory is not None: + kwargs["factory"] = factory + conn_a = await aiosqlite.connect(db_path, **kwargs) + conn_b = await aiosqlite.connect(db_path, **kwargs) + try: + await conn_a.execute("PRAGMA journal_mode=WAL") + await conn_a.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT)") + await conn_a.commit() + await conn_b.execute("PRAGMA journal_mode=WAL") + + # A failed statement leaves a cursor that, on 3.11, still owns it. + cursor = await conn_a.cursor() + await cursor.execute("INSERT INTO t (id, v) VALUES (?, ?)", (1, "x")) + await conn_a.commit() + with pytest.raises(sqlite3.IntegrityError, match="UNIQUE"): + await cursor.execute("INSERT INTO t (id, v) VALUES (?, ?)", (1, "dup")) + await conn_a.rollback() + failed_cursor = cursor._cursor # the underlying sqlite3.Cursor + cursor_ref = weakref.ref(failed_cursor) + del cursor + + # B takes the WAL write lock and keeps it while the loop runs. + await conn_b.execute("BEGIN IMMEDIATE") + await conn_b.execute("INSERT INTO t (id, v) VALUES (2, 'b')") + + # A's worker thread enters sqlite3_step() and waits in the busy handler. + pending_insert = asyncio.ensure_future( + conn_a.execute("INSERT INTO t (id, v) VALUES (3, 'a')") + ) + await asyncio.sleep(0.2) + + async def release_lock() -> None: + await asyncio.sleep(0.2) + await conn_b.commit() + + releaser = asyncio.ensure_future(release_lock()) + await asyncio.sleep(0) + + # Destroying the failed cursor on this thread is what blocked on 3.11. + started = time.monotonic() + del failed_cursor + gc.collect() + blocked = time.monotonic() - started + + await releaser + try: + await pending_insert + outcome = "ok" + except sqlite3.OperationalError as exc: + outcome = str(exc) + if cursor_ref() is not None: + outcome = "failed cursor survived collection; destruction was not exercised" + return blocked, outcome + finally: + await conn_a.close() + await conn_b.close() + + +def test_failed_statement_release_keeps_event_loop_running(tmp_path: Path) -> None: + blocked, outcome = asyncio.run( + _stall_seconds(tmp_path / "stall.db", _StatementReleasingConnection) + ) + assert outcome == "ok" + assert blocked < BUSY_TIMEOUT_SECONDS / 2, ( + f"main thread blocked {blocked:.2f}s in cursor destruction" + ) diff --git a/wrappers/autogen-agent-middleware/README.md b/wrappers/autogen-agent-middleware/README.md index df7bb637..5343bf29 100644 --- a/wrappers/autogen-agent-middleware/README.md +++ b/wrappers/autogen-agent-middleware/README.md @@ -14,7 +14,7 @@ This package is not published to PyPI. Install it from a checkout of this repository: ```bash -git clone https://github.com/PetrefiedThunder/agent-middleware-api.git +git clone https://github.com/agentmiddleware/agent-middleware-api.git cd agent-middleware-api python -m pip install -e ./b2a_sdk python -m pip install -e wrappers/autogen-agent-middleware diff --git a/wrappers/langchain-agent-middleware/README.md b/wrappers/langchain-agent-middleware/README.md index b38d1664..879c3734 100644 --- a/wrappers/langchain-agent-middleware/README.md +++ b/wrappers/langchain-agent-middleware/README.md @@ -14,7 +14,7 @@ This package is not published to PyPI. Install it from a checkout of this repository: ```bash -git clone https://github.com/PetrefiedThunder/agent-middleware-api.git +git clone https://github.com/agentmiddleware/agent-middleware-api.git cd agent-middleware-api python -m pip install -e ./b2a_sdk python -m pip install -e wrappers/langchain-agent-middleware diff --git a/wrappers/openai-agent-middleware/README.md b/wrappers/openai-agent-middleware/README.md index 0da3a527..49911575 100644 --- a/wrappers/openai-agent-middleware/README.md +++ b/wrappers/openai-agent-middleware/README.md @@ -42,7 +42,7 @@ This package is not published to PyPI. Install it from a checkout of this repository: ```bash -git clone https://github.com/PetrefiedThunder/agent-middleware-api.git +git clone https://github.com/agentmiddleware/agent-middleware-api.git cd agent-middleware-api python -m pip install -e ./b2a_sdk python -m pip install -e wrappers/openai-agent-middleware