From 9ddb4b074196b01aac9986a927d6f37c4bad4860 Mon Sep 17 00:00:00 2001 From: Nils Lehnen <30603423+iderex@users.noreply.github.com> Date: Sat, 5 Sep 2026 09:26:44 +0200 Subject: [PATCH 1/2] Give a conduct report a route and take the shut door out of the policy (#10) CODE_OF_CONDUCT.md did not exist, so the only route visible to somebody with a conduct problem on a public board was a public issue, which names the person it is about to everybody before anybody has read it. That is the same failure SECURITY.md was written against, one file over. The routes are the two decided on #10 on 2026-09-04, in that order: the private report form on this repository, then a message through GitHub to the account that holds it. No mailbox is named, here or anywhere else on this board. The document says what I can actually do about a report, that one person holds it with no appeal, and that nothing in this repository enforces a word of it. SECURITY.md carried three readings that had stopped reproducing, and the first of them was routing reporters the wrong way. It said the private form does not answer, quoting {"enabled":false} taken on 2026-08-19, and offered a public issue as the honest alternative until it opened. The form is open: gh api repos/Flowfin/lab/private-vulnerability-reporting {"enabled":true} Run 2026-09-05. So the public-issue fallback is gone, and the file now says a report sent anywhere else is not received, which is the decision on #10 rather than a tightening chosen here. It said nothing is published as a binary, quoting two empty lists. Both answer with one entry now, so there is an artefact somebody can have downloaded, and a policy telling a reporter there is none to have been tampered with was excluding the one class this board has just created. A published artefact that does not match its checksum or its signature is now named as a report I want, and the covered set names the most recent release beside the default branch. It said thirteen files in .github/workflows, checked for pull_request_target and workflow_run. There are fifteen, and I re-ran the four claims that paragraph makes over all of them rather than carrying the sentence forward. Signed-off-by: Nils Lehnen <30603423+iderex@users.noreply.github.com> --- CODE_OF_CONDUCT.md | 155 +++++++++++++++++++++++++++++++++++++++++++++ SECURITY.md | 80 ++++++++++++++++------- 2 files changed, 213 insertions(+), 22 deletions(-) create mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..1d29c82 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,155 @@ +# Code of conduct + +## Who this covers, and where + +Everybody who takes part here: an issue, a pull request, a review, a commit +message, an experiment record, and a private message that started from one of +those. It covers me. I hold this repository and I am inside these rules rather +than above them, and if that sentence were not here the rest of this document +would be a set of rules for other people. + +The door is open on purpose, which is +[docs/decisions/0024-who-may-run-an-experiment-here.md](docs/decisions/0024-who-may-run-an-experiment-here.md), +and this file is part of what that costs. A board anybody may propose an +experiment on owes anybody who shows up a written answer to what happens when +somebody behaves badly. + +## What is expected + +Argue with the work. This board already runs that way for evidence: a claim +carries the command that produced it, and a disagreement is settled by executing +something rather than by who said it. The same rule pointed at people means the +thing under discussion is a record, a measurement or a change, never the person +who wrote it. + +Let an experiment fail. Everything here is public from the first commit, a +failed experiment included, and the whole arrangement depends on it being cheap +to write down an answer of no. [CONTRIBUTING.md](CONTRIBUTING.md) says that +nothing on this board treats a no as a lesser outcome, and that anybody who feels +a check pushing them towards a more flattering answer should report it as a +defect in the check. Treating somebody's failed experiment as a failure of theirs +is the behaviour that would make those sentences false, and it is the one this +board can least afford. + +Keep measured and assumed apart in the sentence. Being wrong in public is the +ordinary cost of working this way, and a correction is a repair rather than a +verdict on whoever needed it. Somebody who writes down that they did not measure +something has done the harder thing. + +Take a refusal with its reason. A change sent back here comes back with what was +wrong, and disagreeing with that reason is fair. Reopening it unchanged somewhere +else is not. + +## What is not acceptable + +- An attack on a person rather than on their work, including one dressed as a + question about their competence. +- Demeaning somebody for who they are, or for a group they belong to. +- Harassment, in the open or in messages that started here and continued + somewhere else. +- Publishing somebody's private details - a legal name, an address, an employer, + anything they did not put here themselves - without their consent. +- Unwanted attention of a sexual kind, and sexual material anywhere in this + repository or its tracker. +- Sustained disruption: a settled argument reopened without new evidence, a + thread derailed on purpose, a review answered with volume instead of + substance. +- Threatening any of the above, whether or not it is carried out. + +That list holds what has to be named and it is a floor rather than a boundary. +Behaviour nobody wrote down is judged by the section above it and is not +permitted by its absence from this one. + +One entry on that list is also a rule about the tree, and the overlap is worth +seeing rather than tripping over. Somebody else's personal data is something this +board may never hold, which is +[docs/decisions/0006-everything-here-is-public.md](docs/decisions/0006-everything-here-is-public.md), +and git history does not forget. So publishing it here is a conduct problem and +an exposure at once, and it is handled as both: what was published is removed and +said out loud rather than quietly reverted. + +## Reporting + +Two routes, in this order. + +**A private report on this repository.** The form under Security is the only +private channel this repository has: + + https://github.com/Flowfin/lab/security/advisories/new + +It is labelled for a vulnerability, because that is what it was built for. A +conduct report sent through it is not misfiled: it reaches the same person, it +stays private, and submitting it publishes nothing. Whether the form answers at +all is a setting on the repository rather than a promise in a file, so it is read +rather than asserted: + + gh api repos/Flowfin/lab/private-vulnerability-reporting + {"enabled":true} + +Run 2026-09-05. That reading needs administrative access to this repository, so +it is not one a reader of this board can make. What a reader can do is open the +address above and see whether the form is there. + +[SECURITY.md](SECURITY.md) names the same form for a vulnerability report. The +two documents point at one door on purpose: a second private channel would be a +second thing to keep alive, and a spare channel is unused right up to the moment +it is needed. + +**A message through GitHub to the account that holds this repository**, which is +https://github.com/iderex. Use this where the first route does not fit - where +the report is about the form, or about me, or where you would rather it did not +sit in a security queue. + +Do not open an issue about a conduct problem. An issue here is public from the +moment it is submitted, and it names the person it is about to everybody before +anybody has read it. That is the same reason this board has a security policy at +all, one file over. + +No mailbox is published, here or anywhere else on this board. An address in a +document outlives whoever was reading it and stays in the history after it is +changed, and an address nobody watches is worse than none, because the document +promises a reply. Both routes above are accounts on a service that already +authenticates whoever sends the message, and neither needs anything kept alive to +go on working. + +## What happens after a report arrives + +I read it and I answer it. If something is missing I ask for it, and if I decide +it is not a problem you get the reason rather than silence. + +There is no deadline and there will not be one, for the reason +[SECURITY.md](SECURITY.md) gives about the other route: a window this board +cannot hold is worse than none, because a reporter left past it cannot tell a +busy week from a report that never arrived. + +What I can do is limited, and stating it exactly is better than implying more: +ask somebody to stop, edit or delete content on this repository, close or lock a +thread, block an account from this repository, and report an account to GitHub. +None of that reaches anybody outside this repository and none of it is a sanction +anywhere else. + +Your name does not appear in whatever follows unless you ask for it to. Where +something has to be said in public it is said about the behaviour and the +outcome. + +## Where this document is weak + +**One person holds it.** A report about my own behaviour comes to me, and there +is no second reader, no appeal and no independent body. That is the honest state +of a board with one holder rather than an arrangement I am recommending, and it +is why the route that does not go through me is written here rather than left to +be found: + + https://github.com/contact/report-abuse + +**Nothing enforces this.** No check in this repository reads this file and none +could. Every leg of the run judges bytes in the tree, and how somebody spoke to +somebody else is not a byte in the tree. What stands behind this document is that +I do what it says, and the way to find out that I did not is to report it. A +document that looked enforced would be worse than this sentence. + +**It is not the Contributor Covenant**, and that is deliberate. That document's +enforcement section describes a body of people, a ladder of consequences and a +review of appeals. None of those exists here, and publishing a ladder nobody +climbs would describe an apparatus this board does not have, which is the failure +the paragraph above is written against. diff --git a/SECURITY.md b/SECURITY.md index 73bcdf7..f3e24fd 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,24 +4,34 @@ Report privately, through the form under Security on this repository: https://github.com/Flowfin/lab/security/advisories/new -The route is a repository setting rather than a file, and it does not answer -today: +That is the whole channel. A report sent anywhere else is not received: no +mailbox is published for this board, and there is no second private route to +fall back to. A report about somebody's behaviour rather than about the software +takes the same form, and +[CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) is where that is written down. + +The route is a repository setting rather than a file, and this paragraph said +until now that it does not answer. It answers: gh api repos/Flowfin/lab/private-vulnerability-reporting - {"enabled":false} + {"enabled":true} + +Run 2026-09-05, against the same repository as the earlier reading of the same +command, which returned `{"enabled":false}` on 2026-08-19 and is what the +paragraph here was written around. The address did not move when the setting +changed, which is why it was safe to publish while the door was shut. -Run 2026-08-19. So this file names the destination and says plainly that it is -shut, rather than sending a reporter to a door that does not open. The address -is the one the form uses once the setting is on, so nothing here moves when it -changes. Until then the honest alternative is a public issue carrying as little -detail as the report can carry: the software, the shape of the problem, and an -offer to send the rest privately. That is a poor arrangement and is written -down as one. Issue #10 is where this file lands and where the gap belongs. +WHAT THAT ENDS IS THE ALTERNATIVE THIS FILE USED TO OFFER. It said that until +the form opened, the honest fallback was a public issue carrying as little +detail as a report can carry. There is no such fallback now and there should not +be one: a public issue about a vulnerability is the outcome a security policy +exists to prevent, and it was written down here as a poor arrangement while it +was the only one. Do not open one. The form above is where a report goes. -With no policy in this repository, what a reader has been shown until now is -the organisation default, which says the private form is enabled on every -repository here and that a first answer arrives within a few days. Neither is -true of this repository today. This file is what governs this one. +The organisation default policy is still served on repositories in this +organisation that carry no policy of their own, and it says the private form is +enabled on every repository here. That is now true of this one. This file is +what governs this one, and where the two differ this one wins. ## What is here, because the description undersells it @@ -62,12 +72,23 @@ somebody's licence is in scope. A workflow that grants more than its job needs, or that can be made to run a fork's code with a token. Nothing here is triggered by `pull_request_target` or -`workflow_run`, which I checked across all thirteen files in +`workflow_run`, which I checked across all fifteen files in `.github/workflows` today, every workflow declares an empty or read-only scope at the top level, `persist-credentials` is off at every checkout, and the actions are pinned by commit. Anything that undoes one of those is worth reporting before something uses it. +A published artefact that does not match what this board says it published. +There is a release now, and every file in it is covered by `SHA256SUMS` with +`SHA256SUMS.sig` over that file, verified against the signing keys the platform +publishes for the account that cut it rather than against a key shipped beside +the signature. The release notes carry the two commands. A downloaded file whose +digest does not match its line, a checksum file whose signature does not verify, +or a release asset that appeared or changed after the release was published, are +each a report I want. Which of those is a compromise and which is a mistake on my +side is not something a reader can tell from outside, and that is exactly why it +comes here rather than being assumed to be the second. + A break in a claim this repository publishes. It says the runner opens no network connection and writes nothing to the tree it walks, and `cmd/lab/network_test.go` holds the first of those. Making either sentence @@ -136,10 +157,20 @@ There is no server, no socket, no account, no session, no database and no stored personal data anywhere in this repository. The runner reads files and prints what it found, so most of what a reader arrives with does not exist here: nothing to log in to, nothing to escalate into, nothing to enumerate, and -no request path to inject anything into. Nothing is published as a binary -either. `gh api repos/Flowfin/lab/releases` and `gh api repos/Flowfin/lab/tags` -both answer with an empty list today, so there is no artefact anybody -downloaded and none to have been tampered with. +no request path to inject anything into. + +WHAT THIS PARAGRAPH USED TO SAY NEXT WAS THAT NOTHING IS PUBLISHED AS A BINARY, +AND IT QUOTED TWO EMPTY LISTS FOR IT. Both answer with one entry now: + + gh api repos/Flowfin/lab/releases --jq 'length' + 1 + gh api repos/Flowfin/lab/tags --jq 'length' + 1 + +Run 2026-09-05. So there is an artefact somebody can have downloaded, and the +sentence saying there is none to have been tampered with has stopped being true. +What replaces it is the paragraph above about a published artefact, which is a +report I want rather than a class this file waves away. A refusal you disagree with is not a vulnerability, and neither is a check that is too strict about a legitimate tree, nor a supply-chain score lower than you @@ -163,6 +194,11 @@ window, so stating one would be a promise that goes quietly wrong on the first busy week, and a reporter told to expect an answer by a date who does not get one is left guessing whether the report arrived at all. -What is covered is the default branch as it stands. There are no releases and -no tags, no version is maintained in parallel, and nothing to backport a fix -to: a fix is a commit on `main`, and whoever wants it takes a newer checkout. +What is covered is the default branch as it stands, and the most recent release. +This paragraph said there were no releases and no tags; there is one of each, and +the reading is in the section above. No version is maintained in parallel and +there is nothing to backport a fix to: a fix is a commit on `main`, the release +after it carries the fix, and whoever wants it takes a newer checkout or a newer +artefact. An older release is not patched in place and its assets are not +replaced, because replacing a file that a published checksum and signature +already cover is the shape a reader has no way to tell from tampering. From 205201a97690df0326d61cbbe969d949345d136e Mon Sep 17 00:00:00 2001 From: Nils Lehnen <30603423+iderex@users.noreply.github.com> Date: Sat, 5 Sep 2026 09:29:04 +0200 Subject: [PATCH 2/2] Start the changelog now that there is a release to write an entry for (#44) There is a release and a tag on this board, which is what #44 has been waiting on since it was opened: gh api repos/Flowfin/lab/releases --jq 'length' 1 gh api repos/Flowfin/lab/tags --jq 'length' 1 Somebody now holds a version of this runner, and until this commit the tree answered neither of the two questions that produces. Which version do I have was answered by the binary already. What changed was answered by nothing, and the board is allowed to break its own tool between versions, which is exactly why that has to be written down rather than left to a reader comparing two commits. CHANGELOG.md carries an entry for v0.1.0 and the compatibility position in front of it. The position is that there is no promise of stability and that a change is not made silently: a change invalidating records already on the default branch is announced in the entry for the release that carries it, the record under docs/decisions/ is still where such a change is decided, and the exit codes are the one part of the runner anything may key on. The entry reads the published artefact rather than a build made here. The digest was checked against SHA256SUMS and the signature over that file verified against the keys the platform publishes for the account, before the version output in the entry was taken from the binary that came out of it. docs/operator-guide.md said in the present tense that there is no changelog in this repository and no release for one to carry an entry for, so that a format change was announced by the record deciding it and by nothing else. That is the sentence #44 says moves into the changelog when one lands, and this is it moving: the page now points at CHANGELOG.md as the place somebody running a version finds out about a change. That pointer carries a guard rather than resting on somebody noticing, and I proved it bites rather than reading the code for it. The invariants path leg resolves a link naming a file above the document, which is the reading a document under docs/ needs to reach the root. With CHANGELOG.md moved out of the tree: go test ./internal/invariants -run TestThisRepositorySatisfiesTheInvariants -count=1 --- FAIL: TestThisRepositorySatisfiesTheInvariants docs/operator-guide.md: it names CHANGELOG.md, which is not in this tree (document-names-a-path-that-does-not-resolve) and green again with it back. The path in that output is written with the separators the host uses; this machine is on Windows. `lab check .` does not refuse it and I checked that too before writing this: the path leg is a repository invariant rather than a record check, so the walk stays green over a document pointing at nothing. Naming the wrong route here would have been a claim that a route nobody runs holds the pointer. Signed-off-by: Nils Lehnen <30603423+iderex@users.noreply.github.com> --- CHANGELOG.md | 84 ++++++++++++++++++++++++++++++++++++++++++ docs/operator-guide.md | 11 ++++-- 2 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..396a635 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,84 @@ +# Changelog + +One entry per release, newest first. An entry says what changed and what it +means for somebody moving from the release before it. + +## What this board promises about compatibility + +Nothing, and that is the useful answer rather than an evasive one. This is a +board for experiments that do not have to finish, the runner exists to read this +repository's own records, and +[CONTRIBUTING.md](CONTRIBUTING.md) says that nothing on another board is allowed +to depend on anything here. A version number implying guarantees I do not intend +to keep would be worse than no number at all. + +What I do undertake is that a change is not made silently. That is the one thing +publishing an artefact creates that cannot be withdrawn afterwards, which +[docs/decisions/0021-what-this-board-publishes.md](docs/decisions/0021-what-this-board-publishes.md) +names as a cost of publishing at all: once somebody is running a version, they +have an expectation of continuity whether or not anybody offered them one. This +file is what that expectation is answered with. + +The experiment record format may change. A change to it is decided in a record +under [docs/decisions/](docs/decisions/), and what happens to the records already +on the default branch on the day it changes is +[docs/decisions/0013-how-the-record-format-changes.md](docs/decisions/0013-how-the-record-format-changes.md), +which is where that was decided rather than restated here. A change that would +invalidate records already on the default branch is announced in the entry for +the release that carries it, under a heading naming it as such, and this file is +where somebody upgrading looks for that heading. + +The exit codes the runner returns are the one part of it anything may key on, +and they are a contract in +[docs/decisions/0011-the-exit-codes.md](docs/decisions/0011-the-exit-codes.md). +The command interface around them is not a contract: a verb may be renamed and +the text a verb prints may be rewritten, and neither is announced anywhere except +here. + +## v0.1.0 - 2026-09-04 + +The first release, so there is nothing to upgrade from and no behaviour that +changed. What this entry is for is saying what a downloaded file now is, since +until this tag the only way to run the runner was to build it out of the +repository it checks, which is a weaker position to check from. + +**What is published.** A binary for each of the six platforms +[docs/decisions/0012-the-supported-platforms.md](docs/decisions/0012-the-supported-platforms.md) +fixes, with `NOTICE.md`, `LICENSE` and `privacy.md` beside them, plus the +third-party notices and the bill of materials rendered from the module table +inside a published binary. `SHA256SUMS` covers every one of those files and +`SHA256SUMS.sig` covers `SHA256SUMS`. The notes on the release carry the two +commands that check a download, and the signing keys they verify against are the +ones the platform publishes for the account rather than a key shipped in the +release. + +**What the binary reports about itself.** `lab version` prints the version the +toolchain stamped from version control at build time rather than a constant +written into a source file, so the published binary reports the tag. Read off +the published asset rather than off a build made here, after checking its digest +against the line for it in `SHA256SUMS`: + + sha256sum --check --ignore-missing SHA256SUMS + lab_v0.1.0_windows_amd64.exe: OK + + ./lab_v0.1.0_windows_amd64.exe version + lab v0.1.0 + built from commit ed16621442c00a3ad47edd918d68edf68341753e, 2026-09-01T09:14:10Z + +A build from an ordinary checkout reports a version derived from the commit +instead, a build from a tree carrying changes version control does not hold says +so on its own line, and the output explains which of the three it is handing you. + +**Three things this release is not**, none of them new here. It is not something +anybody is asked to install, it is not advertised, and nothing on another board +depends on it, which is the scope +[CONTRIBUTING.md](CONTRIBUTING.md) opens with and which the release notes repeat +to whoever is holding the file rather than reading the repository. Who the +operator of an artefact is meant to be, and what publishing one costs, is +[docs/decisions/0021-what-this-board-publishes.md](docs/decisions/0021-what-this-board-publishes.md). + +**What the archives do not contain.** There are no archives. The files travel as +separate assets beside the binaries, because an archive records a modification +time per entry and two runs from one tag would then produce two archives whose +bytes differ while the files inside them are identical, which would make the +reproducibility claim a claim about the archiver. diff --git a/docs/operator-guide.md b/docs/operator-guide.md index 136ac68..6af38ec 100644 --- a/docs/operator-guide.md +++ b/docs/operator-guide.md @@ -289,6 +289,11 @@ already on the default branch on the day it changes is [docs/decisions/0013-how-the-record-format-changes.md](decisions/0013-how-the-record-format-changes.md), which is where that was decided, rather than restated here. -There is no changelog in this repository and no release for one to carry an -entry for, so today a change to the format is announced by the record that -decides it and by nothing else. +This page said until now that there is no changelog in this repository and no +release for one to carry an entry for, so that a change to the format was +announced by the record that decides it and by nothing else. Both halves have +ended. [CHANGELOG.md](../CHANGELOG.md) is at the root of the checkout, it carries +an entry for the first release, and it is where a change that would invalidate +records already on the default branch is announced. The record under +docs/decisions/ is still where such a change is decided; the changelog is where +somebody running a version finds out about it.