ADR-0084.
ADR-0084-security-intake-leaves-one-inbox-the-judgement-stays-human.md
ADR-0084 — Security intake leaves one person's inbox; the judgement stays human
Date: 2026-09-30
Status: accepted 2026-09-30 — the recommended split, chosen by the maintainer; the two measurements under "What would settle it" remain to be run and can reopen it
Deciders: the maintainer
Method: SECURITY.md, runward/runbook.md §3 step 2, ROADMAP.md ("Security handling") and the
repository's workflows read on the branch of this ADR (base main at 0.42.3); GitHub's documentation
read at the source on 2026-09-30 and quoted below; the repository's merge settings, branch protection
and Dependabot history read through the API the same day (status codes and counts only: no advisory,
alert or report content was read or is recorded here)
Relates to: ADR-0039 (the operator layer
stays outside the CLI), ADR-0054 (nothing runward
operates holds state), ADR-0065 (what
an agent may propose, only the operator's hand completes), RWD-2026-0163 (the three security settings)
Context
The promise. SECURITY.md names one channel, the repository's private vulnerability reporting
form, and one deadline: "Acknowledgement: within 7 days of the report." It promises no fix time.
ROADMAP.md lists, under "Security handling, as the mature projects do it": "Security intake that does
not depend on one person's inbox. An automated acknowledgment inside the 7 days, a notification to the
maintainer, Dependabot security patches merged automatically on a green CI when they are
development-only, and a weekly CodeQL digest. This is operator-layer work (ADR-0039), outside the CLI."
What carries the promise today. One person's GitHub notifications. GitHub's page Managing privately reported security vulnerabilities: "When a new vulnerability is privately reported in a repository, GitHub notifies repository administrators and security managers if: They're watching the repository for all activity or are subscribed to “Security alerts” notifications. They have notifications enabled for the repository." A filtered e-mail, a muted repository or a week away and the 7 days pass silently; nothing measures the delay.
What the platform allows, read at the source (2026-09-30).
- Listing reports awaiting triage. REST
GET /repos/{owner}/{repo}/security-advisoriestakesstate, which "Can be one of:triage,draft,published,closed". Its access rule: "The authenticated user can access unpublished security advisories from a repository if they are a security manager or administrator of that repository, or if they are a collaborator on any security advisory." Permissions required for fine-grained personal access tokens lists the endpoint under Repository permissions for "Repository security advisories", accessread, tokenPAT; the GitHub Apps page lists it under the same permission, tokens "UAT, IAT" ("UAT = user access token, IAT = installation access token"). - The workflow token has no such permission. The
permissionstable of Workflow syntax for GitHub Actions namesactions,artifact-metadata,attestations,checks,code-quality,contents,deployments,discussions,id-token,issues,packages,pages,pull-requests,security-events,statusesandvulnerability-alerts; none covers repository security advisories. Use GITHUB_TOKEN for authentication in workflows: "If you need a token that requires permissions that aren't available in theGITHUB_TOKEN, create a GitHub App and generate an installation access token within your workflow. [...] Alternatively, you can create a personal access token, store it as a secret in your repository". Whether aGITHUB_TOKENis refused on the endpoint has not been measured (see "What would settle it"). - No workflow event fires on a report. The webhook event
repository_advisoryexists, "Action type:published,reported", available to arepository,organizationorappwebhook, and "To subscribe to this event, a GitHub App must have at least read-level access for the 'Repository security advisories' permission." Events that trigger workflows lists no advisory event: a workflow can only poll onschedule. - No documented API comments on an advisory. The maintainer page says: "To ask for more
information, or to open a discussion with the reporter, you can comment on the advisory. Any comments
are visible only to the reporter and to any collaborators on the advisory." The REST reference for
repository security advisories has eight operations (list for an organisation, list, create,
privately report, get, update, request a CVE, create a temporary private fork) and no comment
operation; the public GraphQL schema (
schema.docs.graphql, fetched 2026-09-30) contains no repository-advisory type. An automated acknowledgment written to the reporter, as the roadmap phrases it, has no documented channel. The only machine-writable change a reporter sees is the advisory's state (PATCH, "Repository security advisories"write), and accepting or closing a report is the triage decision itself. - Code scanning.
security-events: read"permits an action to list the code scanning alerts for the repository";GET /repos/{owner}/{repo}/code-scanning/alertsfilters onstate(open,closed,dismissed,fixed)..github/workflows/codeql.ymlalready runs weekly (cron: "37 5 * * 2"). - Dependabot auto-merge. Automating Dependabot with GitHub Actions gives the pattern:
dependabot/fetch-metadata(outputsdependency-names,dependency-type,update-type), thengh pr merge --auto --mergeunderpermissions: contents: write, pull-requests: write, with the note "you should enable Require status checks to pass before merging for the target branch". The action's README:dependency-typeis one of "direct:production,direct:developmentandindirect";update-typeis "The highest semver change being made by this PR". The troubleshooting page: runs triggered by Dependabot "receive a read-onlyGITHUB_TOKEN" by default, and "You can use thepermissionskey in your workflow to increase the access for the token". The options reference: the default three-day cooldown "does not apply to security updates". - Where a run's output lands. Using workflow run logs: "Read access to the repository is required" to view or download logs. On this public repository that is every logged-in account.
The repository as measured (2026-09-30). allow_auto_merge is false. Branch protection on main
requires four status checks (test (22), test (24), floor-ts, core tests, network-isolated),
requires no review and does not bind administrators. The repository holds 0 Actions secrets and 0
Dependabot secrets, and the runbook states it as a property: "the repository keeps no Actions secret
[...] so no CI job can run this". Dependabot has opened 50 pull requests since 2026-07-15: 19 merged,
31 closed unmerged; the merged ones waited a median of 0.3 day, at most 18.8 days. The published
package has three runtime dependencies; every other npm dependency at the root is a development
dependency, among them typescript, which compiles the dist/ that ships.
Decision
Split the intake by what it has to read. Whatever reads private security state (reports
in triage, code scanning alerts, Dependabot alerts) runs in the maintainer's operator layer, outside
this repository, with a token that never enters it. Whatever reads only public state (a Dependabot pull
request's metadata) runs as a workflow in this repository on the GITHUB_TOKEN, with no secret.
- Report watch, outside the repository (operator layer, ADR-0039). The maintainer's operator agent
polls
GET /repos/stranxik/runward/security-advisories?state=triage(or receives therepository_advisoryreportedwebhook, if the maintainer gives it a private endpoint) and sends a notification on a channel that is not the e-mail inbox: at the report, again at day 5 if the report is still intriage, and a last one at day 7. Its token: a fine-grained personal access token scoped to this one repository, read-only, with Repository security advisories: read, Code scanning alerts: read and Dependabot alerts: read and nothing else, an expiration of at most 90 days, kept in the operator's secret store (never in a file, never in this repository), its rotation date written in the operator's own runbook. The notification carries the advisory's link and age, never its text. - Acknowledgment stays a human comment, and the watch makes the deadline visible. Since no
documented API writes to the reporter (Context, 4), the 7-day acknowledgment is the maintainer's
comment on the advisory; the automation's job is that the maintainer cannot miss the clock.
SECURITY.mdkeeps its wording; if the maintainer wants it to say how the acknowledgment is produced, that is a text change decided separately. - Weekly CodeQL and Dependabot digest, outside the repository. The same agent, weekly, counts open code scanning alerts by severity and open Dependabot alerts by severity, and sends the counts with links. Nothing is posted in the repository: an issue or a run log is readable by every logged-in account (Context, 7), and an open alert is an unfixed defect.
- Dependabot auto-merge, inside the repository, on the
GITHUB_TOKEN. One workflow onpull_request, gated ongithub.event.pull_request.user.login == 'dependabot[bot]', withpermissions: contents: write, pull-requests: writeat job level, runsdependabot/fetch-metadatapinned by commit SHA and enablesgh pr merge --autoonly when all of the following hold: the ecosystem isnpmat the root or in/floor-ts;dependency-typeisdirect:development;update-typeisversion-update:semver-patchorversion-update:semver-minor; no updated dependency is on a deny-list kept in the workflow, starting withtypescript(it builds what ships). The required checks are the merge condition; auto-merge adds none and removes none. GitHub Actions updates (chore(ci)) never auto-merge: they change what runs with the workflows' privileges, which the threat model says is "untrusted until reviewed". Turning onallow_auto_mergeis an administrator's act, the maintainer's. - What stays a human decision, with no automation around it. Whether a report is a vulnerability
(accept as draft, or close); when and how to fix it; the advisory's text, its severity, its CVE
request and its publication; the acknowledgment comment; any dependency update outside the envelope
of point 4, production dependencies and
indirectones included; the three security settings the runbook reads back at each release; the token's issue, scope and rotation. - What is automated. The notification at the report and its day-5 and day-7 reminders; the weekly digest; the merge of development-only patch and minor updates once the required checks are green.
Nothing here enters the CLI, the gate, the mission or the verdict path (ADR-0054). The only change to this repository, if the decision is taken, is the auto-merge workflow and a line in the runbook.
Alternatives discarded
- A scheduled workflow in this repository with a narrowly scoped token (a fine-grained PAT or a
GitHub App installation token with "Repository security advisories: read", stored as an Actions
secret). It is the cheapest to build and the costliest to hold. It ends the repository's
zero-secret property, which the runbook states and which the threat model relies on; it puts a token
that reads unpublished vulnerability reports in a public repository's CI, where every workflow file
and action is one pull request away from it (a
pull_requestrun from a fork receives no secret, but a merged change to any workflow does); its logs are readable by any logged-in account, so one carelessechopublishes an embargoed report; and it still has nowhere private to send the notification (an issue is public, an e-mail is the inbox this ADR leaves). A GitHub App narrows the blast radius (installation tokens "expire after 1 hour") but adds an App private key as the secret instead. Kept as the fallback if the maintainer has no operator agent, and then only with the App, a job that prints status codes and counts only, and a notification through an external channel whose own secret is counted. - Do nothing, keep it manual. Costs nothing and holds no token. The 7-day acknowledgment keeps resting on one notification setting that nothing checks, and Dependabot's pull requests keep waiting on a person (median 0.3 day, but 18.8 days at worst, measured). Honest, and the default until this ADR is decided.
- Everything outside the repository, auto-merge included. The operator agent could merge Dependabot
pull requests with a write token. That needs a token with
Contents: writeliving outside GitHub's own lifetime rules, where the in-repository workflow needs no secret at all, since theGITHUB_TOKEN"expires when the job finishes or after its effective maximum lifetime". Worse on the axis this ADR is about. - Auto-merge every Dependabot security update, production included. It is what the roadmap's phrase could be read to ask, and it would let an unreviewed version into the three packages that ship, on a schedule that security updates do not cool down (Context, 6). A patch that closes a vulnerability in a runtime dependency is a release decision (ADR-0068), not a merge.
Consequences
- Positive. The acknowledgment deadline gets a clock that someone other than the inbox watches, with two reminders before it lapses. Development-only bumps merge on the same checks a person would wait for. The repository keeps zero secrets and no workflow reads private security state.
- Negative, accepted. The acknowledgment itself stays manual: the promise still depends on one
person, now warned three times instead of once. The report watch depends on the operator agent
running; if it stops, the intake falls back to today's state silently unless the agent reports its
own liveness. The fine-grained token is a standing secret on the maintainer's side, with a
rotation to keep.
indirectupdates (a vulnerable transitive development package, the case the roadmap names forqs) stay manual, becausefetch-metadatadoes not say whether a transitive package is development-only. - On the documents.
runward/governance/threat-model.mdcounts "The repository's two scheduled workflows (watch-external-facts, scorecard)";.github/workflows/codeql.ymlis scheduled too, so the count is already off by one onmain. The auto-merge workflow is not scheduled and does not change it; that line is corrected separately.docs/compliance/regulated-adoption.mddescribes vulnerability management and gains one sentence on auto-merge if point 4 is adopted. - Supply chain. A development dependency is not harmless because it does not ship: it runs in CI and, for the compiler, produces the shipped code. The deny-list is the control; it is short on purpose and reviewed at each reevaluation.
What would settle it
- For the split (points 1 to 4): the maintainer confirms that the operator agent can hold a
fine-grained token and reach a notification channel, and that a test report lands as a notification
within one polling interval. The test report is filed from an account that is not an administrator
of the repository, so it arrives as a reporter's would; a draft advisory created by the maintainer
exercises the
statefilter only fordraft, which does not prove thetriagepath. - Against the fallback (a workflow with a token): one
workflow_dispatchrun that calls the endpoint with theGITHUB_TOKENand prints only the HTTP status. A 200 onstate=triagewould contradict Context, 2 and reopen the in-repository option without a secret; a 403 or 404 closes it. - For an automated acknowledgment: a documented endpoint that writes a comment on a repository security advisory. Its appearance in the REST reference or the GitHub changelog moves the acknowledgment from point 2 to point 6.
- For point 4's envelope: over the first 90 days of auto-merge, the list of merged updates read against the deny-list; any auto-merged update later reverted or tied to a defect narrows it.
Reevaluation trigger (mandatory, dated)
Reopen when GitHub documents an API that comments on a repository security advisory, or adds a
repository-advisory permission to the GITHUB_TOKEN, or an Actions event for reports; when the
project gains a second maintainer or a security manager (the single-person premise changes); when an
acknowledgment deadline is missed or met only after the day-7 reminder; or when an auto-merged update
is reverted.
Trigger set on: 2026-09-30 · Watched via: the GitHub REST changelog and the repository
security advisories reference at each release's runbook step 2; the defect register (found-by any)
for a missed acknowledgment or a reverted auto-merge.
References
SECURITY.md("Reporting a vulnerability");ROADMAP.md("Security handling, as the mature projects do it");runward/runbook.md§3 step 2;runward/governance/threat-model.md;.github/dependabot.yml;.github/workflows/codeql.yml.- GitHub Docs, read 2026-09-30: REST API endpoints for repository security advisories;
Permissions required for fine-grained personal access tokens; Permissions required for GitHub
Apps; Workflow syntax for GitHub Actions (
permissions); Use GITHUB_TOKEN for authentication in workflows; GITHUB_TOKEN; Webhook events and payloads (repository_advisory); Events that trigger workflows; Managing privately reported security vulnerabilities; REST API endpoints for code scanning; Automating Dependabot with GitHub Actions; Troubleshooting Dependabot on GitHub Actions; Dependabot options reference (cooldown,dependency-type); Using workflow run logs; Managing your personal access tokens; Generating an installation access token for a GitHub App. dependabot/fetch-metadata, README (inputsalert-lookup, outputsdependency-type,update-type).