Fix access inventory evidence and reconcile closure coordination

Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a11beb-4eef-7fc1-94a7-597130a01ae9
This commit is contained in:
tegwick 2026-10-09 00:23:04 +02:00
parent 490a89dca6
commit 57ad4abefc
9 changed files with 342 additions and 111 deletions

View file

@ -6,8 +6,11 @@ Dev Hub (State Hub API): http://127.0.0.1:8000
This project is **deliberately deferred**. Read `GOAL.md` "Why this is
deferred" and the `ORGREF-WP-0001` entry gate before proposing execution work.
T01 and T02 are read-only and safe to advance at any time; **T03 onward must
not start until all four entry-gate conditions hold.**
Within ORGREF-WP-0001, T01 and T02 are read-only and safe to advance at any
time; **ORGREF-WP-0001-T03 onward must not start until all four entry-gate
conditions hold.** ORGREF-WP-0002 coordinates access control independently,
under its own accepted policy, owner boundaries and visibility-wave gates.
It does not authorize repository transfers or waive the org-migration gate.
If you believe the gate is met, say which of the four conditions changed and
what evidence shows it — do not infer readiness from elapsed time.

12
GOAL.md
View file

@ -3,7 +3,7 @@ repo: prj-forgejo-org-refactor
repo_flavor: project
project_status: draft
started: "2026-08-11"
reviewed: "2026-09-28"
reviewed: "2026-10-09"
---
# Project goal
@ -42,6 +42,16 @@ dependency between the rollback prerequisite and T04. Preparation artifacts:
[inventory](inventory/README.md), [taxonomy](taxonomy/README.md), and
[preparation record](history/2026-09-28-preparation.md).
## Access-control coordination — 2026-10-09
ORGREF-WP-0002 independently coordinates the accepted October 8 policy: private
forge repositories and packages, external deliberate publication, authenticated
consumers and staged visibility changes. T01–T03 have accepted preparation;
identity implementation and closure proofs remain with their owner records.
See [policy](decisions/forge-visibility-policy.md) and the
[October 9 review](history/2026-10-09-review-and-maintenance.md). This work does
not satisfy or waive the four-condition org-transfer entry gate.
## Invariants
1. **No silent breakage.** Every reference that moves is inventoried before it

View file

@ -9,7 +9,7 @@
Temporary coordination repo for splitting the single `coulomb/` Forgejo
organization into stewardship organizations without breaking GitOps, image
pulls, or clones.
pulls, or clones, and coordinating private forge access under ORGREF-WP-0002.
---
@ -32,6 +32,8 @@ by ID; it does not duplicate their tasks.
package registry
- Migration sequencing, rollback plan, and cutover evidence
- Promotion of the durable org-assignment convention to a permanent home
- Forge access-consumer inventory, identity-owner coordination and visibility
sequencing under the accepted ORGREF-WP-0002 policy
---
@ -53,9 +55,10 @@ by ID; it does not duplicate their tasks.
| Repo | Role |
| --- | --- |
| `railiance-forge` | Forgejo runtime owner — executes org/transfer operations |
| `railiance-forge` | Historical forge-owner records; re-confirm ownership before org/transfer operations |
| `key-cape` / `net-kingdom` | OIDC contract and KnownSecurityCase client registration |
| `railiance-platform` | ArgoCD Application sources, OpenBao lanes referencing repo URLs |
| `railiance-apps` | Chart image `repository:` paths and chart metadata |
| `railiance-apps` | Current Forgejo Helm/runtime and runner owner; chart image paths and metadata |
| `railiance-master` | Durable home candidate for the org-assignment convention |
| `the-custodian` | Canon home candidate; repo-classification alignment |
| Current server-backed `coulomb/` repo inventory | Subjects of the migration; 112 was the seed count |
@ -97,3 +100,7 @@ project's workplans, never copied.
both tasks wait on explicit evidence and acceptance gaps. Execution requires the unchanged
`ORGREF-WP-0001` entry gate and resolution of its circular rollback dependency
- Evidence and next sequence: [readiness review](history/2026-09-28-readiness-review.md)
- October 8–9: WP-0002 policy and inventories accepted; identity integration
waits on KEY-WP-0038 / NK-WP-0048 and RAPPS-WP-0016. Private forge access
is independent of the unchanged WP-0001 org-transfer gate.

View file

@ -0,0 +1,57 @@
# Review and maintenance — 2026-10-09
User instruction: check changes and open work, review and implement, then commit
and sync. The checkout began clean and matched origin/main after fetch.
## Open work and authority
ORGREF-WP-0001 remains blocked: T01/T02 wait on authenticated completeness and
stewardship acceptance; T03–T05 retain all four entry conditions. No new evidence
establishes readiness and the rollback-gate amendment remains proposed.
ORGREF-WP-0002 has T01–T03 done and T04–T09 waiting. The October 8 policy settles
private packages, no anonymous forge access, Telegram uploads, the interim
visibility-only guardrails and the agent driver. KEY-WP-0038 has an accepted
claim contract; registration T02 waits on NK-WP-0048-T06, and claims proof T03
is pending. RAPPS-WP-0016 has break-glass preparation T01 todo; authentication
and account/team proof remain waiting. Their stable IDs are linked in this
project's workplan. Owner implementation stays in those repositories.
## Implemented
- Corrected access collector package-index classification: API namespaces do
not establish repository identity. Hostnames remain distinct for legacy-host
disposition, rather than combining old and current forge references.
- Schema 0.2 replaces the unsupported anonymous-pull inference with a count of
pulls without named credentials. Node and service-account authentication
require separate evidence, and named secrets are not proof of working access.
- Made traversal deterministic, excluded the coordinator's generated artifacts,
reused the reference collector's sensitive/binary exclusions, and reported
exclusions and read errors. Symlinks and non-regular files (including FIFOs)
are not followed or read; diagnostics never contain raw exception text.
- Reconciled GOAL/SCOPE and WP-0002 with the accepted policy and actual owner
records. Moved collector authentication proof to T06, before closure; T09
verifies completeness and handback. Retained interim and normal wave gates.
- Qualified the AGENTS.md gate explicitly as WP-0001's gate, consistent with
WP-0002's existing independence. No condition or rollback exception changed.
- Put task headings before their task blocks so projected titles refer to the
correct stable task IDs. Removed obsolete undecided-policy claims and withdrew
unsafe inline Secret/registry inspection commands while preserving the dated
founder results.
## Validation and limits
Ten unit tests pass, covering CLI determinism, separate hosts, package targets,
credential uncertainty, sensitive/binary/symlink/FIFO exclusions, sanitized read
errors and the existing migration inventory checks. Whitespace validation passes.
A scoped CLI replay scanned key-cape and reused the retained October 8 runtime
capture: 10 GitOps consumers, 43 image pulls without named credentials, zero
scan errors. The coordinator was correctly excluded. Temporary output remains
in /tmp; retained accepted evidence was not overwritten.
A fleet-wide replay was interrupted after it did not finish promptly; it is not
complete evidence. The scoped replay is a collector integration check, not a
fresh fleet census, deployable-coverage receipt or unchanged-fleet G1 pair.
No credential requests, Secret reads, owner assignments, forge visibility
changes, transfers or other runtime changes were made. Remaining work stays
with the documented decisions and owner records.

View file

@ -23,7 +23,7 @@ Secrets and node registry configuration were not inspected.
| package index | kaizen-agentic, vergabe-teilnahme | PyPI `simple/` index without credentials | package-read token |
| federation | reuse-surface | about 60 capability indexes by raw URL | read identity, or a published index location |
| code readers | about 8 files, below | clones, API and raw reads without token handling | owner review; re-point to an authenticated path |
| campaign images | pr-hall-of-helix, fluid-telegram | Telegram fetching post images by raw URL | a deliberate public asset location (ORGREF-WP-0002-T01 point 4) |
| campaign images | pr-hall-of-helix, fluid-telegram | Telegram fetching post images by raw URL | adapter fetches privately and uploads to Telegram (accepted policy; FT-WP-0001-T14) |
Lower risk: `repo-gitops` and `repo-image-reference` are the cluster consumers
seen from their source repos. `doc-link` means links break for the public, not
@ -71,29 +71,15 @@ URLs and whynot-design's SSH `repository` field.
All 160 local clones use SSH with the founder's key (`forgejo-remote`). They keep
working as long as that account can read the repositories.
## Founder checks (presence only, no secret values)
## Historical founder checks
Agents do not read Secrets, so run these in your own terminal and paste the output.
1. ArgoCD repository credentials, by name, type and URL only:
```bash
kubectl -n argocd get secrets \
-l 'argocd.argoproj.io/secret-type in (repository,repo-creds)' \
-o go-template='{{range .items}}{{.metadata.name}} {{index .metadata.labels "argocd.argoproj.io/secret-type"}} {{if .data.url}}{{.data.url | base64decode}}{{end}}{{"\n"}}{{end}}'
```
2. k3s registry configuration on railiance01, as key names with every value
stripped:
```bash
ssh railiance01 'sudo test -f /etc/rancher/k3s/registries.yaml \
&& sudo sed -nE "s/^([[:space:]]*[A-Za-z0-9_.\"-]+:).*/\1/p" /etc/rancher/k3s/registries.yaml \
|| echo absent'
```
`absent`, or no `auth:` under the forge host, means all 43 pulls depend on
anonymous registry access today.
The October 8 results below are preserved as dated owner evidence. The original
inline Secret template and registry-value-stripping commands are withdrawn:
the estate forbids standalone go-template/jsonpath against real Secrets, and
text stripping is not a reliable boundary for registry credentials. Do not
repeat them. Obtain a current sanitized receipt from the platform/node owner;
Secret annotation checks use the output-suppressing platform maintenance helper
specified in the-custodian's agent environment orientation, section 6.
### Results, 2026-10-08 (run by the founder)
@ -114,3 +100,20 @@ Agents do not read Secrets, so run these in your own terminal and paste the outp
- Consumers outside these working trees (external sites and links, other
people's clones) cannot be found by scanning; T08's anonymous probe after each
wave is the backstop.
## Collector corrections — 2026-10-09
The retained October 8 JSON uses schema 0.1. Its `anonymous_image_pulls` field
was an inference from absent pod pull-secret names; only the separate founder
node-auth check supports the anonymous-access conclusion for that snapshot.
Do not reuse this inference for another cluster or capture.
Schema 0.2 reports `image_pulls_without_named_credentials`, with access marked
unknown until node/service-account credential evidence exists. It preserves
forge hostnames, so legacy references stay separate, and classifies package
API indexes as package consumers with null repository targets (package namespace
is not repository identity). `scan_exclusions` and `scan_error_count` expose
coverage limits; the coordinator is omitted to avoid self-referential output.
Sensitive/encrypted, binary and symlinked paths are not read. The source scan is
deterministic for unchanged inputs, but no claim of full consumer completeness
or deployment state follows from matching outputs.

View file

@ -1,9 +1,14 @@
import sys
import json
import os
import subprocess
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools"))
from access_consumers import classify, runtime_consumers, target_repo # noqa: E402
from access_consumers import classify, runtime_consumers, scan_repo, target_repo # noqa: E402
class ClassifyTest(unittest.TestCase):
@ -18,6 +23,8 @@ class ClassifyTest(unittest.TestCase):
("README.md", "/coulomb/x", "see", "doc-link"),
("docs/evidence/run.json", "/coulomb/x", "x", "record"),
("Makefile", "/coulomb/x", "push", "build-tooling"),
("pyproject.toml", "/api/packages/coulomb/pypi/simple", "url: x", "package-consumer"),
("client.py", "/api/v1/packages/coulomb", "u = ...", "package-consumer"),
]
for f, p, line, want in cases:
self.assertEqual(classify(f, p, line), want, f)
@ -28,16 +35,73 @@ class ClassifyTest(unittest.TestCase):
self.assertEqual(target_repo("/v2/coulomb/flex-auth/manifests/x"), "coulomb/flex-auth")
self.assertEqual(target_repo("/coulomb/activity-core@sha256:ab"), "coulomb/activity-core")
self.assertIsNone(target_repo("/user/login"))
self.assertIsNone(target_repo("/api/packages/coulomb/pypi/simple"))
self.assertIsNone(target_repo("/api/v1/packages/coulomb"))
self.assertIsNone(target_repo("/api/v1/orgs/coulomb/repos"))
def test_runtime_anonymous_pull(self):
def test_missing_pull_secret_does_not_prove_anonymous_access(self):
rt = {"applications": [], "workloads": [{
"namespace": "ns", "kind": "Deployment", "name": "api", "image_pull_secret_names": [],
"images": ["forgejo.coulomb.social/coulomb/x@sha256:00"]}]}
(c,) = runtime_consumers(rt)
self.assertEqual(c["class"], "cluster-image-pull")
self.assertTrue(c["credential"].startswith("anonymous"))
self.assertTrue(c["credential"].startswith("unknown"))
self.assertEqual(c["target"], "coulomb/x")
def test_scan_excludes_sensitive_and_binary_files_without_values(self):
with tempfile.TemporaryDirectory() as d:
repo = Path(d)
(repo / "client.py").write_text('url = "https://user:hidden@forgejo.coulomb.social/coulomb/valid?token=hidden"\n')
(repo / ".env").write_text("forgejo.coulomb.social/coulomb/private-value\n")
(repo / "secret.sops.yaml").write_text("forgejo.coulomb.social/coulomb/encrypted-value\n")
(repo / "binary").write_bytes(b"\0forgejo.coulomb.social/coulomb/binary-value")
(repo / "link.py").symlink_to(repo / "client.py")
os.mkfifo(repo / "pipe")
diagnostics = []
hits = scan_repo(repo, diagnostics)
self.assertEqual([h["target"] for h in hits], ["coulomb/valid"])
serialized = json.dumps(hits)
for value in ["hidden", "private-value", "encrypted-value", "binary-value"]:
self.assertNotIn(value, serialized)
self.assertEqual({x["reason"] for x in diagnostics},
{"sensitive-or-encrypted", "binary-content", "symlink-not-followed", "non-regular-file"})
def test_cli_is_repeatable_and_keeps_legacy_host_separate(self):
with tempfile.TemporaryDirectory() as d:
root = Path(d)
repo = root / "reader"
repo.mkdir()
subprocess.run(["git", "init", "-q", str(repo)], check=True)
(repo / "client.py").write_text(
'a = "https://forgejo.coulomb.social/coulomb/x"\n'
'b = "https://gitea.coulomb.social/coulomb/x"\n')
runtime = root / "runtime.json"
runtime.write_text(json.dumps({"applications": [], "workloads": []}))
assignments = root / "assignments.csv"
assignments.write_text("repository,proposed_org\nx,netkingdom\n")
output = root / "output.json"
command = [sys.executable, str(Path(__file__).resolve().parents[1] / "tools/access_consumers.py"),
"--root", str(root), "--runtime", str(runtime),
"--assignments", str(assignments), "--output", str(output)]
subprocess.run(command, check=True, capture_output=True)
first = output.read_bytes()
subprocess.run(command, check=True, capture_output=True)
self.assertEqual(first, output.read_bytes())
report = json.loads(first)
self.assertEqual({c["host"] for c in report["consumers"]},
{"forgejo.coulomb.social", "gitea.coulomb.social"})
self.assertEqual(report["scan_error_count"], 0)
def test_unreadable_file_is_reported_without_raw_error(self):
with tempfile.TemporaryDirectory() as d:
repo = Path(d)
(repo / "client.py").write_text("placeholder")
diagnostics = []
with patch.object(Path, "read_text", side_effect=OSError("private error detail")):
self.assertEqual(scan_repo(repo, diagnostics), [])
self.assertEqual(diagnostics, [{"repository": repo.name,
"path": "client.py", "reason": "unreadable-file"}])
if __name__ == "__main__":
unittest.main()

View file

@ -17,9 +17,12 @@ import csv
import json
import os
import re
import stat
import subprocess
from pathlib import Path
from inventory import BINARY, SENSITIVE
FORGE_HOSTS = ("forgejo.coulomb.social", "gitea.coulomb.social")
URL = re.compile(
r"(?:https?://)?(?P<host>forgejo\.coulomb\.social|gitea\.coulomb\.social)"
@ -34,6 +37,7 @@ KINDS = [
("record", lambda f, p, line: f.startswith(("docs/evidence/", "evidence/", "tests/", "test/"))
or "/evidence/" in f or "/tests/" in f or f.endswith(("PKG-INFO", ".sum"))),
("raw-fetch", lambda f, p, line: "/raw/" in p or "/media/" in p),
("package-consumer", lambda f, p, line: p.startswith(("/api/packages/", "/api/v1/packages/"))),
("api-client", lambda f, p, line: p.startswith("/api/")),
("ci-action", lambda f, p, line: ".forgejo/workflows" in f or ".gitea/workflows" in f
or ".github/workflows" in f),
@ -65,29 +69,68 @@ def target_repo(path):
parts = [p for p in (path or "").split("/") if p]
if parts[:2] == ["api", "v1"] and len(parts) >= 5 and parts[2] == "repos":
parts = parts[3:]
elif parts and parts[0] in {"api", "v2"}:
elif parts and parts[0] == "api":
# Package paths name an owner/index, not a repository. Other API
# endpoints also cannot establish a repository identity.
return None
elif parts and parts[0] == "v2":
parts = parts[1:]
if len(parts) >= 2 and parts[0] not in {"user", "explore", "assets"}:
return f"{parts[0]}/{parts[1].removesuffix('.git').split('@')[0].split(':')[0]}"
return None
def scan_repo(repo):
def scan_repo(repo, diagnostics=None):
hits = []
for dirpath, dirnames, filenames in os.walk(repo):
dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS and not d.startswith(".")
or d in {".forgejo", ".gitea", ".github"}]
for name in filenames:
if name in SKIP_FILES:
continue
diagnostics = diagnostics if diagnostics is not None else []
def excluded(path, reason):
diagnostics.append({"repository": repo.name, "path": path, "reason": reason})
def walk_error(error):
excluded(str(Path(error.filename).relative_to(repo)), "unreadable-directory")
for dirpath, dirnames, filenames in os.walk(repo, onerror=walk_error):
kept = []
for d in sorted(dirnames):
rel = str((Path(dirpath) / d).relative_to(repo))
if (Path(dirpath) / d).is_symlink():
excluded(rel, "symlink-not-followed")
elif d in SKIP_DIRS or (d.startswith(".") and d not in {".forgejo", ".gitea", ".github"}):
excluded(rel, "excluded-directory")
else:
kept.append(d)
dirnames[:] = kept
for name in sorted(filenames):
full = Path(dirpath) / name
rel = str(full.relative_to(repo))
if name in SKIP_FILES:
excluded(rel, "generated-work-record")
continue
if SENSITIVE.search(rel):
excluded(rel, "sensitive-or-encrypted")
continue
if full.suffix.lower() in BINARY:
excluded(rel, "binary-extension")
continue
try:
if full.is_symlink() or full.stat().st_size > MAX_BYTES:
if full.is_symlink():
excluded(rel, "symlink-not-followed")
continue
info = full.stat()
if not stat.S_ISREG(info.st_mode):
excluded(rel, "non-regular-file")
continue
if info.st_size > MAX_BYTES:
excluded(rel, "over-1MB")
continue
text = full.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
except UnicodeDecodeError:
excluded(rel, "non-utf8-content")
continue
except OSError:
excluded(rel, "unreadable-file")
continue
if "\0" in text:
excluded(rel, "binary-content")
continue
rel = str(full.relative_to(repo))
for number, line in enumerate(text.splitlines(), 1):
for m in URL.finditer(line):
hits.append({
@ -119,7 +162,7 @@ def runtime_consumers(runtime):
m = URL.search(u)
if m:
out.append({"consumer": f"argocd:{app['name']}", "class": "cluster-gitops",
"kind": "gitops-source", "target": target_repo(m.group("path")),
"host": m.group("host"), "kind": "gitops-source", "target": target_repo(m.group("path")),
"credential": "unknown (ArgoCD repo credentials not inspected)"})
for w in runtime.get("workloads", []):
for image in w.get("images", []):
@ -128,9 +171,10 @@ def runtime_consumers(runtime):
secrets = w.get("image_pull_secret_names") or []
out.append({"consumer": f"k3s:{w['namespace']}/{w['kind']}/{w['name']}",
"class": "cluster-image-pull", "kind": "image-pull",
"host": m.group("host"),
"target": target_repo(m.group("path")),
"credential": "pull secret named: " + ",".join(secrets)
if secrets else "anonymous (no imagePullSecrets)"})
if secrets else "unknown (no imagePullSecrets; node registry auth not inspected)"})
return out
@ -160,17 +204,19 @@ def main():
consumers = runtime_consumers(runtime)
remotes = collections.Counter()
scanned = []
diagnostics = []
coordinator = Path(__file__).resolve().parents[1]
for repo in sorted(Path(args.root).iterdir()):
if not (repo / ".git").exists() or repo.name.startswith("rescued-"):
if not (repo / ".git").exists() or repo.name.startswith("rescued-") or repo.resolve() == coordinator:
continue
scanned.append(repo.name)
remotes[remote_form(repo) or "none"] += 1
grouped = collections.defaultdict(list)
for h in scan_repo(repo):
grouped[(h["kind"], h["target"])].append(f"{h['file']}:{h['line']}")
for (kind, target), where in sorted(grouped.items(), key=lambda kv: (kv[0][0], str(kv[0][1]))):
for h in scan_repo(repo, diagnostics):
grouped[(h["host"], h["kind"], h["target"])].append(f"{h['file']}:{h['line']}")
for (host, kind, target), where in sorted(grouped.items(), key=lambda kv: (kv[0][0], kv[0][1], str(kv[0][2]))):
consumers.append({"consumer": f"repo:{repo.name}", "class": CLASS_OF_KIND[kind],
"kind": kind, "target": target, "locations": where[:10],
"host": host, "kind": kind, "target": target, "locations": where[:10],
"location_count": len(where)})
for c in consumers:
@ -179,19 +225,28 @@ def main():
summary = collections.Counter(c["class"] for c in consumers)
report = {
"schema_version": "0.1",
"schema_version": "0.2",
"workplan": "ORGREF-WP-0002-T02",
"runtime_capture": os.path.basename(args.runtime),
"scanned_repositories": len(scanned),
"local_remote_forms": dict(remotes),
"summary_by_class": dict(sorted(summary.items())),
"anonymous_image_pulls": sum(1 for c in consumers if c["class"] == "cluster-image-pull"
and c["credential"].startswith("anonymous")),
"image_pulls_without_named_credentials": sum(1 for c in consumers if c["class"] == "cluster-image-pull"
and c["credential"].startswith("unknown")),
"consumers": consumers,
"scan_exclusions": diagnostics,
"scan_error_count": sum(d["reason"].startswith("unreadable") for d in diagnostics),
"limits": [
"Runtime classes come from the runtime capture named above, not a live read.",
"ArgoCD repository credentials and pull-secret contents were not inspected; "
"a named pull secret is not proof that it authenticates to the forge.",
"Missing imagePullSecrets do not prove anonymous pulls: node-level registry "
"auth and service-account defaults require owner evidence.",
"Sensitive/encrypted, binary, large, symlinked and excluded paths are recorded "
"in scan_exclusions; unreadable paths prevent a completeness claim.",
"The coordinator is excluded to prevent generated inventory self-reference.",
"Hostnames are preserved so legacy-host references can be reviewed separately.",
"Package indexes name a namespace, not a verified repository; their target is null.",
"Only literal forge hostnames are found; computed URLs and consumers outside "
"these working trees (external sites, Telegram, other people's clones) are not.",
"history/ and archived/ directories are skipped; they record, they do not read.",
@ -199,7 +254,8 @@ def main():
}
Path(args.output).write_text(json.dumps(report, indent=1, sort_keys=False) + "\n")
print(json.dumps({k: report[k] for k in ("scanned_repositories", "local_remote_forms",
"summary_by_class", "anonymous_image_pulls")},
"summary_by_class", "image_pulls_without_named_credentials",
"scan_error_count")},
indent=1))

View file

@ -102,6 +102,8 @@ It must not:
## Tasks
### T01 — Build the repeatable reference inventory
```task
id: ORGREF-WP-0001-T01
status: wait
@ -159,6 +161,8 @@ in `railiance-platform/argocd/applications/`; `repository:
forgejo.coulomb.social/coulomb/core-hub` in `railiance-apps/charts/core-hub/values.yaml`;
`home:`/`sources:` URLs across at least five charts.
### T02 — Agree stewardship criteria and taxonomy
```task
id: ORGREF-WP-0001-T02
status: wait
@ -200,6 +204,8 @@ home, both pending acceptance. Decision
whynot as an additional steward, and records evidence for ops-hub/disaster-control.
Naming and maintainer acceptance remain pending.
### T03 — Verify Git and package transfer behavior
```task
id: ORGREF-WP-0001-T03
status: wait
@ -222,6 +228,8 @@ documentation — the container-registry path is the dangerous one because its
failure is deferred to the next pod reschedule rather than surfacing at rename
time.
### T04 — Prove transfer rollback
```task
id: ORGREF-WP-0001-T04
status: wait
@ -240,6 +248,8 @@ Record exact operation, product/version, old/new paths, uncached pulls, GitOps
revision/health, retention protections, credentials scope, cleanup and recovery
time. Unrelated application rollout rollback is not acceptable transfer proof.
### T05 — Write the staged cutover plan
```task
id: ORGREF-WP-0001-T05
status: wait

View file

@ -9,7 +9,7 @@ flavor: implementation
owner: claude-code
topic_slug: railiance
created: "2026-10-08"
updated: "2026-10-08"
updated: "2026-10-09"
depends_on: []
related_repos:
- key-cape
@ -48,9 +48,10 @@ does not apply. It changes **who may read**, not where things live.
It reuses ORGREF-WP-0001's preparation: the `inventory/` collectors, the
stewardship assignments in `taxonomy/` (as owners for each consumer) and the
reference scan. It also gives back to ORGREF-WP-0001: once the forge is closed,
the anonymous collector can no longer see private repositories, so T09 moves it
to an authenticated read identity. That resolves the access half of decision
`5f3680fc` (authenticated metadata).
the anonymous collector can no longer see private repositories. T06 must prove
its authenticated read identity before closure; T09 verifies the handback.
Accepted complete metadata evidence can resolve the access half of decision
`5f3680fc`; having a token alone does not establish completeness.
## Target state
@ -58,14 +59,15 @@ to an authenticated read identity. That resolves the access half of decision
use NetKingdom-issued identities or scoped Forgejo tokens with an owner and a
rotation path. A local administrator account remains only as sealed
break-glass, with its credentials in OpenBao.
- Organisations and repositories are **private by default**. Public visibility
is an explicit, recorded allowlist entry with a reason.
- Organisations, repositories and packages are private, with no anonymous
forge access. Deliberate publication goes to a separate location through the
reviewed per-repository `public/` convention.
- Every consumer that read the forge anonymously reads it with its own
credential, with least privilege, before its repositories close.
- Campaign images that must stay world-readable (Telegram posts fetch them by
URL) come from a deliberate public asset location, not from a forge repo that
happens to be public.
- Anonymous probes show only the allowlist; the evidence is recorded.
- The fluid-telegram adapter reads private images with its own identity and
uploads them to Telegram (FT-WP-0001-T14). A public asset store is later work.
- Anonymous probes show no forge repositories or packages after the final wave;
the evidence is recorded.
## Boundaries
@ -80,7 +82,9 @@ It must not:
- configure key-cape, Forgejo, OpenBao or any consumer itself. Each change lands
through the owner repo's own workplan.
- flip a production repository's visibility before T07's rehearsal passes and
the founder approves that wave
the founder approves that wave, except the separately approved visibility-only
interim route in `decisions/forge-visibility-policy.md` section 5, whose clone,
sign-in, recovery and one-repository rollback checks remain mandatory
- read or print secret values (credential routing: `warden route`)
- move, rename or restructure repositories (that is ORGREF-WP-0001)
@ -90,6 +94,8 @@ separate review, not something the flip repairs.
## Tasks
### T01 — Decide the visibility policy
```task
id: ORGREF-WP-0002-T01
status: done
@ -97,8 +103,6 @@ priority: high
state_hub_task_id: "6387cb32-dd73-5ee0-b51e-d4aea475bcca"
```
### T01 — Decide the visibility policy
Record a founder decision covering:
1. **Default:** private for organisations and repositories, plus whether
@ -117,7 +121,7 @@ Record a founder decision covering:
now with existing local accounts. If yes: which ones, accepting that their
consumers move to local tokens temporarily and are re-homed in T06.
2026-10-08: points 1 and 2 decided, recorded in
2026-10-08: all six policy points and the driver role decided, recorded in
`decisions/forge-visibility-policy.md`. The forge is private with no anonymous
access, and public codebases go to a separate location. Publication is a
per-repo `public/` folder of symlinks only, projected outward by a shared
@ -129,9 +133,14 @@ trust management comes later, not now. Points 5 and 6 decided: interim
lockdown of 13 repositories under the founder's guardrail (tighten gradually,
no lockout, no asset loss, nothing hard to repair: clones first, one repository,
verify and roll back once, then small batches, visibility only). T03 runs in
parallel. Pre-check: executor-sandbox (diverged, unpushed local commits) and
ops-mason (uncommitted changes) wait for their owners. Open: point 3 (packages)
and the owner.
parallel. Packages are private; node registry auth, an ArgoCD credential template
and scoped CI/package identities are the accepted approach. An agent drives the
plan; the founder holds policy and production wave approval. Pre-check:
executor-sandbox (diverged, unpushed local commits) and ops-mason (uncommitted
changes) wait for their owners. All policy points are settled; implementation
proof remains outstanding.
### T02 — Inventory every anonymous reader
```task
id: ORGREF-WP-0002-T02
@ -140,8 +149,6 @@ priority: high
state_hub_task_id: "ace26a22-50e2-58c6-b4ad-85d95c284fce"
```
### T02 — Inventory every anonymous reader
Read-only. Extend `inventory/` with an access-consumer inventory: every party
that reads the forge today without credentials, the repositories or packages it
reads, and the owner (from `taxonomy/assignments.csv`) who will give it an
@ -178,6 +185,8 @@ pulls anonymous). One node-level registry auth entry would cover every pull
(T06). The stale Gitea credential on the node is a cleanup item. Code-reader
confirmation moves to the owners under T06.
### T03 — Review what has already been public
```task
id: ORGREF-WP-0002-T03
status: done
@ -185,8 +194,6 @@ priority: high
state_hub_task_id: "9c2ca7be-23f8-58c5-9bfd-e44a26ce4584"
```
### T03 — Review what has already been public
Before closing, and independent of it: run a secret and sensitive-content scan
over the public repositories' history (credentials, tokens, private keys,
internal hostnames or IPs, personal data). Findings go to their owners for
@ -207,19 +214,23 @@ rest is benign (identifiers, digests, test fixtures, placeholders). Record and
limits: `inventory/history-exposure-2026-10-08.md`. The rotation is tracked with
the owners.
### T04 — NetKingdom: Forgejo as an OIDC client (child workplan in key-cape)
```task
id: ORGREF-WP-0002-T04
status: wait
depends_on: [ORGREF-WP-0002-T01]
blocking_reason: "The NetKingdom side follows the visibility and identity decisions; open the key-cape child workplan once T01 is resolved."
depends_on: [ORGREF-WP-0002-T01, KEY-WP-0038-T03]
blocking_reason: "KEY-WP-0038-T03 must deliver claims evidence; registration T02 waits on NK-WP-0048-T06 (KnownSecurityCase), not an ad-hoc helper."
priority: high
state_hub_task_id: "112617e1-a7c3-59c1-bf25-235d58ec118b"
```
### T04 — NetKingdom: Forgejo as an OIDC client (child workplan in key-cape)
Drafted 2026-10-08 as key-cape `KEY-WP-0038` (ready, founder-approved).
Open a key-cape workplan, linked here by ID, to:
Owner record: key-cape `KEY-WP-0038`
(`098e3e2c-a138-560e-b2d3-3a15eb49d8a9`). Reviewed October 9: T01 claim
contract accepted; T02 registration waits on net-kingdom `NK-WP-0048-T06`
(`d9b26bcf-a7c6-59a2-92a0-f913d76a203c`); T03 claims proof is pending.
The owner uses the accepted KnownSecurityCase route, with ops-mason and
ops-warden, rather than a case-specific script. The owner record covers:
- register Forgejo as a confidential OIDC client (redirect URI, scopes, PKCE)
with its client secret in OpenBao, through the railiance-platform custody path
@ -233,20 +244,22 @@ Open a key-cape workplan, linked here by ID, to:
Acceptance: a test login against a staging client returns the expected claims
for one human, one agent and one service identity.
### T05 — Forgejo: NetKingdom login and team mapping (child workplan in railiance-apps)
```task
id: ORGREF-WP-0002-T05
status: wait
depends_on: [ORGREF-WP-0002-T04]
blocking_reason: "Forgejo can only add the authentication source once the NetKingdom client and claims exist."
depends_on: [ORGREF-WP-0002-T04, RAPPS-WP-0016-T03]
blocking_reason: "RAPPS-WP-0016-T03 must prove account linking and team grants after KEY-WP-0038 delivers the client and claims."
priority: high
state_hub_task_id: "6c635b25-a4ae-5259-9317-de3fe38f6234"
```
### T05 — Forgejo: NetKingdom login and team mapping (child workplan in railiance-apps)
railiance-apps owns the Forgejo Helm release (`docs/forgejo-on-railiance01.md`).
Drafted 2026-10-08 as railiance-apps `RAPPS-WP-0016` (ready, founder-approved).
Open a railiance-apps workplan, linked here by ID, to:
Owner record: railiance-apps `RAPPS-WP-0016`
(`f6b1d6f6-0abb-5774-87b9-bf3cfb640a3b`). Reviewed October 9: break-glass
preparation T01 is todo; authentication, linking and grants remain waiting.
The owner record covers:
- add the NetKingdom OIDC authentication source and claim-to-team mapping;
disable open registration; link existing local accounts (founder, agent
@ -260,6 +273,8 @@ Open a railiance-apps workplan, linked here by ID, to:
Acceptance: the founder and one agent sign in through NetKingdom and see what
their team grants. Break-glass login is verified once and recorded.
### T06 — Give every consumer its own read identity
```task
id: ORGREF-WP-0002-T06
status: wait
@ -269,23 +284,25 @@ priority: high
state_hub_task_id: "1285f94d-3b57-502b-8aeb-96fab9578287"
```
### T06 — Give every consumer its own read identity
For each consumer class in T02, its owner provisions a scoped credential and
proves it works **against a private canary repository and package** created by
this project, while anonymous access to that canary fails:
- cluster: deploy tokens for ArgoCD/Flux repo credentials, `imagePullSecrets`
for registry pulls (railiance-apps, railiance-platform)
- cluster: the accepted ArgoCD repository credential template and node-level
read-only registry login (railiance-apps, railiance-platform and node owner);
prove fresh pulls, including service-account and node credential behavior
- runners: tokens or OIDC for checkout and reusable actions (railiance-apps, which runs `forgejo-runner`)
- agents and workstations: NetKingdom identities or scoped tokens routed through
ops-warden; nothing pasted into repositories
- State Hub, repo-manager and the ORGREF collector: authenticated read identity
- fluid-telegram and pr-hall-of-helix: images moved to the T01 public asset
location, with the `compose visual` raw-URL bases updated
- State Hub, repo-manager and the ORGREF collector: authenticated read identity,
with private repository/package enumeration proven before T08
- fluid-telegram and pr-hall-of-helix: private image fetch and Telegram upload
through FT-WP-0001-T14; prove posts without anonymous raw-image reads
Each owner records its proof in its own workplan. This plan tracks the list.
### T07 — Rehearse the flip and the rollback
```task
id: ORGREF-WP-0002-T07
status: wait
@ -295,14 +312,14 @@ priority: high
state_hub_task_id: "0dcf25e2-cda1-5f87-b7fe-5b7e27635565"
```
### T07 — Rehearse the flip and the rollback
On throwaway repositories and packages: switch public → private → public, and
verify consumer reads, anonymous denial, web links, Actions runs and image pulls
at each step. Record the exact commands or API calls, the time each step takes,
and what a rollback restores (visibility) and what it does not (anything already
fetched). This rehearsal is the gate for T08.
### T08 — Close the forge in waves
```task
id: ORGREF-WP-0002-T08
status: wait
@ -313,8 +330,6 @@ priority: high
state_hub_task_id: "42724077-9837-5aef-ad3b-6551f2103aa2"
```
### T08 — Close the forge in waves
Change production visibility in founder-approved waves. Each wave lists its
repositories and packages and the consumers that read them. Proposed order:
@ -327,6 +342,8 @@ After each wave: consumer health checks, anonymous probe, and a recorded
rollback point. A failing consumer rolls its repositories back to public and
stops the wave. The fix lands with the consumer's owner.
### T09 — Verify, hand back and record
```task
id: ORGREF-WP-0002-T09
status: wait
@ -336,23 +353,27 @@ priority: medium
state_hub_task_id: "82f53328-02f5-5953-830a-00019d7dee92"
```
### T09 — Verify, hand back and record
- Anonymous probe of the server: only the T01 allowlist is visible. Record the
evidence under `inventory/`.
- Move ORGREF-WP-0001's collector to the authenticated identity from T06 and
note the effect on decision `5f3680fc`.
- Anonymous probe of the server: no repositories or packages are visible.
Record the evidence under `inventory/`.
- Verify ORGREF-WP-0001's collector still enumerates private metadata with the
identity proven in T06; reconcile completeness and note the effect on decision
`5f3680fc`. Do not postpone collector migration until after closure.
- Add a row to the-custodian `docs/security-close-down-checklist.md`
recording that the forge is closed by default, plus anything relaxed along the
way that must be re-armed.
- Record any consumer still on a temporary credential as live owner work, not as
prose in this file.
## Open questions for review
## Review — 2026-10-09
- Owner: is the founder the right owner, or should an agent own the driver with
the founder holding T01/T08?
- Should T03 (history review) block T08, or run in parallel and only gate
rotations?
- Is an interim lockdown (T01 point 5) worth the temporary-token churn, given how
long T04–T07 will take?
T01–T03 are done; T04–T09 remain waiting on owner implementation and proof.
The October 8 policy settles driver ownership, package treatment, interim
lockdown and parallel exposure review. No new production wave is authorized by
this review. The interim route must satisfy its own recorded guardrails; the
normal closure route retains T06/T07 and founder wave approval.
Collector schema 0.2 fixes package-index classification, separates legacy hosts,
records scan exclusions/errors and treats missing pull secrets as unknown access.
October 8 founder checks remain historical proof for that runtime snapshot;
new runtime records require current owner confirmation. This review does not
reopen T02 or replace its accepted evidence.