Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
2fc3874
Add an allowlisted gh runner that never puts free text on argv
rocklambros Oct 30, 2026
d9e22ef
Parse gh -i output with the shared runner's parser in the roadmap fetch
rocklambros Oct 30, 2026
79648f7
Add the intake tool's facts, decision, and data line
rocklambros Oct 30, 2026
87b63fa
Hash the cleared head and make the facts hash sort safe
rocklambros Oct 30, 2026
83fcdd4
Decide the intake gate from facts a lead performed
rocklambros Oct 30, 2026
d325097
Keep the bot's own label writes out of the intake facts hash
rocklambros Oct 30, 2026
054c805
Collect intake facts from the shared listings within a call budget
rocklambros Oct 30, 2026
d2a22a3
Keep draft pull requests out of the held issue cap
rocklambros Oct 30, 2026
b8a08ab
Apply intake decisions with paced, reversible writes
rocklambros Oct 30, 2026
58c7f16
Run the intake gate on events, nightly, and on dispatch, with arming,…
rocklambros Oct 30, 2026
db89fde
Keep the auto-merge disable outside the call budget, cap Retry-After,…
rocklambros Oct 30, 2026
1a40bae
Run the intake gate from one workflow step on a protected base ref
rocklambros Oct 30, 2026
6c1d298
List the intake queue on the health issue, numbers and reasons only
rocklambros Oct 30, 2026
5947620
Bound the intake health reads, skip bot merges, and name legacy queue…
rocklambros Oct 30, 2026
02f2a41
Have the roadmap monitor raise the lead alarm when the intake sweep f…
rocklambros Oct 30, 2026
ac09ba6
Say the intake gate in CONTRIBUTING, README, the template, the form, …
rocklambros Oct 30, 2026
5b5dc57
Name the whole documentation lane and triage volunteers in the intake…
rocklambros Oct 30, 2026
caeed80
Record the intake gate design, its premortem, and its plan
rocklambros Oct 30, 2026
1cb3850
Post today's keyword warning in legacy mode and queue a warning-only …
rocklambros Oct 30, 2026
99428cc
Count queue days from the data line's since, not from the pull reques…
rocklambros Oct 30, 2026
2f854fb
Read the rule's landing date from PR_INTAKE_LANDED instead of a place…
rocklambros Oct 30, 2026
fa3cf2c
Read each lead's latest verdict so a later request for changes outran…
rocklambros Oct 30, 2026
09ea9e0
Decide a fork's review run read-only and leave the writes to the nigh…
rocklambros Oct 30, 2026
1eda0ac
Raise on a comments listing in an unreadable shape and say so in Risks
rocklambros Oct 30, 2026
0103744
Run on a label removal only for the queue label, and name integration…
rocklambros Oct 30, 2026
25cdbaa
Fix the batched comment, the claim cap note, the run link, and three …
rocklambros Oct 30, 2026
ce201ba
Name the root notice files, the lane in the flow, and the fixture han…
rocklambros Oct 30, 2026
3220bb4
Record the whole-branch review fixes as amendments and correct the bo…
rocklambros Oct 30, 2026
048f7bc
Carry today's keyword marker inside the intake comment's warning so a…
rocklambros Oct 30, 2026
8b5eaa8
Remove a contributor's name from the intake premortem
rocklambros Oct 31, 2026
0ba6ab9
Record the owner's acceptance of the intake deviation
rocklambros Oct 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
# invitations to @evabenn, @RbBuiltWrong, and @aruneeshsalhotra
# lapsed and are not being re-sent, so their entries stay inert. The other
# owners on their lines still apply, so review coverage holds.
#
# An owner login here is also exempt from pull request intake and can clear
# another contributor's by approval.

# Default owners for anything not matched below.
* @rocklambros @fewdisc @GangGreenTemperTatum @mamicidal @sclintonowasp @afogel @stefanoamorelli @almogbhl @bar-capsule @evabenn @RbBuiltWrong @aruneeshsalhotra @artmaro @Lrd0036
Expand Down
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/3-reference-implementation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ body:
value: >-
This is the work the project needs most. Check the open issues labeled
`help wanted` before filing, since the one you want may already be accepted.
Comment on the issue to claim it before opening the pull request.
- type: input
id: target
attributes:
Expand Down
9 changes: 5 additions & 4 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@
The closing-choice check fails a reference with neither.

A change that alters behavior, normative text, or adds code needs an issue carrying
`status:accepted`. If yours is not accepted yet, open the PR anyway. It will wait
rather than be closed. See Current Priority Scope in CONTRIBUTING.md. -->
`status:accepted` that a lead assigned to you, or a `help wanted` issue you commented
on first. Comment on the issue to claim it before opening the pull request. If you
open the pull request without that, it waits rather than being closed, and a bot
comment says which step is missing. See How work gets accepted in CONTRIBUTING.md. -->

- [ ] This is an editorial correction (typo, grammar, link, formatting) with no change in
meaning, so it needs no issue
- [ ] This is an editorial correction on the documentation lane (typo, grammar, link, formatting) with no change in meaning, so it needs no issue

## Base branch

Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/monitor-roadmap.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ permissions: {}
jobs:
check:
# Scheduled runs come from the default branch but check out main, which may lack the monitor yet.
if: vars.ROADMAP_RENDER_ENABLED == 'true' || vars.ROADMAP_SYNC_ENABLED == 'true'
if: vars.ROADMAP_RENDER_ENABLED == 'true' || vars.ROADMAP_SYNC_ENABLED == 'true' || vars.PR_INTAKE_ENFORCED == 'true'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
actions: read # reads the intake workflow's run list
contents: read
issues: write
steps:
Expand All @@ -39,5 +40,7 @@ jobs:
ROADMAP_RENDER_ENABLED: ${{ vars.ROADMAP_RENDER_ENABLED }}
ROADMAP_SYNC_ENABLED: ${{ vars.ROADMAP_SYNC_ENABLED }}
ROADMAP_HEALTH_ISSUE: ${{ vars.ROADMAP_HEALTH_ISSUE }}
PR_INTAKE_ENFORCED: ${{ vars.PR_INTAKE_ENFORCED }}
PR_INTAKE_TOOL_SHA: ${{ vars.PR_INTAKE_TOOL_SHA }}
TEST_ALARM: ${{ inputs.test_alarm }}
run: python3 -I -S tools/monitor_roadmap.py
177 changes: 85 additions & 92 deletions .github/workflows/pr-intake.yml
Original file line number Diff line number Diff line change
@@ -1,109 +1,102 @@
# Tells a contributor why their pull request is waiting, and labels it so the queue is
# visible.
# Decides whether each pull request has an accepted issue behind it, queues the ones that
# do not, and says why once. The logic lives in tools/pr_intake.py, which this job runs as
# one step. Version 2.0. Owner: ACS project leads.
#
# pull_request_target is used deliberately: GITHUB_TOKEN is read-only on a pull_request
# event from a fork, and every external contribution is a fork. That trigger runs with
# repository write scope against the base branch, so this workflow checks out no code,
# runs no contributor-supplied script, and reads the pull request body from the event
# payload as data.
# event from a fork, and every external contribution is a fork. That trigger runs with the
# base repository's token, so the job checks out the pull request's base ref, never the
# head. main, integration, and release/* are ruleset-protected, and the job's if: admits
# no other base, so only code a lead approved runs beside the token. The tool reads
# titles, bodies, and comments through the API as data. Its env carries GH_TOKEN, the
# number, the event name and action, the dispatch inputs, and three repository variables,
# nothing else.
#
# pull_request_review gets a read-only token when the pull request comes from a fork, so
# on a fork's pull request the tool decides, writes nothing, prints a notice, and exits 0,
# and the nightly sweep applies the decision. A same-repository pull
# request is cleared on the review itself.
#
# The nightly sweep runs at 03:10 UTC so the 04:10 roadmap sweep can report the same
# night's pass. That ordering is best effort: roadmap-refresh.yml records scheduler start
# delays of up to two hours on this repository.
#
# PR_INTAKE_ENFORCED and PR_INTAKE_TOOL_SHA arm enforcement. Unsetting PR_INTAKE_ENFORCED
# is the kill switch, and the tool then runs only the legacy pass. PR_INTAKE_LANDED is the
# day enforcement started, set in the same sitting. It changes one sentence of the comment
# on older pull requests and arms nothing.
# tests/test_pr_intake_workflow.py pins this file.
name: PR intake

on:
# zizmor flags every pull_request_target. The paragraph above is why this one is safe.
# zizmor flags every pull_request_target. This one checks out a protected base ref,
# never the pull request head, so no code from the pull request runs with the token.
pull_request_target: # zizmor: ignore[dangerous-triggers]
types: [opened, edited, reopened]
types: [opened, edited, reopened, ready_for_review, synchronize, unlabeled, auto_merge_enabled]
pull_request_review:
types: [submitted, dismissed]
schedule:
- cron: "10 3 * * *"
workflow_dispatch:
inputs:
pr_number:
description: "One pull request to evaluate or retract. Empty means every open pull request."
type: string
required: false
mode:
description: "evaluate applies the gate. dryrun prints every decision and writes nothing. retract undoes the queue."
type: choice
options: [evaluate, dryrun, retract]
default: evaluate
authors:
description: "Whose pull requests a retraction reaches. bots means only known bot authors."
type: choice
options: [all, bots]
default: all

permissions: {}

concurrency:
group: pr-intake-${{ github.event.pull_request.number }}
cancel-in-progress: true
group: ${{ github.event.pull_request.number && format('pr-intake-{0}', github.event.pull_request.number) || inputs.pr_number && format('pr-intake-{0}', inputs.pr_number) || 'pr-intake-sweep' }}
# A newer event re-reads every fact, so an older event run is safe to cancel. A sweep
# or a dispatch is never cancelled, because it is attended and reports a summary.
cancel-in-progress: ${{ github.event_name == 'pull_request_target' }}

jobs:
triage:
intake:
name: PR intake
# Pull request events run only against a protected base, an edit runs only when it
# changed the body, the title, or the base, and a label removal runs only for the queue
# label, the one removal the gate reads.
if: >-
github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' ||
((github.event.pull_request.base.ref == 'main' || github.event.pull_request.base.ref == 'integration' ||
startsWith(github.event.pull_request.base.ref, 'release/')) &&
(github.event.action != 'edited' || github.event.changes.body || github.event.changes.title ||
github.event.changes.base) &&
(github.event.action != 'unlabeled' || github.event.label.name == 'status:needs-triage'))
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
pull-requests: write
issues: read
contents: read # check out the base ref the tool runs from
pull-requests: write # comment, set and clear the queue label, and turn auto-merge off
issues: read # list accepted and milestoned issues and read claim comments
steps:
- name: Check whether a referenced issue is accepted
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_BODY: ${{ github.event.pull_request.body }}
run: |
set -euo pipefail
# Issue numbers are extracted from the body text, which is untrusted input. It
# is matched against a digit pattern and never evaluated.
NUMBERS="$(printf '%s' "${PR_BODY:-}" | grep -oE '#[0-9]+' | tr -d '#' | sort -u || true)"
if printf '%s' "${PR_BODY:-}" | grep -qiE '^\s*-\s*\[x\]\s*This is an editorial correction'; then
echo "Declared editorial. No issue required."
exit 0
fi
# The `edited` trigger fires every time anyone touches the body, so without
# this the queue comment gets reposted on every edit. The Actions bot reports
# as `github-actions` in some payloads and `github-actions[bot]` in others, so
# the check is a prefix match rather than an exact one.
if gh pr view "$PR_NUMBER" --repo "$REPO" --json comments \
--jq '[.comments[] | select(.author.login | startswith("github-actions")) | select(.body | contains("<!-- acs-closing-keyword") | not)] | length' \
| grep -qv '^0$'; then
echo "Already commented on this pull request."
exit 0
fi
# One listing call, whatever the body holds. A per-reference lookup let one pull
# request body spend the repository's shared GITHUB_TOKEN budget, which the roadmap
# fetch and sync also draw on.
# A failed listing is not fatal. Nothing is then accepted, so the pull request is queued.
ACCEPTED="$(gh issue list --repo "$REPO" --label status:accepted --state all --limit 1000 --json number --jq '.[].number')" || ACCEPTED=""
for n in $NUMBERS; do
if printf '%s\n' "$ACCEPTED" | grep -qx "$n"; then
echo "Issue #$n is accepted."
exit 0
fi
done
gh pr edit "$PR_NUMBER" --repo "$REPO" --add-label 'status:needs-triage'
gh pr comment "$PR_NUMBER" --repo "$REPO" --body "Thanks for this. It is queued rather than ignored.

This pull request does not reference an issue carrying \`status:accepted\`, so a maintainer has not looked at it yet and will not until the underlying issue is triaged. Nothing here is rejected. See [Current Priority Scope](https://github.com/GenAI-Security-Project/agent-control-standard/blob/main/CONTRIBUTING.md#current-priority-scope) for what the project is working on, and [\`help wanted\`](https://github.com/GenAI-Security-Project/agent-control-standard/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) for work that is already accepted.

If this is an editorial correction, tick that box in the description and this comment stops applying."

- name: Flag closing keywords on roadmap issues
# A merge closes whatever the body says it closes, with the merging maintainer as the
# actor, so the roadmap would count it as delivered. This asks the merger to confirm.
# Advisory only: it never fails the job, and edits that leave the body alone skip it.
# It runs even when the step above exited early, because exit 0 is success and the
# keyword check does not depend on whether the pull request is queued.
if: always() && (github.event.action != 'edited' || github.event.changes.body)
continue-on-error: true
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# The base ref on pull request events. integration on schedule and dispatch, the ref
# whose tool a lead dry-runs to produce PR_INTAKE_TOOL_SHA, so the sweep enforces
# exactly the code that was armed.
ref: ${{ github.event.pull_request.base.ref || 'integration' }}
- name: Run the intake gate
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_BODY: ${{ github.event.pull_request.body }}
run: |
set -euo pipefail
# The body is untrusted. Only digits after a closing keyword are taken from it.
REFS="$(printf '%s' "${PR_BODY:-}" \
| grep -oiE '(^|[^[:alnum:]_])(close[sd]?|fix(e[sd])?|resolve[sd]?):?[[:space:]]+((GenAI-Security-Project/agent-control-standard)?#|https://github\.com/GenAI-Security-Project/agent-control-standard/issues/)[0-9]+' \
| grep -oE '[0-9]+$' | sort -u || true)"
[ -n "$REFS" ] || exit 0
MILESTONED="$(gh api --paginate "repos/$REPO/issues?milestone=*&state=open&per_page=100" \
--jq '.[] | select(.pull_request == null) | .number')"
HITS=""
for n in $REFS; do
if printf '%s\n' "$MILESTONED" | grep -qx "$n"; then HITS="$HITS #$n"; fi
done
[ -n "$HITS" ] || exit 0
# The marker names the issues it covers, so a later edit that closes a different
# roadmap issue is flagged again. Only the bot's own comments count, so a PR author
# cannot suppress the warning by commenting the marker themselves.
MARK="<!-- acs-closing-keyword:$(printf '%s' "$HITS" | tr -d '#' | xargs | tr ' ' ',') -->"
if gh pr view "$PR_NUMBER" --repo "$REPO" --json comments \
--jq '.comments[] | select(.author.login | startswith("github-actions")) | .body' \
| grep -qF "$MARK"; then
exit 0
fi
gh pr comment "$PR_NUMBER" --repo "$REPO" --body "$MARK
This pull request's description says it closes$HITS, which is on the project roadmap. When it merges, GitHub closes that issue and the roadmap counts it as delivered. Please confirm the change delivers the whole issue before merging."
PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number }}
INTAKE_MODE: ${{ inputs.mode || 'evaluate' }}
INTAKE_AUTHORS: ${{ inputs.authors || 'all' }}
EVENT_NAME: ${{ github.event_name }}
EVENT_ACTION: ${{ github.event.action }}
PR_INTAKE_ENFORCED: ${{ vars.PR_INTAKE_ENFORCED }}
PR_INTAKE_TOOL_SHA: ${{ vars.PR_INTAKE_TOOL_SHA }}
PR_INTAKE_LANDED: ${{ vars.PR_INTAKE_LANDED }}
run: python3 -I -S tools/pr_intake.py
6 changes: 6 additions & 0 deletions .github/workflows/roadmap-sync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
# Lists the intake sweep's runs for the health issue's queued section.
actions: read
contents: read
issues: write
pull-requests: read
Expand All @@ -84,13 +86,16 @@ jobs:
ROADMAP_REFRESH_ENABLED: ${{ vars.ROADMAP_REFRESH_ENABLED }}
ROADMAP_SYNC_ENABLED: ${{ vars.ROADMAP_SYNC_ENABLED }}
ROADMAP_HEALTH_ISSUE: ${{ vars.ROADMAP_HEALTH_ISSUE }}
PR_INTAKE_BASELINES: ${{ vars.PR_INTAKE_BASELINES }}
run: python3 -I -S tools/roadmap_sync.py sweep --apply

dryrun:
if: github.event_name == 'workflow_dispatch' && inputs.mode != 'sweep'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
# Lists the intake sweep's runs for the health issue's queued section.
actions: read
contents: read
issues: read
pull-requests: read
Expand All @@ -109,4 +114,5 @@ jobs:
ROADMAP_REFRESH_ENABLED: ${{ vars.ROADMAP_REFRESH_ENABLED }}
ROADMAP_SYNC_ENABLED: ${{ vars.ROADMAP_SYNC_ENABLED }}
ROADMAP_HEALTH_ISSUE: ${{ vars.ROADMAP_HEALTH_ISSUE }}
PR_INTAKE_BASELINES: ${{ vars.PR_INTAKE_BASELINES }}
run: python3 -I -S tools/roadmap_sync.py dryrun --issue "$ISSUE_NUMBER"
31 changes: 19 additions & 12 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,21 +58,28 @@ quarter is not a finding without value.

Anyone may open an issue. Only an issue carrying `status:accepted` enters the backlog.

A pull request that changes behavior, alters normative text, or adds code references an
accepted issue. An editorial correction does not, wherever it lands: a typo, a grammar
fix, a broken link, or a formatting repair that leaves the meaning untouched needs no
issue.
If you are a project lead, a workstream lead, a triage volunteer, or a code owner, your
pull requests are not queued. A lead's approval, or a lead's request for changes within
the last 30 days, clears the queue. If a lead has approved and merged a pull request of
yours before, your pull requests are not queued. An editorial fix on the documentation
lane, a page under `docs/topics/` or `design/` or one of the root notice files (a typo, a
link, formatting, no change in meaning), needs no issue: tick the editorial box in the
pull request template, and keep every changed file on that lane. The root notice files are
`README.md`, `CONTRIBUTING.md`, `CONTRIBUTORS.md`, `CODE_OF_CONDUCT.md`, `GOVERNANCE.md`,
`SECURITY.md`, `STYLE.md`, `LICENSING.md`, and `NOTICE`, and `docs/README.md` counts with
them. `DOCS_LANE_FILES` in `tools/base_branch_guard.py` is the list the bot reads. Otherwise
the pull request needs an accepted issue that a lead assigned to you. Comment on the issue to claim
it before opening the pull request. The comment is the claim, and a lead assigns you. An
issue labeled `help wanted` is open to anyone who commented on it before opening the pull
request, two open pull requests per person per issue. A pull request that meets none of
these waits with the `status:needs-triage` label and a bot comment naming the missing
step. It is not closed. If a form, a template, or a bot comment disagrees with this
section, this section wins.

In the pull request's issue section, write `Closes`, `Fixes`, or `Resolves` before an issue
the change completes, and `Part of`, `Refs`, or `Contributes to` before one it only
advances, because the roadmap counts a closed issue as delivered.

If you open a pull request against an issue that is not accepted yet, it will not be
reviewed and nothing about it is rejected. It waits, and a comment will say so. A pull
request that sits untriaged for a long time may be closed with an invitation to reopen
once its issue is accepted. Start from an issue labeled `help wanted` if you want work
that is already accepted.

[Triage authority](./GOVERNANCE.md#triage-authority) in GOVERNANCE.md says who applies
`scope:`, `priority:`, `workstream:`, and `status:accepted`. No issue form can apply them,
which is what makes the rule hold rather than depend on everyone remembering it.
Expand Down Expand Up @@ -161,8 +168,8 @@ what an agent did to its own commits.
## What We Need

Start with [issues labeled `help wanted`](https://github.com/GenAI-Security-Project/agent-control-standard/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22).
Every one of them is already accepted, which means you can open a pull request against it
without waiting on triage.
Every one of them is already accepted. Comment on one to claim it, then open the pull
request. The comment is the claim.

The highest-value contribution right now is running ACS against a real harness and
documenting where it fails. Install the reference implementation, wire it to a coding
Expand Down
Loading
Loading