|
| 1 | +# ADR-0035 — A privacy claim is measured, not asserted; and this repository stays public |
| 2 | + |
| 3 | +- **Date:** 2026-09-21 |
| 4 | +- **Status:** accepted |
| 5 | +- **Area:** disclosure, plan, credentials |
| 6 | +- **Depends on:** ADR-0031 (Actions-first delivery), ADR-0033 (the collector refuses rather than degrades) |
| 7 | +- **Evidence:** `docs/decisions/2026-09-21-delivery-architecture/` E-06 and E-08, plus the measurement below |
| 8 | + |
| 9 | +## Context |
| 10 | + |
| 11 | +Two questions were open and had been answered in prose rather than by checking. |
| 12 | + |
| 13 | +**"Should this repository go private for sensitive research?"** ADR-0031 recorded the |
| 14 | +constraint and nothing acted on it. **"What does a public run expose?"** The collector's own |
| 15 | +header answered it, confidently and wrongly: *"`workflow_dispatch` inputs are themselves |
| 16 | +visible on the run page to anyone with read access."* |
| 17 | + |
| 18 | +That sentence was written by me, from reasoning, and never tested. |
| 19 | + |
| 20 | +## The measurement |
| 21 | + |
| 22 | +Unauthenticated requests against this repository, run `35608301287`, on 2026-09-21. The |
| 23 | +topic was `GitHub Actions workflow artifact retention period`, and every response body was |
| 24 | +searched for it: |
| 25 | + |
| 26 | +| probe | status | topic present | |
| 27 | +|---|---|---| |
| 28 | +| run metadata | `200` readable | no | |
| 29 | +| jobs and step names | `200` readable | no | |
| 30 | +| run timing | `200` readable | no | |
| 31 | +| artifact **listing** | `200` readable | no | |
| 32 | +| repository secrets | `401` refused | — | |
| 33 | +| artifact **download** | `401` refused | — | |
| 34 | +| job **logs** | `403` refused | — | |
| 35 | + |
| 36 | +**A stranger learns the shape of a run and not its subject.** That one happened, when, how |
| 37 | +long it took, what its steps were called, that an artifact of N bytes exists with a given |
| 38 | +name — and nothing about what was researched or what came back. |
| 39 | + |
| 40 | +The claim in the header was wrong in the direction that feels safe, which is still wrong. |
| 41 | +An overstated warning is the same defect as an understated one: a reader who finds a |
| 42 | +warning false once discounts the next, and the next may be the true one. |
| 43 | + |
| 44 | +## Decision |
| 45 | + |
| 46 | +### 1. This repository stays public |
| 47 | + |
| 48 | +Going private is the reflex answer and it is the wrong one here, for a reason that is |
| 49 | +recorded rather than assumed. This account is on the **free** plan — checked, not inferred: |
| 50 | +`gh api user --jq .plan.name` returns `free`. Per E-06, converting a GitHub Free repository |
| 51 | +from public to private makes its protection rules **and environment secrets** *ignored* |
| 52 | +rather than refused. |
| 53 | + |
| 54 | +The collector's credential lives in the `research-collection` environment. Going private |
| 55 | +would make it unreadable, the collector would fail its credential check, and the failure |
| 56 | +would look like a broken workflow rather than a plan limitation. The next move — adding the |
| 57 | +key as a repository secret to "fix" it — is refused by the `scope-check` canary, correctly |
| 58 | +and confusingly. |
| 59 | + |
| 60 | +So privacy for sensitive subjects is a **plan change**, not a visibility toggle, and it is |
| 61 | +not needed for what is measured above. |
| 62 | + |
| 63 | +### 2. The naming rules are the primary control, not a minor one |
| 64 | + |
| 65 | +The measurement says the names are the disclosure surface. Everything the workflow does to |
| 66 | +keep the topic out of `run-name` and the artifact name is therefore the main defence rather |
| 67 | +than a nicety, and `client_ref` being public (it becomes both names) is the sharpest edge |
| 68 | +in the design. |
| 69 | + |
| 70 | +### 3. The claim becomes executable |
| 71 | + |
| 72 | +`lib/disclosure.mjs` and `bin/disclosure.mjs` make the measurement repeatable by anyone: |
| 73 | + |
| 74 | +``` |
| 75 | +node research-kit/bin/disclosure.mjs --repository OWNER/REPO --run <id> --topic "..." |
| 76 | +``` |
| 77 | + |
| 78 | +Unauthenticated by construction — there is no token parameter, and an unknown flag matching |
| 79 | +token/auth/key/secret is refused with that reason, because a probe that quietly |
| 80 | +authenticated would answer a different question while looking like this one. |
| 81 | + |
| 82 | +Exit codes are the finding: `0` shape only, `1` content readable, `2` subject leaked, `3` |
| 83 | +could not measure. A documentation paragraph goes stale silently; a command does not. |
| 84 | + |
| 85 | +### 4. An unchecked thing is never reported as a clean thing |
| 86 | + |
| 87 | +`needleChecked` exists so that "the subject was not exposed" cannot be printed when no |
| 88 | +subject was searched for. Without `--topic`, the report says **not checked**. An |
| 89 | +unreachable endpoint is likewise a refusal to conclude, not a pass. This is the same |
| 90 | +failure the ADR is about, one level down: the probe must not do to its reader what the |
| 91 | +header did to me. |
| 92 | + |
| 93 | +## What is NOT measured, and is therefore not claimed |
| 94 | + |
| 95 | +**What a signed-in user sees in the web UI.** That is a different surface from the REST |
| 96 | +API, and it could not be tested from here — the browser available to this session cannot |
| 97 | +reach github.com. The logs contain the topic (the step prints `TOPIC:` in its `env:` |
| 98 | +group), and the API refuses them to a stranger with `403 Must have admin rights to |
| 99 | +Repository`. Whether the UI is equally strict for a signed-in reader is **unknown**, and |
| 100 | +"unknown" is what the documentation now says rather than "safe". |
| 101 | + |
| 102 | +## Alternatives considered |
| 103 | + |
| 104 | +**Go private anyway, to be safe.** Breaks the collector on this plan, for an exposure the |
| 105 | +measurement does not show. |
| 106 | + |
| 107 | +**Reword the warning.** Cheaper, and it would have left the next claim just as unchecked. |
| 108 | +The problem was not the wording. |
| 109 | + |
| 110 | +**Refuse to run when the topic looks sensitive.** A judgement about meaning, which a regex |
| 111 | +cannot make — the same reason `client_ref` warns rather than refuses. |
| 112 | + |
| 113 | +## Consequences |
| 114 | + |
| 115 | +- `collect.yml`, `README.md` and `research-kit/README.md` now carry the measured table and |
| 116 | + the command, and each states what was not measured. |
| 117 | +- Both corrected passages **quote the claim they replaced**. A correction that erases what |
| 118 | + it corrected teaches nobody, and this one is the second time in two days that something |
| 119 | + built from documentation turned out untrue when run. |
| 120 | +- `bin/disclosure.mjs` is read-only and unauthenticated, so it is safe to run against a |
| 121 | + repository you do not own — which is also the only way to check somebody else's claim |
| 122 | + about their own exposure. |
0 commit comments