From 3142b7397744821b3957aaa198eda591ea5d9b49 Mon Sep 17 00:00:00 2001 From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com> Date: Mon, 19 Oct 2026 07:33:01 +0000 Subject: [PATCH 01/14] ops: GitHub organization configuration runbook, settings scripts, grouped Dependabot Audit of the agentmiddleware GitHub organization on 2026-10-19, written up as docs/github-org-configuration.md with the gaps and the exact fix for each. Repository settings cannot be written from a repository-scoped token, so the fixes ship as scripts the maintainer runs with gh: - scripts/github_repo_settings.py: dry run by default; with --apply turns on delete-branch-on-merge, requires a pull request on main (0 approvals while there is one maintainer, since an author cannot approve their own pull request), and enables secret scanning where the plan allows. The required-check list is copied from the live rule, never hand-edited. - scripts/github_branch_cleanup.py: lists the remote branches with zero commits not already on main (550 branches exist because merged branches were never deleted); deletes only those, only with --apply; keeps backup/ and protected branches. Config changes in the tree: - .github/dependabot.yml: one grouped pull request per ecosystem instead of one per package. Each Dependabot pull request runs the full CI matrix (about 75 billed minutes), and six pip pull requests in one week was a quarter of the monthly private-repository allowance. Adds Docker digest, SDK, wrapper and Railway IaC coverage. - .pre-commit-config.yaml: gitleaks hook pinned to the CI release, as the local stand-in for push protection, which is unavailable on a private repository without the paid add-on. - .github/ISSUE_TEMPLATE/config.yml: security advisory link pointed at the old personal namespace. - docs/trust-release-gate-branch-protection.md: approval count and code-owner review match what a single maintainer can actually merge under. The runbook also records that pushes to main since 2026-10-17 produce only a startup_failure run with no workflow name (issue #805, the Actions billing lock), which leaves main unreleasable because railway-deploy.yml requires a green push run, and lays out the three options. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_015iJCmRk947fokDvpPaTSDf --- .github/ISSUE_TEMPLATE/config.yml | 2 +- .github/dependabot.yml | 75 +++++- .pre-commit-config.yaml | 9 + docs/README.md | 1 + docs/github-org-configuration.md | 237 ++++++++++++++++ docs/trust-release-gate-branch-protection.md | 20 +- scripts/github_branch_cleanup.py | 150 +++++++++++ scripts/github_repo_settings.py | 267 +++++++++++++++++++ 8 files changed, 753 insertions(+), 8 deletions(-) create mode 100644 docs/github-org-configuration.md create mode 100644 scripts/github_branch_cleanup.py create mode 100644 scripts/github_repo_settings.py 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..b3073e21 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,16 +1,89 @@ 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. After +# merging a grouped pip update, run `make lock` and commit the result; the +# lint job's "Verify requirements.lock matches requirements.txt" step fails +# until the lock satisfies the new floors. 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 into one pull + # request so a wrapper floor bump and an SDK floor bump land together. + - 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/.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/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/github-org-configuration.md b/docs/github-org-configuration.md new file mode 100644 index 00000000..61f6535f --- /dev/null +++ b/docs/github-org-configuration.md @@ -0,0 +1,237 @@ +# 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. Re-run the two scripts below to refresh the repository +rows; the organization rows can only be read in the GitHub UI. + +## Do these first, in this order + +1. **Unblock GitHub Actions on the organization account.** Pushes to `main` + since 2026-10-17 produced no CI run, only a `startup_failure` run with no + workflow name. `railway-deploy.yml` refuses to prepare a release for a + commit without a green push run, so `main` is unreleasable until this is + fixed. See [Actions budget](#actions-budget) for the cause and the choice + to make. +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 + ``` + + This turns on "delete branch on merge" and the pull-request requirement on + `main` (0 approvals, see below), and tries to enable secret scanning. +3. **Clean up the 500+ stale branches** once "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 + ``` + +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 | 6: `agent-middleware-api` (private), `agent-middleware` (private, 54 open issues), `agent-middleware-public` (private despite the name), `agent-middleware-spec` (public), `amw-demo` (public), `amw-paypal-agent-demo` (public) | Decide which repository is the product repository; see [Repositories](#repositories) | +| Actions runs on `main` pushes | `startup_failure`, no workflow name, since 2026-10-17 20:19 UTC. Scheduled and Dependabot runs still succeed | 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, 0 approvals, code-owner review off | `github_repo_settings.py --apply` | +| Delete branch on merge | **Off** (why 500+ branches exist) | On | `github_repo_settings.py --apply` | +| Auto-merge, update branch | On | Keep | Already correct | +| Web commit sign-off (DCO) | On | Keep | Already correct | +| Secret scanning, push protection | **Disabled** | Enabled where the plan allows; otherwise the gitleaks pre-commit hook plus the CI `secret_scan` job | `github_repo_settings.py --apply`, see [Secret scanning](#secret-scanning) | +| Dependabot security updates | Enabled | Keep | Already correct | +| Dependabot version updates | One pull request per package, pip and Actions only | Grouped weekly pull requests; Docker digest, SDK, wrappers and Railway IaC covered | Done in `.github/dependabot.yml` on this branch | +| Rulesets | None | None needed; the classic rule above covers `main` | – | +| `master` branch | Exists, protected, 9 commits ahead and 1,239 behind `main`, last commit 2026-05-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. +- Commits on 2026-10-15 are titled "retrigger checks after billing lock + lifted"; pull request 816 (merged 2026-10-17) is titled "after billing + lock". So the lock has happened at least twice this month. +- The workflow files did not change between the last green push run + (`44cd144`, 2026-10-16) and the current head, which rules out a broken + workflow file. + +### Why it keeps happening + +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. + +| Measured on run 2179 (2026-10-16, `main`) | Minutes | +|---|---| +| `test (3.11)`, `test (3.12)`, `test (3.13)` | 12.5 + 17.3 + 10.3 | +| `trust_release_gate`, `postgres_permit_concurrency` | 3.7 + 3.8 | +| `postgres_trust`, `lint`, `invariant_attack_proof`, `production_trust`, `secret_scan`, `constant_test_loop` | about 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 75 | +| Plus `Failure Lab` (fast tier on pull requests, slow tier on `main`) and `docker-publish` | 3 to 10 | + +Between 2026-10-15 and 2026-10-17 there were about 30 CI runs, which is over +2,000 billed minutes in three days. 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 is +roughly 10,000 to 15,000 minutes, or $65 to $105 of overage. + +### The choice + +Pick one. The first is the only option that costs nothing and removes the +problem rather than budgeting for it. + +1. **Make `agent-middleware-api` public.** Public repositories get unlimited + Actions minutes on standard runners, free secret scanning, free push + protection, and private vulnerability reporting. The repository already + carries the BUSL license, a `SECURITY.md`, and tests that assert the public + surfaces link to `github.com/agentmiddleware/agent-middleware-api`, so this + was the plan. Before flipping: run + `gitleaks detect --config .gitleaks.toml --log-opts=--all` on a fresh clone + and confirm the only findings are the allowlisted ones recorded in + `.gitleaks.toml` (the 2026-08-06 key was rotated dead the next day and is + deliberately published in [api-key-rotation.md](api-key-rotation.md)). + Visibility: repository Settings → General → Danger Zone → Change + visibility. +2. **Set a spending limit and leave it private.** Organization Settings → + Billing and licensing → Budgets and alerts: create an Actions budget with + "stop usage when reached" off (or a limit you can afford) and an alert at + 75%. A limit of $0, the default, is what produces the lock. Check the + current month under Billing → Usage, product "Actions". +3. **Reduce the bill per push** (works with either of the above). The + non-required legs `test (3.13)`, `python_sdk (3.13)` and the four + `wrapper_unit` jobs are about 15 minutes of each run; gating them on + `github.event_name == 'push'` keeps them on `main` and drops them from pull + requests. The two required `test` legs are the other 30 minutes and would + need test parallelization to shrink. Not done on this branch: it changes + what runs on pull requests and belongs in its own reviewed change. + +Whichever you pick, after the block clears, `main` still has no green push +run for `railway-deploy.yml` to gate on. `ci.yml` has no manual trigger, so +merge the next pull request (this one will do) and confirm its push run is +green before preparing a release. + +## Secret scanning + +GitHub's own secret scanning and push protection are off on this repository. +On a private repository they require the Secret Protection add-on; on a public +one they are free. `github_repo_settings.py --apply` tries to enable both and +prints GitHub's refusal if the plan does not allow it. + +Until then the controls are: + +- `.pre-commit-config.yaml` now runs gitleaks on the staged diff with the same + `.gitleaks.toml` and the same gitleaks release as CI. Install once with + `pre-commit install`. This is the local equivalent of push protection. +- The CI `secret_scan` job scans the working tree and the pushed commit range + and is a required check on `main`. + +## Remove master + +`master` is a protected leftover from before the default branch was `main`. It +is 1,239 commits behind and carries 9 commits that never reached `main`. +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. None of it is readable or +writable through the repository-scoped token this document was produced +with, so it is a checklist, not a script. + +| 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 | Matches `allow_forking: false` already set on the private repositories | +| 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; this stops a future workflow from pulling an unpinned third-party action | +| 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" | Relevant the day a repository is public | +| 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 where the plan allows | One place instead of six repository pages | +| 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 | +| Billing and licensing | Actions budget and alert | See [Actions budget](#actions-budget) | The current block | +| Profile | Verified domain | Add `thisisatest.tech` (the repository homepage) | 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 require only a pull request. + +| Repository | Visibility | Notes | +|---|---|---| +| `agent-middleware-api` | Private | This document | +| `agent-middleware` | Private | 54 open issues. Created 2026-10-04 as "pre-release, private". If it is the successor of this repository, say so in both READMEs; if not, archive one of them | +| `agent-middleware-public` | Private | The name says public and the description says "private staging repo". Rename or flip visibility so the name is true | +| `agent-middleware-spec` | Public | Enable secret scanning, push protection and private vulnerability reporting; all free on a public repository | +| `amw-demo`, `amw-paypal-agent-demo` | Public | Same as the spec repository | + +## Stale references still in the tree + +These point at the old personal namespace. GitHub redirects them, so nothing +is broken, and they are listed here rather than changed on this branch: + +- `README.md` CI badge and clone URL, `TROUBLESHOOTING.md` bug-report link, + `docs/awi-adoption-guide.md`, `docs/agentmarket-listing.md`, the framework + and wrapper READMEs, `b2a_sdk/README.md` release links, and the LICENSE URL + in `app/main.py`. +- `server.json` names the MCP Registry entry `io.github.PetrefiedThunder/...`. + That is the registry identity the publisher authenticated as; changing it is + a registry re-submission, not a link fix. Leave it. + +## 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 does not change `ci.yml`. The per-push cost reduction above is a + separate, reviewable change. 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/scripts/github_branch_cleanup.py b/scripts/github_branch_cleanup.py new file mode 100644 index 00000000..774a437c --- /dev/null +++ b/scripts/github_branch_cleanup.py @@ -0,0 +1,150 @@ +#!/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[str] = [] + 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) + elif cutoff is not None: + commits = compare.get("commits") or [] + last = commits[-1]["commit"]["committer"]["date"] if commits else None + if last and 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 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 + + failures = 0 + for name in merged: + try: + gh_api("DELETE", f"repos/{args.repo}/git/refs/heads/{quote(name, safe='')}") + print(f" deleted {name}") + except RuntimeError as exc: + failures += 1 + print(f" failed {name}: {exc}") + print(f"\nDeleted {len(merged) - failures} 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..abd0d99c --- /dev/null +++ b/scripts/github_repo_settings.py @@ -0,0 +1,267 @@ +#!/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 + + 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) + + 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"], + } + 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()) From 3ad1a97a9c465afd5c9c001392480fa981c43cbd Mon Sep 17 00:00:00 2001 From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com> Date: Mon, 19 Oct 2026 07:36:38 +0000 Subject: [PATCH 02/14] docs(org): record branch-cleanup dry run counts and that the Actions block cleared Dry run of scripts/github_branch_cleanup.py on 2026-10-19: 550 branches, 103 fully merged into main, 26 unmerged and idle for 90 days, 45 backup/ kept, 375 recent unmerged agent branches that need a decision rather than a script. The pull request carrying this document started all 20 checks normally, so the Actions billing lock reported on 2026-10-17 had cleared by 2026-10-19 07:30 UTC; the runbook now says so instead of presenting the block as current. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_015iJCmRk947fokDvpPaTSDf --- docs/github-org-configuration.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/docs/github-org-configuration.md b/docs/github-org-configuration.md index 61f6535f..9a2a266c 100644 --- a/docs/github-org-configuration.md +++ b/docs/github-org-configuration.md @@ -26,8 +26,8 @@ rows; the organization rows can only be read in the GitHub UI. This turns on "delete branch on merge" and the pull-request requirement on `main` (0 approvals, see below), and tries to enable secret scanning. -3. **Clean up the 500+ stale branches** once "delete branch on merge" is on, - so they stop accumulating: +3. **Clean up the 550 branches** once "delete branch on merge" is on, so they + stop accumulating: ```bash python scripts/github_branch_cleanup.py # lists what is fully merged @@ -35,6 +35,14 @@ rows; the organization rows can only be read in the GitHub UI. 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` (the + `--apply` set); 26 unmerged with no commit in 90 days (oldest from + 2026-04-17, mostly `feat/*-pg` and `codex/*` work that never landed); 45 + `backup/local-2026-10-07/*` kept by prefix; the remaining 375 are unmerged + branches from the last 90 days, nearly all agent-generated. Those need a + decision, not a script: either open the pull request or delete the + branch. The 26 stale ones are the first batch to decide on. + 4. **Work through the [organization settings](#organization-settings) table.** None of it can be scripted from a repository-scoped token. @@ -88,6 +96,9 @@ and turn on code-owner review the day a second maintainer has commit access. - The workflow files did not change between the last green push run (`44cd144`, 2026-10-16) and the current head, which rules out a broken workflow file. +- By 2026-10-19 07:30 UTC the block had cleared: the pull request that adds + this document started all 20 checks normally. The lock is intermittent, + which is the point: it returns whenever the month's minutes run out. ### Why it keeps happening From 38e95fb277c1d932cb0bf1f0629d0b7e278ea205 Mon Sep 17 00:00:00 2001 From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com> Date: Tue, 20 Oct 2026 08:55:34 +0000 Subject: [PATCH 03/14] fix(lint): silence F811 on fixtures imported into test_demo_key_funnel tests/test_demo_key_funnel.py (from #832) imports the client and demo_settings fixtures from tests.test_demo_tenant and then names them as parameters, which ruff reports as F811 twelve times, so the required lint check fails on main's head and on every branch that merges it. Pushes to main have produced no CI run since 2026-10-17, which is why the failure never surfaced there. Per-parameter noqa, the convention tests/test_retry_loop_alert.py already uses, plus ruff format on the one block the formatter wanted changed. Ported into this branch so the required check can pass; the same diff applies cleanly to main. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_015iJCmRk947fokDvpPaTSDf --- tests/test_demo_key_funnel.py | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/tests/test_demo_key_funnel.py b/tests/test_demo_key_funnel.py index dca55369..76cce5d6 100644 --- a/tests/test_demo_key_funnel.py +++ b/tests/test_demo_key_funnel.py @@ -24,7 +24,10 @@ FUNNEL = "/v1/admin/demo-keys/funnel" -async def _issue(client: AsyncClient, **attribution: str) -> dict: +async def _issue( + client: AsyncClient, # noqa: F811 + **attribution: str, +) -> dict: resp = await client.post("/v1/demo/keys", json=attribution) assert resp.status_code == 201, resp.text return resp.json() @@ -38,7 +41,10 @@ async def _key_row(key_id: str) -> APIKeyModel: return result.scalar_one() -async def _use(client: AsyncClient, body: dict) -> None: +async def _use( + client: AsyncClient, # noqa: F811 + body: dict, +) -> None: resp = await client.get("/v1/me/authority", headers=demo_headers(body)) assert resp.status_code == 200, resp.text @@ -49,7 +55,10 @@ async def _use(client: AsyncClient, body: dict) -> None: @pytest.mark.anyio @pytest.mark.parametrize("max_uses", [None, 10]) async def test_first_used_at_set_once_and_never_overwritten( - client: AsyncClient, clean_database: None, demo_settings, max_uses + client: AsyncClient, # noqa: F811 + clean_database: None, + demo_settings, # noqa: F811 + max_uses, ) -> None: # None = unbounded key (ORM path); 10 = guarded UPDATE path. demo_settings.DEMO_KEY_MAX_USES = max_uses @@ -72,7 +81,9 @@ async def test_first_used_at_set_once_and_never_overwritten( @pytest.mark.anyio async def test_funnel_counts_issued_vs_used_by_source( - client: AsyncClient, clean_database: None, demo_settings + client: AsyncClient, # noqa: F811 + clean_database: None, + demo_settings, # noqa: F811 ) -> None: # Five keys from one test client exceed the default per-IP cap of 3. demo_settings.DEMO_ISSUE_PER_IP_PER_DAY = 10 @@ -115,7 +126,9 @@ async def test_funnel_counts_issued_vs_used_by_source( @pytest.mark.anyio async def test_funnel_since_until_filter_on_issuance_time( - client: AsyncClient, clean_database: None, demo_settings + client: AsyncClient, # noqa: F811 + clean_database: None, + demo_settings, # noqa: F811 ) -> None: await _issue(client, utm_source="a") now = datetime.now(timezone.utc) @@ -141,7 +154,9 @@ async def test_funnel_since_until_filter_on_issuance_time( @pytest.mark.anyio async def test_funnel_requires_bootstrap_admin( - client: AsyncClient, clean_database: None, demo_settings + client: AsyncClient, # noqa: F811 + clean_database: None, + demo_settings, # noqa: F811 ) -> None: demo = await _issue(client) normal = await provision_agent_wallet(client) @@ -161,7 +176,9 @@ async def test_funnel_requires_bootstrap_admin( @pytest.mark.anyio async def test_funnel_works_with_kill_switch_off( - client: AsyncClient, clean_database: None, demo_settings + client: AsyncClient, # noqa: F811 + clean_database: None, + demo_settings, # noqa: F811 ) -> None: await _issue(client, utm_source="a") demo_settings.ENABLE_DEMO_TENANT = False @@ -224,9 +241,7 @@ def test_050_upgrade_backfills_creates_view_and_downgrades( assert "demo_key_funnel_by_source" in inspect(engine).get_view_names() with engine.begin() as connection: backfilled = dict( - connection.execute( - text("SELECT key_id, first_used_at FROM api_keys") - ).all() + connection.execute(text("SELECT key_id, first_used_at FROM api_keys")).all() ) view_rows = connection.execute( text( From bd3de94fa210578da1f04bbcc475a00d44bfe9eb Mon Sep 17 00:00:00 2001 From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com> Date: Tue, 20 Oct 2026 08:57:23 +0000 Subject: [PATCH 04/14] docs(org): the Actions block recurs in windows; point at the run banner The no-name startup_failure run has fired 139 times since 2026-09-15 in four windows between 2026-10-14 and 2026-10-19, on push and pull-request events alike, with normal runs in between. Record the windows, the run page that shows GitHub's reason, and the release consequence for main commits that land inside a window. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_015iJCmRk947fokDvpPaTSDf --- docs/github-org-configuration.md | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/docs/github-org-configuration.md b/docs/github-org-configuration.md index 9a2a266c..e18d0b23 100644 --- a/docs/github-org-configuration.md +++ b/docs/github-org-configuration.md @@ -96,9 +96,24 @@ and turn on code-owner review the day a second maintainer has commit access. - The workflow files did not change between the last green push run (`44cd144`, 2026-10-16) and the current head, which rules out a broken workflow file. -- By 2026-10-19 07:30 UTC the block had cleared: the pull request that adds - this document started all 20 checks normally. The lock is intermittent, - which is the point: it returns whenever the month's minutes run out. +- 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, clustered on 2026-10-14 + 19:54 to 21:26, 2026-10-17 20:19 to 20:35, 2026-10-19 11:23 to 11:28 and + 2026-10-19 21:26 to 21:28 UTC, on push and pull-request events alike, + including the push of every `main` commit since 2026-10-17. Between those + windows (for example this pull request's runs on 2026-10-19 07:30 and + 2026-10-20 08:50) 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 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 keeps happening From 64f6ae9d0b4131dd5022c07fed4478e5499229c8 Mon Sep 17 00:00:00 2001 From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com> Date: Tue, 20 Oct 2026 09:08:29 +0000 Subject: [PATCH 05/14] docs(org): record that the repositories went public on 2026-10-20 Five of six repositories are public as of 2026-10-20; GitHub switched on secret scanning, push protection, validity checks, non-provider patterns and private vulnerability reporting at the flip. The Actions-minutes bill is gone for standard runners, so the budget section records the decision instead of posing the choice. Remaining items: agent-middleware-public is still private, three public repositories still describe themselves as private, and fork pull-request approval now matters. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_015iJCmRk947fokDvpPaTSDf --- docs/github-org-configuration.md | 140 ++++++++++++++++--------------- 1 file changed, 73 insertions(+), 67 deletions(-) diff --git a/docs/github-org-configuration.md b/docs/github-org-configuration.md index e18d0b23..6433efa9 100644 --- a/docs/github-org-configuration.md +++ b/docs/github-org-configuration.md @@ -5,17 +5,20 @@ 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. Re-run the two scripts below to refresh the repository -rows; the organization rows can only be read in the GitHub UI. +Audit date: 2026-10-19, updated 2026-10-20 after the repositories were made +public. Re-run the two scripts below to refresh the repository rows; the +organization rows can only be read in the GitHub UI. ## Do these first, in this order -1. **Unblock GitHub Actions on the organization account.** Pushes to `main` - since 2026-10-17 produced no CI run, only a `startup_failure` run with no - workflow name. `railway-deploy.yml` refuses to prepare a release for a - commit without a green push run, so `main` is unreleasable until this is - fixed. See [Actions budget](#actions-budget) for the cause and the choice - to make. +1. **Done 2026-10-20: the repositories are public.** That removes the + Actions minutes bill on standard runners and switched on secret scanning, + push protection and private vulnerability reporting by itself. Two + follow-ups: watch the push run of the next `main` merge (if the no-name + `startup_failure` in [Actions budget](#actions-budget) recurs on a public + repository, billing was not the cause), and fix the descriptions that + still say "private" on public repositories (see + [Repositories](#repositories)). 2. **Apply the repository settings** from a machine where `gh auth status` shows your account: @@ -25,7 +28,7 @@ rows; the organization rows can only be read in the GitHub UI. ``` This turns on "delete branch on merge" and the pull-request requirement on - `main` (0 approvals, see below), and tries to enable secret scanning. + `main` (0 approvals, see below). Secret scanning is already on. 3. **Clean up the 550 branches** once "delete branch on merge" is on, so they stop accumulating: @@ -54,8 +57,8 @@ rows; the organization rows can only be read in the GitHub UI. |---|---|---| | 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 | 6: `agent-middleware-api` (private), `agent-middleware` (private, 54 open issues), `agent-middleware-public` (private despite the name), `agent-middleware-spec` (public), `amw-demo` (public), `amw-paypal-agent-demo` (public) | Decide which repository is the product repository; see [Repositories](#repositories) | -| Actions runs on `main` pushes | `startup_failure`, no workflow name, since 2026-10-17 20:19 UTC. Scheduled and Dependabot runs still succeed | Green push run on every `main` commit | +| Repositories | 6. Since 2026-10-20 five are public; `agent-middleware-public` is still private. Three public repositories still describe themselves as private | Decide which repository is the product repository and fix the descriptions; 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. Scheduled, Dependabot and most pull-request runs succeed | Green push run on every `main` commit; confirm on the first merge after going public | | Two-factor requirement, base permission, Actions policies, security configuration | Not readable from here | See [Organization settings](#organization-settings) | ### `agent-middleware-api` @@ -70,7 +73,8 @@ rows; the organization rows can only be read in the GitHub UI. | Delete branch on merge | **Off** (why 500+ branches exist) | On | `github_repo_settings.py --apply` | | Auto-merge, update branch | On | Keep | Already correct | | Web commit sign-off (DCO) | On | Keep | Already correct | -| Secret scanning, push protection | **Disabled** | Enabled where the plan allows; otherwise the gitleaks pre-commit hook plus the CI `secret_scan` job | `github_repo_settings.py --apply`, see [Secret scanning](#secret-scanning) | +| 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 weekly pull requests; Docker digest, SDK, wrappers and Railway IaC covered | Done in `.github/dependabot.yml` on this branch | | Rulesets | None | None needed; the classic rule above covers `main` | – | @@ -115,11 +119,12 @@ and turn on code-owner review the day a second maintainer has commit access. 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 keeps happening +### 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. +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 | |---|---| @@ -137,56 +142,56 @@ 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 is roughly 10,000 to 15,000 minutes, or $65 to $105 of overage. -### The choice - -Pick one. The first is the only option that costs nothing and removes the -problem rather than budgeting for it. - -1. **Make `agent-middleware-api` public.** Public repositories get unlimited - Actions minutes on standard runners, free secret scanning, free push - protection, and private vulnerability reporting. The repository already - carries the BUSL license, a `SECURITY.md`, and tests that assert the public - surfaces link to `github.com/agentmiddleware/agent-middleware-api`, so this - was the plan. Before flipping: run - `gitleaks detect --config .gitleaks.toml --log-opts=--all` on a fresh clone - and confirm the only findings are the allowlisted ones recorded in - `.gitleaks.toml` (the 2026-08-06 key was rotated dead the next day and is - deliberately published in [api-key-rotation.md](api-key-rotation.md)). - Visibility: repository Settings → General → Danger Zone → Change - visibility. -2. **Set a spending limit and leave it private.** Organization Settings → - Billing and licensing → Budgets and alerts: create an Actions budget with - "stop usage when reached" off (or a limit you can afford) and an alert at - 75%. A limit of $0, the default, is what produces the lock. Check the - current month under Billing → Usage, product "Actions". -3. **Reduce the bill per push** (works with either of the above). The - non-required legs `test (3.13)`, `python_sdk (3.13)` and the four - `wrapper_unit` jobs are about 15 minutes of each run; gating them on - `github.event_name == 'push'` keeps them on `main` and drops them from pull - requests. The two required `test` legs are the other 30 minutes and would - need test parallelization to shrink. Not done on this branch: it changes - what runs on pull requests and belongs in its own reviewed change. - -Whichever you pick, after the block clears, `main` still has no green push -run for `railway-deploy.yml` to gate on. `ci.yml` has no manual trigger, so -merge the next pull request (this one will do) and confirm its push run is -green before preparing a release. +### 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 the BUSL +license, a `SECURITY.md`, and tests asserting that public surfaces link to +`github.com/agentmiddleware/agent-middleware-api`. The commit history is now +public too, including the 2026-08-06 key that was rotated dead the next day +and is deliberately published in [api-key-rotation.md](api-key-rotation.md); +every other historical finding is allowlisted synthetic material +(`.gitleaks.toml`). + +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` still has no green push run for `railway-deploy.yml` to gate on. +`ci.yml` has no manual trigger, so merge the next pull request (this one will +do) and confirm its push run is green before preparing a release. ## Secret scanning -GitHub's own secret scanning and push protection are off on this repository. -On a private repository they require the Secret Protection add-on; on a public -one they are free. `github_repo_settings.py --apply` tries to enable both and -prints GitHub's refusal if the plan does not allow it. +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). `github_repo_settings.py` reports them and would re-enable them if +they were ever switched off. -Until then the controls are: +Three layers now cover the same risk, and all three stay: -- `.pre-commit-config.yaml` now runs gitleaks on the staged diff with the same - `.gitleaks.toml` and the same gitleaks release as CI. Install once with - `pre-commit install`. This is the local equivalent of push protection. +- 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`. +Alerts from GitHub's scanner appear under the repository's Security tab. +Expect the 2026-08-06 retired key to be flagged from history; close that +alert 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`. It @@ -215,13 +220,13 @@ with, so it is a checklist, not a script. | 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 | Matches `allow_forking: false` already set on the private repositories | +| 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; this stops a future workflow from pulling an unpinned third-party action | | 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" | Relevant the day a repository is public | -| 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 where the plan allows | One place instead of six repository pages | -| 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 | -| Billing and licensing | Actions budget and alert | See [Actions budget](#actions-budget) | The current block | +| Actions → General | Fork pull request workflows | "Require approval for all external contributors" | Now live: every public repository accepts fork pull requests, and a fork's workflow run would otherwise 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 six 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 | | Profile | Verified domain | Add `thisisatest.tech` (the repository homepage) | Shows the "verified" badge next to the organization | ## Repositories @@ -230,13 +235,14 @@ 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 require only a pull request. -| Repository | Visibility | Notes | +| Repository | Visibility (2026-10-20) | Notes | |---|---|---| -| `agent-middleware-api` | Private | This document | -| `agent-middleware` | Private | 54 open issues. Created 2026-10-04 as "pre-release, private". If it is the successor of this repository, say so in both READMEs; if not, archive one of them | -| `agent-middleware-public` | Private | The name says public and the description says "private staging repo". Rename or flip visibility so the name is true | -| `agent-middleware-spec` | Public | Enable secret scanning, push protection and private vulnerability reporting; all free on a public repository | -| `amw-demo`, `amw-paypal-agent-demo` | Public | Same as the spec repository | +| `agent-middleware-api` | Public | This document | +| `agent-middleware` | Public | 45 open issues. Description still says "pre-release, private": fix it. If it is the successor of this repository, say so in both READMEs; if not, archive one of them | +| `agent-middleware-public` | **Private** | The only one left private, and the one whose name says public. Description still says "private staging repo". Flip it or rename it so the name is true | +| `agent-middleware-spec` | Public | Confirm secret scanning and private vulnerability reporting are on (GitHub enables them on public repositories, but check the Security tab once) | +| `amw-demo` | Public | Description still says "private until approved": fix it. Same Security-tab check | +| `amw-paypal-agent-demo` | Public | Same Security-tab check | ## Stale references still in the tree From 101d8f151c9e2a1f71d39f7fdc672f07775548c6 Mon Sep 17 00:00:00 2001 From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com> Date: Tue, 20 Oct 2026 09:28:57 +0000 Subject: [PATCH 06/14] test: take main's F811 fix for test_demo_key_funnel (#837) over the ported one main fixed the same lint failure in #837 after this branch ported its own fix in 38e95fb. Keep main's version so this pull request no longer touches the file. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_015iJCmRk947fokDvpPaTSDf --- tests/test_demo_key_funnel.py | 31 +++++++------------------------ 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/tests/test_demo_key_funnel.py b/tests/test_demo_key_funnel.py index 92f74666..2312fd1c 100644 --- a/tests/test_demo_key_funnel.py +++ b/tests/test_demo_key_funnel.py @@ -29,10 +29,7 @@ FUNNEL = "/v1/admin/demo-keys/funnel" -async def _issue( - client: AsyncClient, # noqa: F811 - **attribution: str, -) -> dict: +async def _issue(client: AsyncClient, **attribution: str) -> dict: resp = await client.post("/v1/demo/keys", json=attribution) assert resp.status_code == 201, resp.text return resp.json() @@ -46,10 +43,7 @@ async def _key_row(key_id: str) -> APIKeyModel: return result.scalar_one() -async def _use( - client: AsyncClient, # noqa: F811 - body: dict, -) -> None: +async def _use(client: AsyncClient, body: dict) -> None: resp = await client.get("/v1/me/authority", headers=demo_headers(body)) assert resp.status_code == 200, resp.text @@ -60,10 +54,7 @@ async def _use( @pytest.mark.anyio @pytest.mark.parametrize("max_uses", [None, 10]) async def test_first_used_at_set_once_and_never_overwritten( - client: AsyncClient, # noqa: F811 - clean_database: None, - demo_settings, # noqa: F811 - max_uses, + client: AsyncClient, clean_database: None, demo_settings, max_uses ) -> None: # None = unbounded key (ORM path); 10 = guarded UPDATE path. demo_settings.DEMO_KEY_MAX_USES = max_uses @@ -86,9 +77,7 @@ async def test_first_used_at_set_once_and_never_overwritten( @pytest.mark.anyio async def test_funnel_counts_issued_vs_used_by_source( - client: AsyncClient, # noqa: F811 - clean_database: None, - demo_settings, # noqa: F811 + client: AsyncClient, clean_database: None, demo_settings ) -> None: # Five keys from one test client exceed the default per-IP cap of 3. demo_settings.DEMO_ISSUE_PER_IP_PER_DAY = 10 @@ -131,9 +120,7 @@ async def test_funnel_counts_issued_vs_used_by_source( @pytest.mark.anyio async def test_funnel_since_until_filter_on_issuance_time( - client: AsyncClient, # noqa: F811 - clean_database: None, - demo_settings, # noqa: F811 + client: AsyncClient, clean_database: None, demo_settings ) -> None: await _issue(client, utm_source="a") now = datetime.now(timezone.utc) @@ -159,9 +146,7 @@ async def test_funnel_since_until_filter_on_issuance_time( @pytest.mark.anyio async def test_funnel_requires_bootstrap_admin( - client: AsyncClient, # noqa: F811 - clean_database: None, - demo_settings, # noqa: F811 + client: AsyncClient, clean_database: None, demo_settings ) -> None: demo = await _issue(client) normal = await provision_agent_wallet(client) @@ -181,9 +166,7 @@ async def test_funnel_requires_bootstrap_admin( @pytest.mark.anyio async def test_funnel_works_with_kill_switch_off( - client: AsyncClient, # noqa: F811 - clean_database: None, - demo_settings, # noqa: F811 + client: AsyncClient, clean_database: None, demo_settings ) -> None: await _issue(client, utm_source="a") demo_settings.ENABLE_DEMO_TENANT = False From a4e445347f5f4e001877193af70049e74ebce367 Mon Sep 17 00:00:00 2001 From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com> Date: Tue, 20 Oct 2026 09:49:41 +0000 Subject: [PATCH 07/14] fix(scripts): address Codex review on the GitHub settings and branch scripts - github_branch_cleanup.py: staleness reads the branch tip commit instead of the last entry of the compare page (oldest-first, one page), so a long branch with recent work is no longer reported stale. Before each deletion the branch tip is re-read and the zero-ahead compare re-run, so a branch that moved during the minutes-long scan is skipped, never deleted. - github_repo_settings.py: the protection PUT replays required_linear_history, block_creations, lock_branch and allow_fork_syncing from the live rule instead of letting the endpoint reset them to false. - dependabot.yml: the lock has to be regenerated on the Dependabot branch before merge (the required lint job blocks a stale lock and admins cannot bypass it), not after; and the SDK-and-wrappers entry is described as per-directory grouping, which is what is documented. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_015iJCmRk947fokDvpPaTSDf --- .github/dependabot.yml | 19 ++++++++++----- scripts/github_branch_cleanup.py | 40 +++++++++++++++++++++++++------- scripts/github_repo_settings.py | 12 ++++++++++ 3 files changed, 56 insertions(+), 15 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b3073e21..159aae7e 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,10 +6,14 @@ version: 2 # 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. After -# merging a grouped pip update, run `make lock` and commit the result; the -# lint job's "Verify requirements.lock matches requirements.txt" step fails -# until the lock satisfies the new floors. +# 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: "/" @@ -31,8 +35,11 @@ updates: patterns: - "*" - # Published packages with their own manifests. Grouped into one pull - # request so a wrapper floor bump and an SDK floor bump land together. + # 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" diff --git a/scripts/github_branch_cleanup.py b/scripts/github_branch_cleanup.py index 774a437c..f2b3b1a8 100644 --- a/scripts/github_branch_cleanup.py +++ b/scripts/github_branch_cleanup.py @@ -78,7 +78,7 @@ def main(argv: list[str] | None = None) -> int: print(f"gh api failed: {exc}", file=sys.stderr) return 2 - merged: list[str] = [] + 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 @@ -106,11 +106,14 @@ def main(argv: list[str] | None = None) -> int: print(f" ? {name}: compare failed ({exc})") continue if compare.get("ahead_by") == 0: - merged.append(name) + merged.append((name, branch["commit"]["sha"])) elif cutoff is not None: - commits = compare.get("commits") or [] - last = commits[-1]["commit"]["committer"]["date"] if commits else None - if last and datetime.fromisoformat(last.replace("Z", "+00:00")) < cutoff: + # 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)}") @@ -120,7 +123,7 @@ def main(argv: list[str] | None = None) -> int: f"{skipped['base']} base." ) print(f"\nMerged into {args.base} (safe to delete): {len(merged)}") - for name in merged: + for name, _sha in merged: print(f" {name}") if cutoff is not None: print( @@ -134,15 +137,34 @@ def main(argv: list[str] | None = None) -> int: 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 - for name in merged: + deleted = 0 + for name, scanned_sha in merged: + encoded = quote(name, safe="") try: - gh_api("DELETE", f"repos/{args.repo}/git/refs/heads/{quote(name, safe='')}") + 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 {len(merged) - failures} of {len(merged)} merged branches.") + print(f"\nDeleted {deleted} of {len(merged)} merged branches.") return 1 if failures else 0 diff --git a/scripts/github_repo_settings.py b/scripts/github_repo_settings.py index abd0d99c..49d1f792 100644 --- a/scripts/github_repo_settings.py +++ b/scripts/github_repo_settings.py @@ -146,6 +146,15 @@ def check_branch_protection( 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 @@ -156,6 +165,8 @@ def check_branch_protection( 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 '-'}" @@ -181,6 +192,7 @@ def check_branch_protection( "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) From 5e0fd2f8408e670111d0b81d381ab124f58fbdfe Mon Sep 17 00:00:00 2001 From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com> Date: Tue, 20 Oct 2026 09:55:13 +0000 Subject: [PATCH 08/14] docs(org): refresh branch-cleanup counts after the 2026-10-20 dry run 310 branches remain (about 240 were deleted by hand since the 2026-10-19 run): 4 fully merged, 3 unmerged and idle for 90 days, 45 backup/ kept, 257 recent unmerged. Counts now come from the branch-tip date, not the compare page. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_015iJCmRk947fokDvpPaTSDf --- docs/github-org-configuration.md | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/docs/github-org-configuration.md b/docs/github-org-configuration.md index 6433efa9..57aecb2a 100644 --- a/docs/github-org-configuration.md +++ b/docs/github-org-configuration.md @@ -29,8 +29,8 @@ organization rows can only be read in the GitHub UI. This turns on "delete branch on merge" and the pull-request requirement on `main` (0 approvals, see below). Secret scanning is already on. -3. **Clean up the 550 branches** once "delete branch on merge" is on, so they - stop accumulating: +3. **Clean up the remaining branches** once "delete branch on merge" is on, + so they stop accumulating: ```bash python scripts/github_branch_cleanup.py # lists what is fully merged @@ -38,13 +38,16 @@ organization rows can only be read in the GitHub UI. 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` (the - `--apply` set); 26 unmerged with no commit in 90 days (oldest from - 2026-04-17, mostly `feat/*-pg` and `codex/*` work that never landed); 45 - `backup/local-2026-10-07/*` kept by prefix; the remaining 375 are unmerged - branches from the last 90 days, nearly all agent-generated. Those need a - decision, not a script: either open the pull request or delete the - branch. The 26 stale ones are the first batch to decide on. + 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; 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 257 + 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. @@ -70,7 +73,7 @@ organization rows can only be read in the GitHub UI. | `main` force push / deletion | Both blocked | Keep | Already correct | | `main` conversation resolution | Required | Keep | Already correct | | `main` requires a pull request | **Off** | On, 0 approvals, code-owner review off | `github_repo_settings.py --apply` | -| Delete branch on merge | **Off** (why 500+ branches exist) | On | `github_repo_settings.py --apply` | +| Delete branch on merge | **Off** (why 550 branches had accumulated by 2026-10-19) | On | `github_repo_settings.py --apply` | | 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) | From b3da4b4c9a71ccda286a79461145919ca0b609d0 Mon Sep 17 00:00:00 2001 From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com> Date: Tue, 20 Oct 2026 10:14:53 +0000 Subject: [PATCH 09/14] docs: point repository links at the agentmiddleware organization namespace Live docs, wrapper and integration READMEs, the SDK release links, the OpenAPI license URL (docs/openapi.json regenerated) and the two operational repo slugs in the Railway and constant-test-loop runbooks now name agentmiddleware/agent-middleware-api. GitHub redirected the old personal namespace, so nothing was broken; the links just said the wrong owner on a now-public repository. Left as they were: server.json (the MCP Registry identity the publisher authenticated as), the historical QA and issue-resolution records under docs/, two historical mentions in docs/tech-debt-remediation-plan.md and FINAL_SUMMARY.md, and the test that pins the old URL as forbidden on public surfaces. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_015iJCmRk947fokDvpPaTSDf --- FINAL_SUMMARY.md | 2 +- README.md | 4 ++-- TROUBLESHOOTING.md | 2 +- app/main.py | 2 +- b2a_sdk/README.md | 4 ++-- docs/GAP_CLOSURE_PLAN.md | 2 +- docs/agentmarket-listing.md | 2 +- docs/agentmarket-submission.md | 4 ++-- docs/awi-adoption-guide.md | 6 +++--- docs/constant-test-loop.md | 2 +- docs/deploy-railway.md | 2 +- docs/openapi.json | 2 +- framework_integrations/README.autogen.md | 2 +- framework_integrations/README.crewai.md | 2 +- framework_integrations/README.langgraph.md | 2 +- framework_integrations/README.llamaindex.md | 2 +- wrappers/autogen-agent-middleware/README.md | 2 +- wrappers/langchain-agent-middleware/README.md | 2 +- wrappers/openai-agent-middleware/README.md | 2 +- 19 files changed, 24 insertions(+), 24 deletions(-) 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 6c226196..453c0839 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 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-BUSL--1.1%20core%20%2F%20MIT%20SDKs-blue) @@ -92,7 +92,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/main.py b/app/main.py index cdd7ffdc..60188359 100644 --- a/app/main.py +++ b/app/main.py @@ -630,7 +630,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/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/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 5b4c889d..8ca38bc6 100644 --- a/docs/deploy-railway.md +++ b/docs/deploy-railway.md @@ -578,7 +578,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/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/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/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 From 41af6f2f5282994cbc96cc5bfb02bef0c59e2193 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 22:32:40 +0000 Subject: [PATCH 10/14] docs(org): record the history rewrite, relicense and their fallout The 2026-10-20 22:19 UTC force-push rewrote every commit to scrub the retired key and replaced LICENSE with Apache 2.0. Consequences recorded: secret_scan's pinned verified-clean base is no longer an ancestor of main (the rewrite push went red on "Scan commit range"), .gitleaks.toml and api-key-rotation.md cite a commit that no longer exists, four documents still describe BSL 1.1, and unrebased branches have no merge base. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_015iJCmRk947fokDvpPaTSDf --- docs/github-org-configuration.md | 67 +++++++++++++++++++++++++------- 1 file changed, 54 insertions(+), 13 deletions(-) diff --git a/docs/github-org-configuration.md b/docs/github-org-configuration.md index 57aecb2a..eb7f4a9e 100644 --- a/docs/github-org-configuration.md +++ b/docs/github-org-configuration.md @@ -150,13 +150,13 @@ roughly 10,000 to 15,000 minutes, or $65 to $105 of overage. **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 the BUSL -license, a `SECURITY.md`, and tests asserting that public surfaces link to -`github.com/agentmiddleware/agent-middleware-api`. The commit history is now -public too, including the 2026-08-06 key that was rotated dead the next day -and is deliberately published in [api-key-rotation.md](api-key-rotation.md); -every other historical finding is allowlisted synthetic material -(`.gitleaks.toml`). +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) to replace +the 2026-08-06 retired key value with `***REMOVED***`, and replaced `LICENSE` +(Business Source License 1.1) with the Apache License 2.0 in commit `861d46c`. +The fallout is listed under [After the history rewrite](#after-the-history-rewrite). The two options not taken, kept for the record: @@ -168,9 +168,49 @@ The two options not taken, kept for the record: 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` still has no green push run for `railway-deploy.yml` to gate on. -`ci.yml` has no manual trigger, so merge the next pull request (this one will -do) and confirm its push run is green before preparing a release. +`main` had green push runs again on 2026-10-20 (12:01 and 12:26 UTC), 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 other jobs cancelled. `ci.yml` has +no manual trigger, so the next merge (this pull request will do) is what gives +`railway-deploy.yml` a green push run to gate on; a normal merge carries a +known `before` SHA, so its `secret_scan` range is well defined. + +## After the history rewrite + +The 2026-10-20 22:19 UTC force-push replaced every commit on `main` (1,404 +of them) and on this branch. These are the consequences found so far; none +is fixed by this pull request because each is a decision for the maintainer. + +- **`secret_scan` fails closed on `main`'s rewrite push.** + `scripts/secret_scan_range.sh` pins `dd16051` (#811) as the verified-clean + base for the fallback range used when the push's old tip is unknown. That + SHA is no longer an ancestor of `main`, so the "Scan commit range" step + failed on run 39507256676. Normal pushes are unaffected (they carry a real + `before` SHA). To restore the fallback: run a full-history scan on a fresh + clone (`gitleaks detect --config .gitleaks.toml --log-opts=--all`), confirm + it is clean, then re-pin `VERIFIED_BASE` to the rewritten commit of #811 + (`git log --oneline origin/main --grep "#811"`) and update any SHA + `tests/test_secret_scan_range.py` asserts. +- **Two documents now describe the old history.** The scope note in + `.gitleaks.toml` and the "Correction (2026-09-19)" section of + [api-key-rotation.md](api-key-rotation.md) say the retired key is reachable + from `main` through commit `9e45009`. After the rewrite neither the value + nor that commit exists on `main`. Rewrite both to say the value was scrubbed + on 2026-10-20 (and that the key was already dead at the provider since + 2026-08-07, which was always the real remediation). +- **The license documents disagree with `LICENSE`.** `LICENSE` is Apache + License 2.0 since `861d46c`; `LICENSING.md`, `CONTRIBUTING.md` ("Licensing + of contributions"), `GOVERNANCE.md` and the `README.md` badge and licensing + paragraph still say Business Source License 1.1 for the core. If the + relicense is intended, update those four; if not, restore `LICENSE`. Either + way the change should be a deliberate commit, because it decides whether a + competitor may host the gateway as a service. +- **Every clone and unmerged branch is on dead history.** A branch that has + not been rebased onto the rewritten `main` has no merge base with it, so + GitHub shows its pull request as tens of thousands of lines changed (this + pull request showed 94,797 until its branch was rewritten too). + `github_branch_cleanup.py` is unaffected in the safe direction: such + branches compare as "ahead", so `--apply` never deletes them. ## Secret scanning @@ -191,9 +231,10 @@ Three layers now cover the same risk, and all three stay: - The CI `secret_scan` job scans the working tree and the pushed commit range and is a required check on `main`. -Alerts from GitHub's scanner appear under the repository's Security tab. -Expect the 2026-08-06 retired key to be flagged from history; close that -alert as "revoked" with a link to [api-key-rotation.md](api-key-rotation.md). +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 From 846536cbfe971da10308e99f94a8840d29fbd06b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 22:01:27 +0000 Subject: [PATCH 11/14] fix(ci): re-pin the secret-scan base after the history rewrite The 2026-10-20 history rewrite changed every SHA, so the verified-clean base scripts/secret_scan_range.sh pinned for its missing-base fallback (dd16051, #811) no longer existed: the fallback failed closed on the rewrite push and test_pinned_verified_base_is_on_this_history skipped instead of asserting. A full-history gitleaks 8.21.2 scan of a fresh clone of the rewritten repository (every branch, 1,410 commits) on 2026-10-22 found only two non-secrets, now allowlisted: the ***REMOVED*** marker the rewrite left in place of the retired 2026-08-06 key, and a synthetic key in a site test on an unmerged branch. VERIFIED_BASE is re-pinned to 9289867, the main tip that scan covered; the scope note in .gitleaks.toml, the secret_scan comment in ci.yml and docs/api-key-rotation.md now describe the rewritten history instead of claiming the key is still reachable through a commit that is gone. docs/github-org-configuration.md is updated to the state on 2026-10-22: the repository was unarchived, delete-branch-on-merge is on, every branch shares history with the rewritten main, and the items above are fixed rather than open. The license-document drift stays open as a maintainer decision. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_015iJCmRk947fokDvpPaTSDf --- .github/workflows/ci.yml | 6 +- .gitleaks.toml | 35 +++-- docs/api-key-rotation.md | 35 ++++- docs/github-org-configuration.md | 213 +++++++++++++++++++------------ scripts/secret_scan_range.sh | 13 +- 5 files changed, 198 insertions(+), 104 deletions(-) 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..514602e6 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -1,19 +1,20 @@ # 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 +# 2026-08-06 leaked key (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) +# exists in this repository's history any more. Clones and forks taken before +# 2026-10-20 still carry both. Every other historical finding is synthetic +# test material or a content hash, allowlisted below. [extend] useDefault = true @@ -47,6 +48,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 +99,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/docs/api-key-rotation.md b/docs/api-key-rotation.md index ddc07edf..e4370eb1 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 | `***REMOVED***` | Hardcoded in `scripts/stress_test_live.py`, reachable on public `main`. 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 — see warning below). 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,34 @@ 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 the maintainer force-pushed a rewrite of the whole repository +(every commit SHA changed, every branch included) that 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 no longer exists. +- A full-history gitleaks scan (`gitleaks detect --log-opts=--all` under + `.gitleaks.toml`, every branch, 1,410 commits) reports nothing outside the + allowlist. Its only findings were the `***REMOVED***` marker itself, in the + 2026-08-04 commit of `scripts/stress_test_live.py`, and a synthetic + `amw_demo_secret_value` in a site test on an unmerged branch; both are + allowlisted as non-secrets. +- `scripts/secret_scan_range.sh` now pins the `main` tip that scan covered as + its verified-clean base; the previous pin was a SHA that no longer exists. + +What this changes in what to claim: the value is no longer in this +repository's history. It is still in every clone and fork taken before +2026-10-20, and it was published in plaintext in this file from 2026-08-06 +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, rotated within a day, verified dead, removed from the +repository's history on 2026-10-20, and still present in pre-rewrite copies. + ## Rotation procedure (Railway) Before changing keys, qualify the exact-SHA release and a schema-compatible diff --git a/docs/github-org-configuration.md b/docs/github-org-configuration.md index eb7f4a9e..96b83166 100644 --- a/docs/github-org-configuration.md +++ b/docs/github-org-configuration.md @@ -6,19 +6,40 @@ 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. Re-run the two scripts below to refresh the repository rows; the -organization rows can only be read in the GitHub UI. +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: this repository was archived on 2026-10-20 (recorded in +that day's update of this document) and unarchived again by 2026-10-21 19:15 +UTC, when pull request 842 was opened; GitHub does not record when. Since +then `main` has merged ten pull requests (842 to 851, between 2026-10-21 +22:08 and 2026-10-22 07:04 UTC: 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). One of them, 850, added a 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", so the banner states the intent +and the archive itself is deferred until the open site pull requests land. +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 it is archived. ## Do these first, in this order -1. **Done 2026-10-20: the repositories are public.** That removes the - Actions minutes bill on standard runners and switched on secret scanning, - push protection and private vulnerability reporting by itself. Two - follow-ups: watch the push run of the next `main` merge (if the no-name - `startup_failure` in [Actions budget](#actions-budget) recurs on a public - repository, billing was not the cause), and fix the descriptions that - still say "private" on public repositories (see - [Repositories](#repositories)). +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 and `amw-demo`'s descriptions are fixed; re-read the + others (see [Repositories](#repositories)). 2. **Apply the repository settings** from a machine where `gh auth status` shows your account: @@ -27,10 +48,14 @@ organization rows can only be read in the GitHub UI. python scripts/github_repo_settings.py --apply ``` - This turns on "delete branch on merge" and the pull-request requirement on - `main` (0 approvals, see below). Secret scanning is already on. -3. **Clean up the remaining branches** once "delete branch on merge" is on, - so they stop accumulating: + "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 @@ -41,12 +66,12 @@ organization rows can only be read in the GitHub UI. 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; 4 fully merged (the `--apply` - set); 3 unmerged with no commit in 90 days + 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 257 - 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 + `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) @@ -60,8 +85,8 @@ organization rows can only be read in the GitHub UI. |---|---|---| | 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 | 6. Since 2026-10-20 five are public; `agent-middleware-public` is still private. Three public repositories still describe themselves as private | Decide which repository is the product repository and fix the descriptions; 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. Scheduled, Dependabot and most pull-request runs succeed | Green push run on every `main` commit; confirm on the first merge after going public | +| 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 the product repository, 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 the run for `9289867`, which was cancelled, not failed, while its Docker publish run 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` @@ -72,16 +97,16 @@ organization rows can only be read in the GitHub UI. | `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, 0 approvals, code-owner review off | `github_repo_settings.py --apply` | -| Delete branch on merge | **Off** (why 550 branches had accumulated by 2026-10-19) | On | `github_repo_settings.py --apply` | +| `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 weekly pull requests; Docker digest, SDK, wrappers and Railway IaC covered | Done in `.github/dependabot.yml` on this branch | -| Rulesets | None | None needed; the classic rule above covers `main` | – | -| `master` branch | Exists, protected, 9 commits ahead and 1,239 behind `main`, last commit 2026-05-22 | Deleted | See [Remove master](#remove-master) | +| 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. @@ -155,8 +180,10 @@ those on by itself at the flip. The repository already carried a `github.com/agentmiddleware/agent-middleware-api`. Later the same day the maintainer rewrote the entire history (every commit SHA changed) to replace the 2026-08-06 retired key value with `***REMOVED***`, and replaced `LICENSE` -(Business Source License 1.1) with the Apache License 2.0 in commit `861d46c`. -The fallout is listed under [After the history rewrite](#after-the-history-rewrite). +(Business Source License 1.1) with the Apache License 2.0 in commit `861d46c`, +and archived the repository; it was unarchived again by 2026-10-21 and stays +so while site pull requests are open (pull request 850). The fallout is +listed under [After the history rewrite](#after-the-history-rewrite). The two options not taken, kept for the record: @@ -170,47 +197,59 @@ The two options not taken, kept for the record: `main` had green push runs again on 2026-10-20 (12:01 and 12:26 UTC), 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 other jobs cancelled. `ci.yml` has -no manual trigger, so the next merge (this pull request will do) is what gives -`railway-deploy.yml` a green push run to gate on; a normal merge carries a -known `before` SHA, so its `secret_scan` range is well defined. +red on `secret_scan` (see below) with the other jobs cancelled. 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); the run for +`9289867` on 2026-10-22 was cancelled rather than failed. `ci.yml` still has +no manual trigger, so a cancelled push run can only be replaced by the next +merge. ## After the history rewrite The 2026-10-20 22:19 UTC force-push replaced every commit on `main` (1,404 -of them) and on this branch. These are the consequences found so far; none -is fixed by this pull request because each is a decision for the maintainer. - -- **`secret_scan` fails closed on `main`'s rewrite push.** - `scripts/secret_scan_range.sh` pins `dd16051` (#811) as the verified-clean - base for the fallback range used when the push's old tip is unknown. That - SHA is no longer an ancestor of `main`, so the "Scan commit range" step - failed on run 39507256676. Normal pushes are unaffected (they carry a real - `before` SHA). To restore the fallback: run a full-history scan on a fresh - clone (`gitleaks detect --config .gitleaks.toml --log-opts=--all`), confirm - it is clean, then re-pin `VERIFIED_BASE` to the rewritten commit of #811 - (`git log --oneline origin/main --grep "#811"`) and update any SHA - `tests/test_secret_scan_range.py` asserts. -- **Two documents now describe the old history.** The scope note in - `.gitleaks.toml` and the "Correction (2026-09-19)" section of - [api-key-rotation.md](api-key-rotation.md) say the retired key is reachable - from `main` through commit `9e45009`. After the rewrite neither the value - nor that commit exists on `main`. Rewrite both to say the value was scrubbed - on 2026-10-20 (and that the key was already dead at the provider since - 2026-08-07, which was always the real remediation). -- **The license documents disagree with `LICENSE`.** `LICENSE` is Apache - License 2.0 since `861d46c`; `LICENSING.md`, `CONTRIBUTING.md` ("Licensing - of contributions"), `GOVERNANCE.md` and the `README.md` badge and licensing - paragraph still say Business Source License 1.1 for the core. If the - relicense is intended, update those four; if not, restore `LICENSE`. Either - way the change should be a deliberate commit, because it decides whether a - competitor may host the gateway as a service. -- **Every clone and unmerged branch is on dead history.** A branch that has - not been rebased onto the rewritten `main` has no merge base with it, so - GitHub shows its pull request as tens of thousands of lines changed (this - pull request showed 94,797 until its branch was rewritten too). - `github_branch_cleanup.py` is unaffected in the safe direction: such - branches compare as "ahead", so `--apply` never deletes them. +of them) and, as it turned out, on every other branch too. These were the +consequences; the state on 2026-10-22 is given for each. + +- **`secret_scan` failed closed on `main`'s rewrite push. Fixed on this + branch.** `scripts/secret_scan_range.sh` pinned `dd16051` (#811) as the + verified-clean base for the fallback range used when a push's old tip is + unknown; that SHA stopped existing, so the "Scan commit range" step failed + on run 39507256676 and `test_pinned_verified_base_is_on_this_history` + silently skipped (it treats a missing pin as a shallow checkout). Normal + pushes were unaffected because they carry a real `before` SHA. On + 2026-10-22 a full-history scan of a fresh clone (gitleaks 8.21.2, the + release CI pins, `--log-opts=--all`, 284 branches, 1,410 commits scanned) found two + things, both non-secrets and now allowlisted in `.gitleaks.toml`: the + `***REMOVED***` marker the rewrite left where the retired key's value had + been in the 2026-08-04 commit of `scripts/stress_test_live.py`, and a + synthetic `amw_demo_secret_value` in `site/test_demo_key.mjs` on the + unmerged `devops/demo-key-utm` branch. `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 the value nor that + commit exists any more. 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 @@ -231,6 +270,11 @@ Three layers now cover the same risk, and all three stay: - 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 @@ -238,8 +282,10 @@ 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`. It -is 1,239 commits behind and carries 9 commits that never reached `main`. +`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 and +carried 9 commits that never reached `main`; the first command below gives +the current numbers. Protected branches cannot be deleted through the branch list, so: ```bash @@ -268,10 +314,11 @@ with, so it is a checklist, not a script. | 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; this stops a future workflow from pulling an unpinned third-party action | | 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 a fork's workflow run would otherwise 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 six repository pages; also covers the one repository still private | +| 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 | -| Profile | Verified domain | Add `thisisatest.tech` (the repository homepage) | Shows the "verified" badge next to the organization | +| 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 @@ -279,27 +326,31 @@ 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 require only a pull request. -| Repository | Visibility (2026-10-20) | Notes | +| Repository | Visibility (2026-10-22) | Notes | |---|---|---| -| `agent-middleware-api` | Public | This document | -| `agent-middleware` | Public | 45 open issues. Description still says "pre-release, private": fix it. If it is the successor of this repository, say so in both READMEs; if not, archive one of them | -| `agent-middleware-public` | **Private** | The only one left private, and the one whose name says public. Description still says "private staging repo". Flip it or rename it so the name is true | -| `agent-middleware-spec` | Public | Confirm secret scanning and private vulnerability reporting are on (GitHub enables them on public repositories, but check the Security tab once) | -| `amw-demo` | Public | Description still says "private until approved": fix it. Same Security-tab check | +| `agent-middleware-api` | Public | This document. Archived 2026-10-20, unarchived by 2026-10-21; the #850 banner announces the archive, which is deferred while site pull requests are open (see the top of this document). Description fixed | +| `agent-middleware` | Public | The product repository, 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`, `dependabot.yml` for its own Actions pins | +| `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 | Archived on 2026-10-20; its content moved to `agent-middleware/spec` | +| `amw-demo` | Public | Description said "private until approved" on 2026-10-19 and was fixed by 2026-10-20. Security-tab check once | | `amw-paypal-agent-demo` | Public | Same Security-tab check | ## Stale references still in the tree -These point at the old personal namespace. GitHub redirects them, so nothing -is broken, and they are listed here rather than changed on this branch: +Links to the old personal namespace are fixed on this branch: `README.md` +(CI badge and clone URL), `TROUBLESHOOTING.md` (bug-report link), +`FINAL_SUMMARY.md`, `app/main.py` (the OpenAPI license URL, with +`docs/openapi.json` regenerated), `b2a_sdk/README.md`, the framework and +wrapper READMEs, 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`. GitHub redirects +the old URLs, so nothing was broken; they were wrong in the way a security +questionnaire notices. One is deliberately left: -- `README.md` CI badge and clone URL, `TROUBLESHOOTING.md` bug-report link, - `docs/awi-adoption-guide.md`, `docs/agentmarket-listing.md`, the framework - and wrapper READMEs, `b2a_sdk/README.md` release links, and the LICENSE URL - in `app/main.py`. - `server.json` names the MCP Registry entry `io.github.PetrefiedThunder/...`. That is the registry identity the publisher authenticated as; changing it is - a registry re-submission, not a link fix. Leave it. + a registry re-submission, not a link fix. ## What this document does not do 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() { From fd1c42c237a9ef1e29911f8b66a487ecb072c1d4 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 22:18:31 +0000 Subject: [PATCH 12/14] docs: runbook corrections from the 2026-10-22 fact-check Two main push runs were cancelled after the rewrite, not one (7408143 had only test (3.11) cancelled, 25 minutes in); the README banner names agent-middleware as where development continues, not as "the product repository"; the Dependabot Docker and Railway entries are monthly, not weekly. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_015iJCmRk947fokDvpPaTSDf --- docs/github-org-configuration.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/github-org-configuration.md b/docs/github-org-configuration.md index 96b83166..011913e5 100644 --- a/docs/github-org-configuration.md +++ b/docs/github-org-configuration.md @@ -85,8 +85,8 @@ treats this repository as live until it is archived. |---|---|---| | 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 the product repository, 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 the run for `9289867`, which was cancelled, not failed, while its Docker publish run succeeded | Green push run on every `main` commit | +| 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` @@ -104,7 +104,7 @@ treats this repository as live until it is archived. | 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 weekly pull requests; Docker digest, SDK, wrappers and Railway IaC covered | Done in `.github/dependabot.yml` on this branch | +| 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 | | 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 | @@ -199,10 +199,11 @@ The two options not taken, kept for the record: the force-push of the rewritten history at 22:19 UTC produced a run that is red on `secret_scan` (see below) with the other jobs cancelled. 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); the run for -`9289867` on 2026-10-22 was cancelled rather than failed. `ci.yml` still has -no manual trigger, so a cancelled push run can only be replaced by the next -merge. +`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 can only be replaced by +the next merge. ## After the history rewrite @@ -329,7 +330,7 @@ per check its CI reports, or leave it empty to require only a pull request. | Repository | Visibility (2026-10-22) | Notes | |---|---|---| | `agent-middleware-api` | Public | This document. Archived 2026-10-20, unarchived by 2026-10-21; the #850 banner announces the archive, which is deferred while site pull requests are open (see the top of this document). Description fixed | -| `agent-middleware` | Public | The product repository, per this repository's README banner. 45 open issues on 2026-10-20 | +| `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`, `dependabot.yml` for its own Actions pins | | `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 | Archived on 2026-10-20; its content moved to `agent-middleware/spec` | From 508c8f33b80acd12dd986da2359e70d04982a684 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 22:59:07 +0000 Subject: [PATCH 13/14] fix(db): release failed SQLite statements on the worker thread (Python 3.11 stall) test (3.11) on this branch's fd1c42c stopped producing output at 35% and was cancelled by the 25-minute job timeout; main's run for 7408143 (#847) shows the same. Through CPython 3.11 a sqlite3 cursor whose execute() raised keeps its prepared statement attached, and destroying that cursor later calls sqlite3_reset() under the connection mutex. aiosqlite runs statements on a worker thread that holds that mutex for the whole busy_timeout wait, so when the cyclic GC destroys such a cursor on the event-loop thread during a concurrent write, the loop freezes for up to 30 s and the lock holder, which can only commit from the loop, never does. get_engine() now passes a sqlite3.Connection factory (SQLite URLs only) whose cursors close themselves on the worker thread when execute or executemany raises, which releases the statement where the mutex is already held. CPython 3.12+ detaches the statement itself, so the factory is a no-op there. The new test reproduces the mechanism with two aiosqlite connections and a 10 s busy timeout: without the factory on 3.11 the main thread blocks 9.85 s and the write fails with "database is locked"; with it, both succeed. Same change as the one prepared for agentmiddleware/agent-middleware, whose gateway has the identical engine block. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_015iJCmRk947fokDvpPaTSDf --- app/db/database.py | 61 ++++++++++- tests/test_sqlite_failed_cursor_release.py | 117 +++++++++++++++++++++ 2 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 tests/test_sqlite_failed_cursor_release.py 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/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" + ) From e404e6d928259b2247c5d01b180607dfd118a085 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 22:59:07 +0000 Subject: [PATCH 14/14] docs: second round of fact-check corrections The rewrite push's secret_scan failed because its range ran from the old tip to the new one with no common ancestor and hit the ***REMOVED*** marker, not through the stale fallback pin; the stale pin would only have mattered on a push without a usable before SHA. The repository 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. GitHub still serves the pre-rewrite commit 9e45009 by SHA and pull request 448 quotes the retired value, so the scope note and rotation record say "no longer reachable", not "gone". Actions budget figures recomputed from the runs API (80 CI runs and about 5,000 billed minutes for 2026-10-15 to 17; 709 runs in the 30 days to 2026-10-19). Smaller corrections to the master, organization-settings, repositories and stale-reference sections, and the test (3.11) stall row. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_015iJCmRk947fokDvpPaTSDf --- .gitleaks.toml | 17 ++- docs/api-key-rotation.md | 53 ++++--- docs/github-org-configuration.md | 238 +++++++++++++++++++------------ 3 files changed, 189 insertions(+), 119 deletions(-) diff --git a/.gitleaks.toml b/.gitleaks.toml index 514602e6..1d5d1fd6 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -5,16 +5,21 @@ # 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 -# 2026-08-06 leaked key (see docs/api-key-rotation.md). It was rotated dead at +# 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) -# exists in this repository's history any more. Clones and forks taken before -# 2026-10-20 still carry both. Every other historical finding is synthetic -# test material or a content hash, allowlisted below. +# 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 diff --git a/docs/api-key-rotation.md b/docs/api-key-rotation.md index e4370eb1..b62b63ea 100644 --- a/docs/api-key-rotation.md +++ b/docs/api-key-rotation.md @@ -24,7 +24,7 @@ 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 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 — 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` @@ -75,31 +75,44 @@ credibility is the product. ### Update (2026-10-20): the history was rewritten after all -On 2026-10-20 the maintainer force-pushed a rewrite of the whole repository -(every commit SHA changed, every branch included) that 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 +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 no longer exists. + 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. Its only findings were the `***REMOVED***` marker itself, in the - 2026-08-04 commit of `scripts/stress_test_live.py`, and a synthetic - `amw_demo_secret_value` in a site test on an unmerged branch; both are - allowlisted as non-secrets. -- `scripts/secret_scan_range.sh` now pins the `main` tip that scan covered as - its verified-clean base; the previous pin was a SHA that no longer exists. - -What this changes in what to claim: the value is no longer in this -repository's history. It is still in every clone and fork taken before -2026-10-20, and it was published in plaintext in this file from 2026-08-06 -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 + 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, rotated within a day, verified dead, removed from the -repository's history on 2026-10-20, and still present in pre-rewrite copies. +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) diff --git a/docs/github-org-configuration.md b/docs/github-org-configuration.md index 011913e5..b1b276d6 100644 --- a/docs/github-org-configuration.md +++ b/docs/github-org-configuration.md @@ -10,23 +10,24 @@ 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: this repository was archived on 2026-10-20 (recorded in -that day's update of this document) and unarchived again by 2026-10-21 19:15 -UTC, when pull request 842 was opened; GitHub does not record when. Since -then `main` has merged ten pull requests (842 to 851, between 2026-10-21 -22:08 and 2026-10-22 07:04 UTC: 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). One of them, 850, added a 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", so the banner states the intent -and the archive itself is deferred until the open site pull requests land. +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 it is archived. +treats this repository as live until the maintainer archives it for good. ## Do these first, in this order @@ -38,8 +39,8 @@ treats this repository as live until it is archived. 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 and `amw-demo`'s descriptions are fixed; re-read the - others (see [Repositories](#repositories)). + 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: @@ -105,6 +106,7 @@ treats this repository as live until it is archived. | 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 | @@ -122,22 +124,27 @@ and turn on code-owner review the day a second maintainer has commit access. - Open issue 805 (2026-10-14) reports `startup_failure` on this repository and attributes it to an organization billing or policy setting. -- Commits on 2026-10-15 are titled "retrigger checks after billing lock - lifted"; pull request 816 (merged 2026-10-17) is titled "after billing - lock". So the lock has happened at least twice this month. -- The workflow files did not change between the last green push run - (`44cd144`, 2026-10-16) and the current head, which rules out a broken - workflow file. +- 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, clustered on 2026-10-14 - 19:54 to 21:26, 2026-10-17 20:19 to 20:35, 2026-10-19 11:23 to 11:28 and - 2026-10-19 21:26 to 21:28 UTC, on push and pull-request events alike, - including the push of every `main` commit since 2026-10-17. Between those - windows (for example this pull request's runs on 2026-10-19 07:30 and - 2026-10-20 08:50) everything starts normally. The API does not expose the + 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 one and read it: + latest `main` push one and read it: . A spending-limit message confirms the billing explanation; anything else changes the fix. @@ -156,19 +163,22 @@ 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.5 + 17.3 + 10.3 | +| `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`, `lint`, `invariant_attack_proof`, `production_trust`, `secret_scan`, `constant_test_loop` | about 1 each | +| `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 75 | -| Plus `Failure Lab` (fast tier on pull requests, slow tier on `main`) and `docker-publish` | 3 to 10 | +| 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 about 30 CI runs, which is over -2,000 billed minutes in three days. The Free plan includes 2,000 minutes a +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 is -roughly 10,000 to 15,000 minutes, or $65 to $105 of overage. +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 @@ -178,11 +188,15 @@ 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) to replace -the 2026-08-06 retired key value with `***REMOVED***`, and replaced `LICENSE` -(Business Source License 1.1) with the Apache License 2.0 in commit `861d46c`, -and archived the repository; it was unarchived again by 2026-10-21 and stays -so while site pull requests are open (pull request 850). The fallout is +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: @@ -195,42 +209,57 @@ The two options not taken, kept for the record: 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 (12:01 and 12:26 UTC), 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 other jobs cancelled. The merges of +`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 can only be replaced by -the next merge. +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,404 -of them) and, as it turned out, on every other branch too. These were the +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 closed on `main`'s rewrite push. Fixed on this - branch.** `scripts/secret_scan_range.sh` pinned `dd16051` (#811) as the - verified-clean base for the fallback range used when a push's old tip is - unknown; that SHA stopped existing, so the "Scan commit range" step failed - on run 39507256676 and `test_pinned_verified_base_is_on_this_history` - silently skipped (it treats a missing pin as a shallow checkout). Normal - pushes were unaffected because they carry a real `before` SHA. On - 2026-10-22 a full-history scan of a fresh clone (gitleaks 8.21.2, the - release CI pins, `--log-opts=--all`, 284 branches, 1,410 commits scanned) found two - things, both non-secrets and now allowlisted in `.gitleaks.toml`: the - `***REMOVED***` marker the rewrite left where the retired key's value had - been in the 2026-08-04 commit of `scripts/stress_test_live.py`, and a - synthetic `amw_demo_secret_value` in `site/test_demo_key.mjs` on the - unmerged `devops/demo-key-utm` branch. `VERIFIED_BASE` is re-pinned to - `9289867`, the `main` tip that scan covered, and the test runs again. +- **`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 the value nor that - commit exists any more. The scope note now records the 2026-10-22 scan, the + 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. @@ -257,8 +286,10 @@ consequences; the state on 2026-10-22 is given for each. 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). `github_repo_settings.py` reports them and would re-enable them if -they were ever switched off. +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: @@ -284,9 +315,11 @@ 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 and -carried 9 commits that never reached `main`; the first command below gives -the current numbers. +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 @@ -302,9 +335,11 @@ that no longer exists. ## Organization settings -Everything here is Organization → Settings. None of it is readable or -writable through the repository-scoped token this document was produced -with, so it is a checklist, not a script. +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 | |---|---|---|---| @@ -312,9 +347,9 @@ with, so it is a checklist, not a script. | 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; this stops a future workflow from pulling an unpinned third-party action | +| 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 a fork's workflow run would otherwise start on its own | +| 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 | @@ -325,33 +360,45 @@ with, so it is a checklist, not a script. 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 require only a pull request. +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. Archived 2026-10-20, unarchived by 2026-10-21; the #850 banner announces the archive, which is deferred while site pull requests are open (see the top of this document). Description fixed | +| `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`, `dependabot.yml` for its own Actions pins | +| `.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 | Archived on 2026-10-20; its content moved to `agent-middleware/spec` | -| `amw-demo` | Public | Description said "private until approved" on 2026-10-19 and was fixed by 2026-10-20. Security-tab check once | +| `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: `README.md` +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`, `app/main.py` (the OpenAPI license URL, with -`docs/openapi.json` regenerated), `b2a_sdk/README.md`, the framework and -wrapper READMEs, 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`. GitHub redirects -the old URLs, so nothing was broken; they were wrong in the way a security -questionnaire notices. One is deliberately left: - -- `server.json` names the MCP Registry entry `io.github.PetrefiedThunder/...`. - That is the registry identity the publisher authenticated as; changing it is - a registry re-submission, not a link fix. +`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 @@ -361,5 +408,10 @@ questionnaire notices. One is deliberately left: - 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 does not change `ci.yml`. The per-push cost reduction above is a - separate, reviewable change. +- 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.