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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .github/ISSUE_TEMPLATE/bug.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Bug report
description: agent-security-stack did something other than what it says it does.
labels: ['bug']
body:
- type: textarea
id: what
attributes:
label: What happened
description: What you ran, and what it did.
render: text
validations:
required: true

- type: textarea
id: expected
attributes:
label: What you expected instead
validations:
required: true

- type: textarea
id: repro
attributes:
label: Reproduction
description: >
The smallest thing that shows it. A saved page, a URL, or a local HTML fixture is ideal — attach it if you can share it.
render: text
validations:
required: true

- type: input
id: version
attributes:
label: agent-security-stack version
placeholder: 'x.y.z'
validations:
required: true

- type: input
id: env
attributes:
label: OS and Node version
placeholder: 'macOS 15.2, node 22.11.0'
validations:
required: true
10 changes: 10 additions & 0 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# blank_issues_enabled stays TRUE on purpose: a template that does not fit is a
# reason to file a plain issue, not a reason to give up. The templates exist to
# make the common reports good, not to gate the uncommon ones.
blank_issues_enabled: true
contact_links:
- name: Report a security vulnerability (private)
url: https://github.com/askalf/agent-security-stack/security/advisories/new
about: >
Never file a vulnerability as a public issue. Private advisories let us
ship a fix before the details are public — see SECURITY.md.
30 changes: 30 additions & 0 deletions .github/ISSUE_TEMPLATE/feature.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Feature request
description: Something agent-security-stack should do that it does not.
labels: ['enhancement']
body:
- type: textarea
id: problem
attributes:
label: The problem
description: >
What are you trying to do, and where does agent-security-stack stop you? Describe
the situation rather than the feature — the best proposals here have
come from a concrete agent-browsing problem someone actually hit.
validations:
required: true

- type: textarea
id: proposal
attributes:
label: What you have in mind
description: Optional. A rough shape is fine; so is "no idea, but here is the problem".
validations:
required: false

- type: textarea
id: alternatives
attributes:
label: What you do today instead
description: The workaround, if there is one.
validations:
required: false
68 changes: 68 additions & 0 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Path → label map for .github/workflows/pr-triage.yml (actions/labeler v7).
#
# Additive only: sync-labels is off, so nothing here ever strips a label a
# human — or Dependabot — put on a PR by hand. Add a path, get a label.
#
# Bare-noun labels, matching the Own Your Stack family vocabulary (redstamp is
# the reference), so a reader moving between repos sees the same words.

mcp:
- changed-files:
- any-glob-to-any-file:
- 'mcp.mjs'
- 'mcp-manifest.json'

cli:
- changed-files:
- any-glob-to-any-file:
- 'bin/**'

audit:
- changed-files:
- any-glob-to-any-file:
- 'audit-trail.mjs'

examples:
- changed-files:
- any-glob-to-any-file:
- 'demo/**'
- 'demo.mjs'

support:
- changed-files:
- any-glob-to-any-file:
- 'support/**'

lockfile:
- changed-files:
- any-glob-to-any-file:
- 'truecopy.lock'

tests:
- changed-files:
- any-glob-to-any-file:
- 'test/**'
- '**/*.test.mjs'

fuzz:
- changed-files:
- any-glob-to-any-file:
- 'fuzz/**'
- '.clusterfuzzlite/**'

github_actions:
- changed-files:
- any-glob-to-any-file:
- '.github/workflows/**'
- '.github/labeler.yml'

documentation:
- changed-files:
- any-glob-to-any-file:
- 'README.md'
- 'CONTRIBUTING.md'
- 'CODE_OF_CONDUCT.md'
- 'SECURITY.md'
- '.github/ISSUE_TEMPLATE/**'
- '.github/pull_request_template.md'

1 change: 1 addition & 0 deletions .github/labels.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{"color":"5319E7","description":"Audit trail","name":"audit"},{"color":"d73a4a","description":"Something isn't working","name":"bug"},{"color":"0E8A16","description":"Command-line surface","name":"cli"},{"color":"0366d6","description":"Pull requests that update a dependency file","name":"dependencies"},{"color":"0075ca","description":"Improvements or additions to documentation","name":"documentation"},{"color":"cfd3d7","description":"This issue or pull request already exists","name":"duplicate"},{"color":"a2eeef","description":"New feature or request","name":"enhancement"},{"color":"FBCA04","description":"Demos","name":"examples"},{"color":"B60205","description":"Fuzzing and ClusterFuzzLite","name":"fuzz"},{"color":"000000","description":"Pull requests that update GitHub Actions code","name":"github_actions"},{"color":"7057ff","description":"Good for newcomers","name":"good first issue"},{"color":"008672","description":"Extra attention is needed","name":"help wanted"},{"color":"e4e669","description":"This doesn't seem right","name":"invalid"},{"color":"168700","description":"Pull requests that update javascript code","name":"javascript"},{"color":"5319E7","description":"truecopy.lock pin set","name":"lockfile"},{"color":"1D76DB","description":"MCP server and manifest","name":"mcp"},{"color":"d876e3","description":"Further information is requested","name":"question"},{"color":"F0883E","description":"200-799 hand-written lines","name":"size/L"},{"color":"FBCA04","description":"50-199 hand-written lines","name":"size/M"},{"color":"8AC926","description":"10-49 hand-written lines","name":"size/S"},{"color":"D93F0B","description":"800+ hand-written lines","name":"size/XL"},{"color":"3FB950","description":"Under 10 hand-written lines","name":"size/XS"},{"color":"8E44AD","description":"Support scripts","name":"support"},{"color":"0052CC","description":"Test suite and CI","name":"tests"},{"color":"ffffff","description":"This will not be worked on","name":"wontfix"}]
20 changes: 20 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!--
Area labels are applied automatically from the paths you touched
(.github/labeler.yml). Add judgement labels yourself if they apply.
-->

## What this changes

<!-- The behaviour that is different afterwards, not a list of files. -->

## Why

<!-- The problem this solves. If it fixes something, what was broken and how it
showed up. Link the issue. -->

## How it was verified

<!-- Commands run and what they printed. Claims about behaviour need evidence:
"it works" is not a result, and a reviewer cannot re-derive one. -->

- [ ] `npm test` passes
57 changes: 57 additions & 0 deletions .github/workflows/labels.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: labels

# .github/labels.json is the repository's DECLARED label set. This job proves it
# is also the LIVE one.
#
# Without it the manifest rots: someone renames a label in the GitHub UI, the
# manifest still lists the old name, and test/labeler-config.test.mjs happily
# confirms that labeler.yml only applies declared labels — all three agreeing
# with each other and none of them with GitHub. The offline test checks
# declared-vs-applied; this checks declared-vs-live. Between them, "every label
# this repo applies actually exists" is proven rather than asserted.
#
# A standalone workflow rather than a job inside ci.yml: it is one API call with
# a read-only token, it has nothing to do with building or testing the product,
# and keeping it separate means the same file drops into every repo in the family
# regardless of what that repo's ci.yml looks like.

on:
push:
branches: [main, master]
pull_request:
paths:
- '.github/labels.json'
- '.github/labeler.yml'
- '.github/ISSUE_TEMPLATE/**'
- '.github/workflows/labels.yml'
workflow_dispatch:

permissions:
contents: read

jobs:
manifest:
name: manifest matches the repo
runs-on: ubuntu-latest
timeout-minutes: 3
permissions:
contents: read
issues: read # gh label list
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: The declared label set is the live label set
env:
GH_TOKEN: ${{ github.token }}
run: |
gh label list --repo "$GITHUB_REPOSITORY" --limit 200 --json name --jq '.[].name' \
| LC_ALL=C sort > live.txt
node -e "JSON.parse(require('fs').readFileSync('.github/labels.json','utf8')).forEach(l => console.log(l.name))" \
| LC_ALL=C sort > declared.txt
if ! diff -u declared.txt live.txt; then
echo "::error::.github/labels.json and the repository's labels differ (- declared only, + live only). Change the label on GitHub AND in the manifest together."
exit 1
fi
echo "$(wc -l < live.txt) labels, manifest matches"
129 changes: 129 additions & 0 deletions .github/workflows/pr-triage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
name: PR triage

# Labels, sizes, assigns, and issue-links every PR so the queue reads at a
# glance instead of being a wall of untriaged titles.
#
# pull_request_target, so PRs from forks get triaged too — a plain
# pull_request run gets a read-only token on a fork PR and would just 403.
# It is the safe half of that trigger: this workflow never checks out or
# executes PR code, it only reads the event payload and calls the API.

on:
pull_request_target:
types: [opened, reopened, ready_for_review, synchronize]

permissions:
contents: read

concurrency:
group: pr-triage-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
triage:
name: triage
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- name: Label by changed path
uses: actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13 # v7.0.0
with:
# Additive. Syncing would strip the `dependencies` label Dependabot
# puts on its own PRs whenever the diff doesn't touch a manifest.
sync-labels: false

- name: Size label, assignee, issue link
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const pr = context.payload.pull_request;
const { owner, repo } = context.repo;
const number = pr.number;

// ── size ──────────────────────────────────────────────────────
// Measured on hand-written lines only. Lockfiles, hash-pinned
// requirements and generated corpora are real changes but they
// aren't review effort, and counting them makes every one of
// them an size/XL.
const GENERATED = [
/(^|\/)package-lock\.json$/,
/(^|\/)requirements-.*\.txt$/,
/(^|\/)go\.sum$/,
/^arena\/corpus.*\.json$/,
/^arena\/.*-results\.json$/,
];
const files = await github.paginate(github.rest.pulls.listFiles, {
owner, repo, pull_number: number, per_page: 100,
});
const churn = files
.filter(f => !GENERATED.some(re => re.test(f.filename)))
.reduce((n, f) => n + f.additions + f.deletions, 0);
const SIZES = [
['size/XS', 10], ['size/S', 50], ['size/M', 200],
['size/L', 800], ['size/XL', Infinity],
];
const size = SIZES.find(([, max]) => churn < max)[0];

const current = pr.labels.map(l => l.name);
for (const stale of current.filter(n => n.startsWith('size/') && n !== size)) {
await github.rest.issues.removeLabel({
owner, repo, issue_number: number, name: stale,
}).catch(() => {});
}
if (!current.includes(size)) {
await github.rest.issues.addLabels({
owner, repo, issue_number: number, labels: [size],
});
core.info(`size: ${size} (${churn} hand-written lines)`);
}

// ── assignee ──────────────────────────────────────────────────
// Whoever opened it owns it until someone says otherwise. Bots
// can't be assignees, and an outside contributor without repo
// access is silently dropped by the API — so this is best-effort
// and never fails the run.
if (pr.assignees.length === 0 && pr.user.type !== 'Bot') {
try {
await github.rest.issues.addAssignees({
owner, repo, issue_number: number, assignees: [pr.user.login],
});
core.info(`assigned: ${pr.user.login}`);
} catch (e) {
core.info(`could not assign ${pr.user.login}: ${e.message}`);
}
}

// ── development (linked issue) ────────────────────────────────
// The Development sidebar link is driven by a closing keyword in
// the body — there's no API for it. So if the branch names an
// issue and the body doesn't already close one, write the
// keyword in. Only explicit branch forms count (issue-42, gh-42,
// 42-some-fix); a bare digit anywhere in a branch name is a
// version or a retry counter far more often than an issue.
const body = pr.body || '';
const closes = /\b(clos(e[sd]?)?|fix(e[sd])?|resolv(e[sd]?))\s+#\d+/i;
const branch = pr.head.ref;
const ref = branch.match(/(?:^|[/_-])(?:issue|issues|gh)[-_]?(\d+)(?:[/_-]|$)/i)
|| branch.match(/^(\d+)-/);

if (!closes.test(body) && ref) {
const issue_number = Number(ref[1]);
try {
const { data: issue } = await github.rest.issues.get({
owner, repo, issue_number,
});
// #N addresses both issues and PRs; only an open issue is a
// thing this PR can close.
if (!issue.pull_request && issue.state === 'open') {
await github.rest.pulls.update({
owner, repo, pull_number: number,
body: `${body.trimEnd()}\n\nCloses #${issue_number}`.trimStart(),
});
core.info(`linked: #${issue_number}`);
}
} catch (e) {
core.info(`no linkable issue #${issue_number}: ${e.message}`);
}
}
Loading