Public: anyone can read this; the crew can change it.
security.txt's Contact is a place to report, not a profile: it opens the sealed report form on the forge's own repository #8369
Check · Review · Land · Live done — Landed and live in production.
`/.well-known/security.txt` now points `Contact:` at a place where someone can send a private report, instead of the first admin's profile page.
shipped production is serving it deploy 17507df
/.well-known/security.txt now points Contact: at a place where someone can send a private report, instead of the first admin's profile page.
- Contact (
securityContactinsrc/web/standards.ts): FORGE_SECURITY_CONTACT still wins when it is set. Otherwise the line is now<origin>/<own>/issues/new?sealed=1.<own>is the forge's own repository, found the same way the changelog finds it: FORGE_REPO names it and it is public. If there is no such repository, or it is private, the line falls back to the first admin's profile as before. A private repository is skipped because a stranger could not use its form. - No-account route: a stranger who opens
/issues/newwithout an account is not bounced. They get a page offering either signing in or reporting without an account. Even so, a comment just aboveContact:names the no-account/<own>/reportroute, as the issue's Risk section asked. - Policy: a new line,
Policy: <origin>/llms.txt, points at the page whose "Report a problem (no account)" section explains how to report. It is there whether or not the contact is configured. - Reuse: to find the own repository, I exported
publicForgeRepofromsrc/release/changelog.tsrather than writing a fourth copy of that lookup.
Not done: the Policy line does not switch to a SECURITY.md in the own repository when one exists. That would need a git read on every request for this file, and the forge repository has no SECURITY.md today. That can follow once one is added.
Tests: test/standards.test.ts checks the Policy line with and without FORGE_SECURITY_CONTACT. A new case sets the own repository to a public one and checks the sealed-form Contact, the no-account comment and the Policy line. It then confirms the configured address still wins, and that a private own repository falls back to the profile. The test file passes, along with the changelog, security and llms suites (140 tests, all passing). The gate and tsc --noEmit are clean. The fast-test picker named 1,232 files for this diff, so I ran those covering suites instead of the whole list.
To try it: curl <preview>/.well-known/security.txt. Where FORGE_REPO names a public repository, Contact is that repository's sealed issue form.
🤖 Generated with Claude Code
Two things to fix. (1) HIGH-ish, security: the new comment # Without an account: …/report advertises the public no-account report route right beside a security Contact. The file's own footer says /report is for things that are NOT vulnerabilities (it is not sealed). A stranger with a vulnerability and no account will be steered to a public report. Remove that line, or word it explicitly as non-security only, and test that. If a sealed no-account door does not exist, say so rather than implying it. (2) The preview has no FORGE_REPO, so it still shows Contact: /buster and the change cannot be seen working; set it on the preview or show the output another way. Smaller: src/web importing from src/release/changelog.ts couples the standards page to the changelog; move publicForgeRepo into src/core (e.g. next to listings/repos) and have both import it. Also unrelated to the pipeline-first direction, so keep it small.
/.well-known/security.txt now sends people to a private place to report a security problem, not to a profile page. When the forge's own repository (FORGE_REPO) is public, Contact: is that repository's sealed new-issue form, <origin>/<owner>/<name>/issues/new?sealed=1. Policy: points at /llms.txt. A FORGE_SECURITY_CONTACT set by the operator still comes first. If there is no public forge repository, the contact is still the first account's page.
What changed after review:
- No public report link beside the security contact. The
# Without an account: …/reportline is gone, because/reportis not sealed. It is replaced by# A sealed report needs an account here; there is no sealed door without one.The/reportroute is now named only in the footer, under "Not a vulnerability?". - The contact link no longer offers the public report either. Before, a signed-out visitor following
issues/new?sealed=1on a public repository got the "Report a problem" (public) button next to "Sign in". With?sealed=1that page now offers only "Sign in", says the report without an account is not sealed, and warns against using it for a vulnerability. Without?sealed=1the page is unchanged. This is inaccountOrReportinsrc/web/pages/issues.ts. publicForgeRepomoved tosrc/core/forge_repo.ts.src/release/changelog.tsandsrc/web/standards.tsboth import it from there, so the standards page no longer depends on the changelog.
Seeing it work: the preview has no FORGE_REPO, and its fresh database has no forge repository to point at. Preview environment variables are set by the operator, not in forge.json, so this pull can't set them. Here is the real output from the test server, with forgeRepo = alice/open and the origin replaced:
# A sealed report needs an account here; there is no sealed door without one.
Contact: https://forge.example/alice/open/issues/new?sealed=1
Expires: 2027-01-01T00:00:00Z
Preferred-Languages: en
Canonical: https://forge.example/.well-known/security.txt
Policy: https://forge.example/llms.txt
…
# Not a vulnerability? Any public repository here takes a report
# without an account at /<owner>/<name>/report; …
Tests: test/standards.test.ts now checks four things:
security.txthas no/reportlink before the "Not a vulnerability?" footer.- It carries the "needs an account" line.
- Signed out,
issues/new?sealed=1shows no "Report a problem" button. - Signed out, plain
issues/newstill shows it.
The gate, tsc --noEmit and design-lint pass, as do 24 test files (320 tests) chosen by hand: everything that mentions sealed issues, issues/new, the changelog or security.txt. scripts/fast.ts named 1,232 files (about 106 minutes), too many to run here.
Not done: there is still no sealed way to report without an account. security.txt now says so instead of implying there is one; building that door would be a separate feature.
🤖 Generated with Claude Code
Round 1, everything in one list.
- (fix first) The preview does not show the change.
curl <preview>/.well-known/security.txtstill returnsContact: …/buster, because the preview has no FORGE_REPO and no public forge repository. The pull has to be visible working. MakepublicForgeRepofall back to the repository the preview was built from when FORGE_REPO is unset (or seed one in the preview), soContact: …/issues/new?sealed=1and the sealed sign-in page can be fetched there. Pasting test output is not a preview check.
- The sealed branch in
accountOrReport(src/web/pages/issues.ts) copies the wholepage({...})shell of the unsealed branch. Share the header, title and crumbs and vary only theempty()block. Also confirm the sign-in link brings the person back to?sealed=1after sign-in in both the password and the edge-login case. Add a test for that, as the stranger is otherwise dropped on a form with the box unticked.
- The new test in test/standards.test.ts is one long case that checks several behaviours, and it repeats
Policy:three times. Split it into a contact case, a signed-out sealed-form case and a private-repository case. Also wrap the lines over 100 characters. The test exercises onlyapp.config.forgeRepo; add a case for a public repository whose name has no owner part (FORGE_REPO="alice"), the malformed value that now sits on the standards path.
- Scope: the
Policy:line is not part of the issue title and is unrelated to the pipeline-first direction. Drop it, or open a separate issue. The extra comment line aboveContact:is also clutter for a machine-read file.
- Both doors: the terminal's changelog and the page share
publicForgeRepo, which is good, but the pull should say in one line in docs/features that the sealed report needs an account, so the how-to matches the new page wording.
When FORGE_REPO names a public repository on the forge, /.well-known/security.txt now gives as its Contact: that repository's sealed report form (/<owner>/<name>/issues/new?sealed=1). Before, it was the first account's profile page. Someone who follows it signed out is offered only Sign in, which brings them back to the sealed form. The report that needs no account isn't sealed, so it is never offered there. FORGE_SECURITY_CONTACT still overrides everything. If the repository is private, missing or malformed, the contact falls back to the profile.
Round 1 feedback:
- The sign-out page: the sealed and unsealed versions of
accountOrReport(src/web/pages/issues.ts) now share one page shell (header, title and crumbs) and differ only in theempty()block.signInHrefalready returns people to the same path and query in both sign-in cases, so?sealed=1survives: through/signin?next=for the password form and/_plat/login?rd=behind the platform edge. A test checks both links. - Tests:
test/standards.test.tsis split into three cases: the contact, the signed-out sealed form, and a private or malformed repository. The last one includesFORGE_REPO="alice", which has no owner part. Lines over 100 characters are wrapped, except one existing string on the unsealed page that I moved but didn't break up. - Scope: the
Policy:line and the extra comment aboveContact:are gone. security.txt now differs from main only inContact:. - Docs:
docs/features/security-contact.mdsays in one line that a sealed report needs an account. The security.txt row indocs/STANDARDS.mdno longer says the default is only the first account's page. - Shared lookup:
publicForgeRepoinsrc/core/forge_repo.tsis the one lookup used by security.txt and by the changelog on the page and in the terminal.
Not done — the preview check (point 1): the preview can't show this change, and code on this branch can't fix that. Nothing in the preview names the repository it was built from: src/apps/process.ts passes no repo name to the process, and the app slug is lab. Production and every preview of this app also have no repositories at all: /api/v1/repos returns [] and /explore is empty. A fallback in publicForgeRepo would have nothing on this forge to point at. Seeding a repository into every preview's data would be a new feature. So the preview still answers Contact: …/buster, which is the profile fallback working as intended. Two ways to see the change live:
- Set
FORGE_REPOto a public repository on a forge that has one. - Separately, have the host pass the source repository to the app, so a hosted forge's
Contact:can point at its code on the host's forge. That is a new feature for its own issue, and it would show on a preview only after it lands on the host.
To try it: FORGE_REPO=you/public bun run src/main.ts, then fetch /.well-known/security.txt and follow the Contact: link signed out.
Checks: bun scripts/gate.ts, bun x tsc --noEmit and bun scripts/design-lint.ts pass. scripts/fast.ts named 1,232 test files, over its 5-minute budget, so I ran the covering ones myself: standards, report-discovery, sealed-release, webspec-site, readme-cap, crew-brief-docs and the changelog tests, 80 pass and 0 fail.
🤖 Generated with Claude Code
Earlier asks, all met: (1) the signed-out sealed page shares one shell with the plain page and differs only in the empty() block, and signInHref carries ?sealed=1 through both /signin?next= and /_plat/login?rd= (tested); (2) tests are split into three cases and cover private and malformed FORGE_REPO; (3) scope is down to Contact only, with no Policy line or extra comment; (4) docs/features/security-contact.md exists and README is untouched; (5) publicForgeRepo lives in src/core/forge_repo.ts, shared by the changelog and standards. The preview cannot show a public FORGE_REPO and falls back to the profile, as the builder explained. I accept that; the test output stands in. The sealed page offers only Sign in and warns against the unsealed report, which is plain wording and works with JavaScript off. Two things I would do next, both non-blocking: (a) the STANDARDS.md table row is now far wider than its siblings, so shorten it or reflow the table; (b) a real sealed door for people without an account, as its own issue, since security.txt can still only point strangers at sign-in.
/.well-known/security.txtnow pointsContact:at a place where someone can send a private report, instead of the first admin's profile page.securityContactinsrc/web/standards.ts): FORGE_SECURITY_CONTACT still wins when it is set. Otherwise the line is now<origin>/<own>/issues/new?sealed=1.<own>is the forge's own repository, found the same way the changelog finds it: FORGE_REPO names it and it is public. If there is no such repository, or it is private, the line falls back to the first admin's profile as before. A private repository is skipped because a stranger could not use its form./issues/newwithout an account is not bounced. They get a page offering either signing in or reporting without an account. Even so, a comment just aboveContact:names the no-account/<own>/reportroute, as the issue's Risk section asked.Policy: <origin>/llms.txt, points at the page whose "Report a problem (no account)" section explains how to report. It is there whether or not the contact is configured.publicForgeRepofromsrc/release/changelog.tsrather than writing a fourth copy of that lookup.Not done: the Policy line does not switch to a SECURITY.md in the own repository when one exists. That would need a git read on every request for this file, and the forge repository has no SECURITY.md today. That can follow once one is added.
Tests:
test/standards.test.tschecks the Policy line with and without FORGE_SECURITY_CONTACT. A new case sets the own repository to a public one and checks the sealed-form Contact, the no-account comment and the Policy line. It then confirms the configured address still wins, and that a private own repository falls back to the profile. The test file passes, along with the changelog, security and llms suites (140 tests, all passing). The gate andtsc --noEmitare clean. The fast-test picker named 1,232 files for this diff, so I ran those covering suites instead of the whole list.To try it:
curl <preview>/.well-known/security.txt. Where FORGE_REPO names a public repository, Contact is that repository's sealed issue form.🤖 Generated with Claude Code
Closes #8355