From ec42d1df489cc28cca30b721a1eb397b1f184b59 Mon Sep 17 00:00:00 2001 From: rocklambros Date: Mon, 21 Sep 2026 10:38:47 -0600 Subject: [PATCH 1/2] Audit workflows with zizmor, after fixing everything it finds today CodeQL default setup was turned on for this repository on 2026-09-21 and covers the Actions language. This adds zizmor, which the [OWASP GitHub Actions Security Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/GitHub_Actions_Security_Cheat_Sheet.html) recommends beside CodeQL for defense in depth. zizmor reads each workflow's logic and checks what CodeQL does not: hash pins no tag points to, version comments that disagree with their pin, App tokens broader than the job needs, and credentials a checkout leaves in `.git/config`. Run against `integration` before this change with online audits on, zizmor 1.30.1 reported 15 findings. This clears all of them, so the Security tab starts clean. - **`board-reconcile.yml`, 2 high.** The App installation token inherited the installation's whole grant and every repository it covers. It now names this repository and the four permissions the reconciler uses. The installation is that narrow today, but its settings live outside this repository and were over-provisioned once already, with `repository_projects: write` removed on 2026-09-21. With the grant stated in the workflow, a wider installation still mints a narrow token. The repository is named literally because a scheduled run carries no repository in its event payload. - **`pr-intake.yml`, 1 high.** zizmor flags every `pull_request_target`. This one checks out no code and reads the pull request body as data through an environment variable, which the workflow's header already explains. It carries an inline `zizmor: ignore` that points at that explanation. - **Eleven checkouts across nine workflows, 11 low.** Each left the job's token in `.git/config`. None of those jobs pushes with git. They call `gh` with `GH_TOKEN` or touch no network at all, and `sync_version.yml` hands its push to `create-pull-request`, which configures its own token. All eleven now set `persist-credentials: false`. The checkout in `validate-owasp-metadata.yaml` also gains the `# v7.0.1` comment every other pin carries. - **`reference-implementation.yml`, 1 medium.** The setup-bun pin `735343b` is v2.0.2, and its comment said v2.0.1. Only the comment changes. Dependabot's #83 already treats it as 2.0.2. The new workflow runs zizmor from a `zizmor` dependency group in `uv.lock`, pinned with hashes. No new action enters the Actions allowlist, and Dependabot's uv ecosystem moves zizmor under the existing 7-day cooldown. The group is not a default group, so `uv sync --locked` in the deploy and test jobs never installs it. A default sync against this lockfile would uninstall it. It runs on pull requests and pushes that touch `.github/`, an action definition, `pyproject.toml`, or `uv.lock`, and weekly, because the online audits compare pins against advisories published after the pin. Findings upload as SARIF under the category `zizmor`, and the job stays green on findings so code scanning tracks them and a ruleset can gate on them later. A tool or upload failure still fails the job. A pull request from a fork cannot upload with its read-only token, so it gets annotations instead. `upload-sarif` is pinned at v4.38.0 because v4.38.1 was three days old, inside the cooldown this repository applies to its dependencies. One change cannot be verified before merge. The `board-automation` environment releases its key only to `integration`, so a dry-run dispatch from this branch cannot mint a token. After merge, dispatch `Reconcile project board` on `integration` with apply off. A wrong scope fails the token request loudly, and the next nightly run is the one at risk, so the dry run belongs on the same day. Open pull requests #162 and #169 add workflows that zizmor will flag once they land, for unset `persist-credentials` and the same `# v2.0.1` setup-bun comment. Verified locally: zizmor 1.30.1 with online audits reports no findings, with 1 ignored and 26 low-confidence findings suppressed by the default persona. The workflow's own command produces SARIF 2.1.0 with zero results. `actionlint` is clean. `uv lock --check` passes under uv 0.9.9, the version CI pins, and the lockfile diff adds only zizmor. `uv run pytest` gives 278 passed, 1 skipped. Signed-off-by: Rock Lambros --- .github/workflows/board-reconcile.yml | 10 +++ .github/workflows/deploy-pages.yml | 12 +++ .github/workflows/monitor-pages.yml | 3 + .github/workflows/open-promotion.yml | 3 + .github/workflows/pr-base-guard.yml | 3 + .github/workflows/pr-intake.yml | 3 +- .../workflows/reference-implementation.yml | 6 +- .github/workflows/scope-review.yml | 4 + .github/workflows/sync-integration.yml | 3 + .github/workflows/sync_version.yml | 4 + .../workflows/validate-owasp-metadata.yaml | 6 +- .github/workflows/zizmor.yml | 90 +++++++++++++++++++ pyproject.toml | 4 + uv.lock | 22 +++++ 14 files changed, 170 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/zizmor.yml diff --git a/.github/workflows/board-reconcile.yml b/.github/workflows/board-reconcile.yml index 1fe2434..37ade31 100644 --- a/.github/workflows/board-reconcile.yml +++ b/.github/workflows/board-reconcile.yml @@ -101,6 +101,16 @@ jobs: # pull requests, which is the whole grant. It cannot write code, change # settings, or reach any other repository. owner: ${{ github.repository_owner }} + # The same grant, stated again here so the token cannot outgrow it. The + # installation's settings live outside this repository and have drifted once + # already. With these lines a wider installation still mints a narrow token. + # The repository is named literally because a scheduled run carries no + # repository in its event payload. A rename fails the token request loudly. + repositories: agent-control-standard + permission-organization-projects: write + permission-issues: read + permission-pull-requests: read + permission-metadata: read - name: Reconcile the board env: diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml index b3dc20b..378e312 100644 --- a/.github/workflows/deploy-pages.yml +++ b/.github/workflows/deploy-pages.yml @@ -27,6 +27,10 @@ jobs: steps: - name: Check out the repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # Nothing here pushes with git, so the checkout's token has no reason to + # stay behind in .git/config. + persist-credentials: false - name: Install uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 @@ -63,6 +67,10 @@ jobs: - name: Check out the repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # Nothing here pushes with git, so the checkout's token has no reason to + # stay behind in .git/config. + persist-credentials: false - name: Install uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 @@ -123,6 +131,10 @@ jobs: steps: - name: Check out the repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # Nothing here pushes with git, so the checkout's token has no reason to + # stay behind in .git/config. + persist-credentials: false - name: Deploy to Pages id: deployment diff --git a/.github/workflows/monitor-pages.yml b/.github/workflows/monitor-pages.yml index bc59140..8006c21 100644 --- a/.github/workflows/monitor-pages.yml +++ b/.github/workflows/monitor-pages.yml @@ -22,6 +22,9 @@ jobs: - name: Check out the repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + # Nothing here pushes with git, so the checkout's token has no reason to + # stay behind in .git/config. + persist-credentials: false # A scheduled workflow runs from the default branch. Once the default moves to # integration, checking out the default here would verify integration's schema # set against a site that publishes from main, failing every six hours between diff --git a/.github/workflows/open-promotion.yml b/.github/workflows/open-promotion.yml index a82d419..236d6ff 100644 --- a/.github/workflows/open-promotion.yml +++ b/.github/workflows/open-promotion.yml @@ -23,6 +23,9 @@ jobs: - name: Check out the repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + # Nothing here pushes with git, so the checkout's token has no reason to + # stay behind in .git/config. + persist-credentials: false fetch-depth: 0 - name: Open the promotion pull request if integration is ahead diff --git a/.github/workflows/pr-base-guard.yml b/.github/workflows/pr-base-guard.yml index b3107f3..9b37cb2 100644 --- a/.github/workflows/pr-base-guard.yml +++ b/.github/workflows/pr-base-guard.yml @@ -24,6 +24,9 @@ jobs: - name: Check out the repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + # Nothing here pushes with git, so the checkout's token has no reason to + # stay behind in .git/config. + persist-credentials: false # Both endpoints of the diff have to be present to compute a merge base. fetch-depth: 0 diff --git a/.github/workflows/pr-intake.yml b/.github/workflows/pr-intake.yml index 71d2574..c21b76b 100644 --- a/.github/workflows/pr-intake.yml +++ b/.github/workflows/pr-intake.yml @@ -9,7 +9,8 @@ name: PR intake on: - pull_request_target: + # zizmor flags every pull_request_target. The paragraph above is why this one is safe. + pull_request_target: # zizmor: ignore[dangerous-triggers] types: [opened, edited, reopened] permissions: {} diff --git a/.github/workflows/reference-implementation.yml b/.github/workflows/reference-implementation.yml index 4a93bc1..424d2a9 100644 --- a/.github/workflows/reference-implementation.yml +++ b/.github/workflows/reference-implementation.yml @@ -42,13 +42,17 @@ jobs: steps: - name: Check out the repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # Nothing here pushes with git, so the checkout's token has no reason to + # stay behind in .git/config. + persist-credentials: false # Pinned to an exact version rather than a moving major. The bridge in # packages/agt-bridge/src/opa-path.ts works around a Bun defect measured on a # specific version, so the runtime version is part of this tree's contract # rather than an implementation detail. - name: Install Bun - uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.1 + uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2 with: bun-version: "1.3.11" diff --git a/.github/workflows/scope-review.yml b/.github/workflows/scope-review.yml index 047044d..a063d8a 100644 --- a/.github/workflows/scope-review.yml +++ b/.github/workflows/scope-review.yml @@ -20,6 +20,10 @@ jobs: steps: - name: Check out the repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # Nothing here pushes with git, so the checkout's token has no reason to + # stay behind in .git/config. + persist-credentials: false - name: Open a review issue if the date has passed env: diff --git a/.github/workflows/sync-integration.yml b/.github/workflows/sync-integration.yml index cb4a1f3..2ce0b06 100644 --- a/.github/workflows/sync-integration.yml +++ b/.github/workflows/sync-integration.yml @@ -26,6 +26,9 @@ jobs: - name: Check out the repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + # Nothing here pushes with git, so the checkout's token has no reason to + # stay behind in .git/config. + persist-credentials: false fetch-depth: 0 - name: Open or update the sync pull request diff --git a/.github/workflows/sync_version.yml b/.github/workflows/sync_version.yml index fe29a0f..a31f170 100644 --- a/.github/workflows/sync_version.yml +++ b/.github/workflows/sync_version.yml @@ -27,6 +27,10 @@ jobs: steps: - name: Check out the repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # create-pull-request sets up its own token for the push, so nothing needs + # the checkout's token left behind in .git/config. + persist-credentials: false - name: Install uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 diff --git a/.github/workflows/validate-owasp-metadata.yaml b/.github/workflows/validate-owasp-metadata.yaml index 1aeef36..66dc2eb 100644 --- a/.github/workflows/validate-owasp-metadata.yaml +++ b/.github/workflows/validate-owasp-metadata.yaml @@ -21,7 +21,11 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # Nothing here pushes with git, so the checkout's token has no reason to + # stay behind in .git/config. + persist-credentials: false - name: Validate metadata file uses: owasp/nest-schema/.github/actions/validate@a733198b4a942eb12d3ee8629cd9e0d409b1b2b9 diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml new file mode 100644 index 0000000..259aa72 --- /dev/null +++ b/.github/workflows/zizmor.yml @@ -0,0 +1,90 @@ +# Audits every workflow, composite action, and dependabot.yml with zizmor. +# +# CodeQL's actions analysis covers the common injection paths. zizmor is the defense in +# depth the OWASP GitHub Actions Security Cheat Sheet recommends beside it, and it checks +# what CodeQL does not: hash pins no tag points to, version comments that disagree with +# their pin, App tokens broader than the workflow needs, and credentials a checkout +# leaves behind in .git/config. +# +# Findings go to the Security tab instead of failing this job. Code scanning tracks them +# across runs, and a ruleset can gate merges on them. A tool or upload failure still +# fails the job, so the scan cannot skip itself quietly. +name: zizmor + +on: + pull_request: + paths: + - ".github/**" + - "**/action.yml" + - "**/action.yaml" + - "pyproject.toml" + - "uv.lock" + push: + branches: ["main", "integration"] + paths: + - ".github/**" + - "**/action.yml" + - "**/action.yaml" + - "pyproject.toml" + - "uv.lock" + schedule: + # Weekly even when no workflow changed, because the online audits compare pinned + # actions against advisories that are published after the pin. + - cron: "0 6 * * 1" + workflow_dispatch: + +# Deny by default. The one job below grants itself only what it needs. +permissions: {} + +concurrency: + group: zizmor-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + zizmor: + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write # upload the SARIF report to code scanning + steps: + - name: Check out the repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # Nothing here pushes with git, so the checkout's token has no reason to + # stay behind in .git/config. + persist-credentials: false + + - name: Install uv + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + with: + version: "0.9.9" + + # zizmor runs from its own dependency group in uv.lock, so its version is pinned + # with hashes and --locked refuses a lockfile that drifted from pyproject.toml. + - name: Audit workflows and actions + env: + # The online audits read the public API: impostor commits, version comments, + # and known-vulnerable actions. The job's read-only token is all they use. + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + uv run --locked --only-group zizmor \ + zizmor --no-progress --no-exit-codes --format sarif . > zizmor.sarif + + - name: Upload the report to code scanning + # A pull request from a fork runs with a read-only token and cannot upload. + # Its findings print as annotations in the next step instead. + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + uses: github/codeql-action/upload-sarif@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0 + with: + sarif_file: zizmor.sarif + category: zizmor + + - name: Annotate a pull request from a fork + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + uv run --locked --only-group zizmor \ + zizmor --no-progress --no-exit-codes --format github . diff --git a/pyproject.toml b/pyproject.toml index 1ad3b3f..453e9bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,10 @@ dependencies = [ "mike>=1.2.0", "mkdocs-material>=9.6.14", "pymdown-extensions>= [dependency-groups] dev = [ "pytest>=8.0", "jsonschema>=4.25.0",] +# Workflow security analysis, run by .github/workflows/zizmor.yml. A named group rather +# than dev, so the docs build and the test gate never install it. The lockfile pins the +# version with hashes, and Dependabot moves it under the usual cooldown. +zizmor = [ "zizmor>=1.30.1",] [tool.pytest.ini_options] # The deploy workflow runs a bare `uv run pytest -v` as the gate the site build diff --git a/uv.lock b/uv.lock index db2ef13..8cbcf92 100644 --- a/uv.lock +++ b/uv.lock @@ -17,6 +17,9 @@ dev = [ { name = "jsonschema" }, { name = "pytest" }, ] +zizmor = [ + { name = "zizmor" }, +] [package.metadata] requires-dist = [ @@ -30,6 +33,7 @@ dev = [ { name = "jsonschema", specifier = ">=4.25.0" }, { name = "pytest", specifier = ">=8.0" }, ] +zizmor = [{ name = "zizmor", specifier = ">=1.30.1" }] [[package]] name = "attrs" @@ -798,3 +802,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, ] + +[[package]] +name = "zizmor" +version = "1.30.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/c5/3d3dc61c0b64c082e1f704f5cf44f6a38153adbccf8787dd06a1a0ed14e0/zizmor-1.30.1.tar.gz", hash = "sha256:cbb7e5cb2fec471066d5c326bcaa69ed3eefdcf6a76d7ad38335b924acf86c07", size = 575745, upload-time = "2026-09-09T05:27:21.325Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a1/a877361a6c6ae13fb0ff4ed3c9d681ccddb9c838c5c3ca8f163a0fcfded9/zizmor-1.30.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17fda74e15fe41a6aa354ea4cf71fc7ff3cd025cda6584bb6d7466b3f1d00fe6", size = 9148491, upload-time = "2026-09-09T05:26:58.842Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0e/ea44a8f466b9a080dc0634497f6c9ee63dd586a94f5e2cd743e06c4df140/zizmor-1.30.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f9eb092f089e35fa9fb3b70aeec0a19eca7dacf2ab161f1d50b18351895f085c", size = 8743712, upload-time = "2026-09-09T05:27:01.342Z" }, + { url = "https://files.pythonhosted.org/packages/37/9c/43764a6e9eb5ad5a332202afc3e63622e3380669ecf7079b6b0de892e474/zizmor-1.30.1-py3-none-manylinux_2_24_aarch64.whl", hash = "sha256:92906f448365672ebb657fb50d36f997290a3aaba8cac723214741ea9054845d", size = 9009843, upload-time = "2026-09-09T05:27:03.522Z" }, + { url = "https://files.pythonhosted.org/packages/cf/2d/96c9c7b37e46e2b092e5daac3c6e9d4966ed7b58845f3f04950043d89c34/zizmor-1.30.1-py3-none-manylinux_2_28_armv7l.whl", hash = "sha256:5e8a3d54c3d9d51307371f4d1d49f8f9e56de00205c6cc660c6846a95ad4e135", size = 8620204, upload-time = "2026-09-09T05:27:06.001Z" }, + { url = "https://files.pythonhosted.org/packages/63/55/1900b53d34dcebc207cf3ceb7957b3748e8bb40958f062e3853ab72f2395/zizmor-1.30.1-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:eee12266b793cb87ad4a7e3af2e72404f8a63e3de5eb099b80bf7b1cfd232a8e", size = 9467893, upload-time = "2026-09-09T05:27:08.07Z" }, + { url = "https://files.pythonhosted.org/packages/9b/aa/d998bbdebf1f40e0d3dd027f94bceb961065640b29bc82bf5ffa9266fdb9/zizmor-1.30.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:15679642e8c4f825ba3f22537c698b1bbbad19ead23640f62ec45ebbe7abc803", size = 9038587, upload-time = "2026-09-09T05:27:10.226Z" }, + { url = "https://files.pythonhosted.org/packages/e0/16/0512afae720fd4b7e22bbb6f5bdd24fad49d6b1b92cd34ac71e0d8076109/zizmor-1.30.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:bf63099a27ef3bf1329dbab44bb9d1027124855c71bd0eca864d5c8e1c5c0db1", size = 8583701, upload-time = "2026-09-09T05:27:12.73Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ff/72efacfb79177835609fe712e9d543a0e5a05d41e3774e907f0b610c8619/zizmor-1.30.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:4a5ef2fa4fbd2984f794cfc2f95790bd03fa0bae801487ed550e044675874e60", size = 9558806, upload-time = "2026-09-09T05:27:15.124Z" }, + { url = "https://files.pythonhosted.org/packages/bb/64/1746121c819319ad7e9d0dfaa96de38dc52d4ac5f6485472b11362f3c6ba/zizmor-1.30.1-py3-none-win32.whl", hash = "sha256:42512bb4a1bce4c787d221d1572ff6b691fa52a3d129b2910be8afaf860c5871", size = 7773805, upload-time = "2026-09-09T05:27:17.218Z" }, + { url = "https://files.pythonhosted.org/packages/db/fe/2889fcbcf7728dd5e013bae7cf5c4e634d981a9e26d55e1da6ab9006404f/zizmor-1.30.1-py3-none-win_amd64.whl", hash = "sha256:06e039fae8e185e2a1a8b002f2d95a6f5c3b548048bdd79e004dcb501fcd0868", size = 8925696, upload-time = "2026-09-09T05:27:19.159Z" }, +] From 27d2019e38ba301c909a478fb9cb348bffc16178 Mon Sep 17 00:00:00 2001 From: rocklambros Date: Mon, 21 Sep 2026 10:50:56 -0600 Subject: [PATCH 2/2] Leave the board App token scope unchanged for now Token scopes are not changing in this pull request. zizmor's two github-app findings on board-reconcile.yml stay open in code scanning, where they record the deferred decision instead of disappearing behind a suppression. Signed-off-by: rocklambros --- .github/workflows/board-reconcile.yml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/.github/workflows/board-reconcile.yml b/.github/workflows/board-reconcile.yml index 37ade31..1fe2434 100644 --- a/.github/workflows/board-reconcile.yml +++ b/.github/workflows/board-reconcile.yml @@ -101,16 +101,6 @@ jobs: # pull requests, which is the whole grant. It cannot write code, change # settings, or reach any other repository. owner: ${{ github.repository_owner }} - # The same grant, stated again here so the token cannot outgrow it. The - # installation's settings live outside this repository and have drifted once - # already. With these lines a wider installation still mints a narrow token. - # The repository is named literally because a scheduled run carries no - # repository in its event payload. A rename fails the token request loudly. - repositories: agent-control-standard - permission-organization-projects: write - permission-issues: read - permission-pull-requests: read - permission-metadata: read - name: Reconcile the board env: