diff --git a/.loom/externals/.locks b/.loom/externals/.locks index 8d1faf9..d0474e6 100644 --- a/.loom/externals/.locks +++ b/.loom/externals/.locks @@ -1,6 +1,7 @@ # loomctl locks — one record per adopted document. # pathurletag The url is resolved: a short form would follow # whatever the default branch is at the time you ask. +git.hypertheory-labs.dev/jeffry/homelab-cluster/gitea.md https://git.hypertheory-labs.dev/jeffry/homelab-cluster/raw/branch/main/.loom/published/gitea.md "530c5bef62bbd325956ed170bb2decf37975e4b9" git.hypertheory-labs.dev/loom/annotating/annotating.md https://git.hypertheory-labs.dev/loom/annotating/raw/branch/main/.loom/published/annotating.md "9b1f7e6ca92f2339b2d433686c27845362944bdb" git.hypertheory-labs.dev/loom/bedrock/loom-directory.md https://git.hypertheory-labs.dev/loom/bedrock/raw/branch/main/.loom/published/loom-directory.md "ee0f49cb900c0812678061971194325d9cba366a" git.hypertheory-labs.dev/loom/bedrock/publication.md https://git.hypertheory-labs.dev/loom/bedrock/raw/branch/main/.loom/published/publication.md "eb0cb63629a36b056f215dfbe24567d1918cec38" @@ -8,4 +9,4 @@ git.hypertheory-labs.dev/loom/bedrock/recording-decisions.md https://git.hyperth git.hypertheory-labs.dev/loom/bedrock/sibling-facets.md https://git.hypertheory-labs.dev/loom/bedrock/raw/branch/main/.loom/published/sibling-facets.md "a46446a34ccb8bfc533d3cce19f4c88548c4fa04" git.hypertheory-labs.dev/loom/bedrock/starting.md https://git.hypertheory-labs.dev/loom/bedrock/raw/branch/main/.loom/published/starting.md "7d997a30248a88c90b23f2f9453d7cfceb03848e" git.hypertheory-labs.dev/loom/cart/cart.md https://git.hypertheory-labs.dev/loom/cart/raw/branch/main/.loom/published/cart.md "49fc852bdd280293f0f5e0050034e3b89ff7255e" -git.hypertheory-labs.dev/loom/externals/externals.md https://git.hypertheory-labs.dev/loom/externals/raw/branch/main/.loom/published/externals.md "9641f0e77b6f5c0161fa7593805277ba0c1e6176" +git.hypertheory-labs.dev/loom/externals/externals.md https://git.hypertheory-labs.dev/loom/externals/raw/branch/main/.loom/published/externals.md "a7586eb52caf275d9bcedbbd8042c43e5aaad0b9" diff --git a/.loom/externals/git.hypertheory-labs.dev/jeffry/homelab-cluster/gitea.md b/.loom/externals/git.hypertheory-labs.dev/jeffry/homelab-cluster/gitea.md new file mode 100644 index 0000000..530c5be --- /dev/null +++ b/.loom/externals/git.hypertheory-labs.dev/jeffry/homelab-cluster/gitea.md @@ -0,0 +1,69 @@ +# The git host + +**`git.hypertheory-labs.dev`**, on the public internet, with a real certificate. +**This is where the `loom/*` repositories live.** + +--- + +## SSH is on 2222, and it is not optional to know + +**Git-over-SSH does not pass through Traefik** — it is raw TCP, on a +`LoadBalancer` that binds a host port on every node. **Port 22 is held by each +node's own `sshd`**, so the service is on **2222**. + +``` +ssh://git@git.hypertheory-labs.dev:2222//.git +``` + +**A clone URL without the port will not work**, and the failure looks like an +authentication problem rather than a wrong port. + +```sh +ssh -T -p 2222 git@git.hypertheory-labs.dev # "Hi there, !" once a key is registered +``` + +*`Permission denied (publickey)` from a node IP is the **success** case for an +unregistered key — the server answered and offered its host key.* + +## Registration is closed + +**One account.** *The anonymous landing page serves no sign-up link.* **If you +need access, somebody creates it for you.** + +## Two things that will surprise you + +**Sessions do not survive a restart.** *There is no Redis or valkey here — cache +and session are in memory, deliberately.* **The queue is on disk and does +survive.** *With one user this is nearly free; it stops being free if this ever +grows real users.* + +**If the control-plane node is down, this is down.** *The repository volume is +pinned to it and cannot move.* **Postgres is unaffected** — it replicates — *but +the git objects live on a volume that cannot be rescheduled.* See +[storage](storage.md). + +## Never pin the chart below what is deployed + +**Gitea does not migrate its schema backward.** *An older chart fails in the +`configure-gitea` init container with "database is for a newer Gitea", the +rollout hangs, and the old pod keeps serving.* + +**Check `helm history` before setting a version.** *This has already happened +once.* + +## The container registry + +**Gitea has one. Access to it is not worked out**, and that is an open problem +rather than an omission — see [`gaps/`](../gaps/publishing-container-images.md). + +--- + +## Checking this is still true + +**Verified 2026-09-03**, after a rebuild from scratch. + +```sh +kubectl get svc gitea-ssh -n gitea # EXTERNAL-IP = node IPs, 2222/TCP +kubectl get ingress -n gitea # CLASS=traefik, git.hypertheory-labs.dev +curl -sS -o /dev/null -w "%{http_code}\n" https://git.hypertheory-labs.dev/ +``` diff --git a/.loom/externals/git.hypertheory-labs.dev/jeffry/homelab-cluster/gitea.usages.md b/.loom/externals/git.hypertheory-labs.dev/jeffry/homelab-cluster/gitea.usages.md new file mode 100644 index 0000000..11ee116 --- /dev/null +++ b/.loom/externals/git.hypertheory-labs.dev/jeffry/homelab-cluster/gitea.usages.md @@ -0,0 +1,25 @@ +# Usages — `gitea.md` + +**Adopted 2026-09-07, and it is the reason this tool exists.** + +*This page was published, accurate, and unreachable when it would have helped. +The `:2222` fact cost three tool calls and a guess, and the page says:* **"A clone +URL without the port will not work, and the failure looks like an authentication +problem rather than a wrong port."** *It named the failure before it happened.* + +## What we use + +**SSH is on `2222`, and a clone URL without the port fails as an auth error.** +*Used by anybody working in this repository by hand.* **Not used by `loomctl`** — +*every transport it has is HTTPS on 443* — **which is worth saying, because it is +the difference between a fact for people and a fact for the tool.** + +**Registration is closed; one account.** *Which is why +`internal/external/polad.go` and `internal/external/external.go` are built around +a single reader identity per host, and why nothing here tries to check a +publication as somebody else.* + +## What we expected and did not find + +**Nothing.** *The gap this document would have filled was ours, not its: it was +private, and now it is not.* diff --git a/.loom/externals/git.hypertheory-labs.dev/loom/cart/cart.usages.md b/.loom/externals/git.hypertheory-labs.dev/loom/cart/cart.usages.md index b0a1a36..82ad596 100644 --- a/.loom/externals/git.hypertheory-labs.dev/loom/cart/cart.usages.md +++ b/.loom/externals/git.hypertheory-labs.dev/loom/cart/cart.usages.md @@ -33,3 +33,34 @@ revert to.* *Which is the argument for stating a belief rather than a preference: **ours was shown wrong in a way we could see**, and a preference could not have been.* + +--- + +## Cast against `v1`: the cart is not committed + +**`v1` adds no role, so nothing above changed.** *It adds something an adopter +gets wrong by default, and this file said nothing about it until now.* + +**`.loom/cart/` is in `.gitignore`.** *Cast in this repository on 2026-09-07, +after `v1` landed mid-round.* + +> **We got this wrong before it was written down.** *Two rounds — `osprey` and +> `marmalade` — were committed, and they are still in this repository's history.* +> **Not rewritten:** *honouring a rule adopted afterwards by rewriting history +> would cost more than it buys*, **and the cost is exactly the one `v1` names** — +> *the negotiation stays quotable forever.* + +**What depends on this casting:** *`.gitignore`, and +`internal/external/polad.go`* — **which stages a candidate into +`.loom/cart/current/polad/` and therefore writes only into the untracked tree.** +*If the cart were ever committed again, `check` would start proposing changes +inside version control, which is the opposite of what a polad is for.* + +## An open conflict this repository cannot settle + +**The annotation protocol this repository works under says *commit before +dissolving; git is the only archive of the conversation*.** *An ignored cart has +no archive*, **so dissolving a notes file destroys the annotations outright.** + +*Both documents are loom's. **Recorded here because the conflict is visible from +inside an adopter and not from inside either document.*** diff --git a/.loom/externals/git.hypertheory-labs.dev/loom/externals/externals.md b/.loom/externals/git.hypertheory-labs.dev/loom/externals/externals.md index 9641f0e..a7586eb 100644 --- a/.loom/externals/git.hypertheory-labs.dev/loom/externals/externals.md +++ b/.loom/externals/git.hypertheory-labs.dev/loom/externals/externals.md @@ -24,6 +24,28 @@ in the document.* you fetch may refer to others; follow one when you hit something you do not know. **Pre-resolving that is how you get a `node_modules`.*** +### Confidentiality does not travel with the copy + +**Adopting is copying.** *So a document from a repository somebody may not read +ends up in a repository they may* — **and the publisher loses control of it at the +moment of adoption**, because the copy's visibility is governed by your repository +and not by theirs. + +> **Do not adopt from a source less readable than the repository you are adopting +> into.** *If you may read it and your readers may not, copying it publishes it.* + +**Two ways out, and the second is better when it is available.** + +**Reference-only** — *record the lock and fetch on demand, keep no copy.* **You +give up reading it offline**, which is most of what a copy is for, *and you keep +the dependency recorded and checkable.* + +**Ask them to publish** — *the thing you needed was almost certainly not the +confidential part.* **A repository that must stay private can still have a public +sibling that publishes**, and the split is usually along a line that already +exists: **the operational tree is what is sensitive; the pages telling somebody +what to decide are not.** + ## Two facets beside it - **`.usages.md`** — *what we use, and **which of our artifacts depend on it*** diff --git a/.loom/externals/git.hypertheory-labs.dev/loom/externals/externals.usages.md b/.loom/externals/git.hypertheory-labs.dev/loom/externals/externals.usages.md new file mode 100644 index 0000000..5d2d806 --- /dev/null +++ b/.loom/externals/git.hypertheory-labs.dev/loom/externals/externals.usages.md @@ -0,0 +1,52 @@ +# Usages — `externals` + +**This repository implements this document.** *That is an unusual usage: most +adopters use a convention, and `loomctl` is the convention's mechanism, so nearly +every rule here names a line of code.* + +## What we use, and what implements it + +**Freshness is a conditional request, locked on the publisher's `ETag`, +verbatim** — *`internal/lock/lock.go`* (**the record: path, resolved URL, `ETag`**) +*and* `internal/external/external.go` (*`fetch`, which sends `If-None-Match`*). +**We compute no hash anywhere**, *which this document requires and which was +measured to matter: on gitea the `ETag` is the git blob hash and on GitHub it is +not.* + +**The status table — `304`, `200`, `410`, `404`** — *`checkLocked` in +`internal/external/external.go`.* **`404` is reported unresolved, naming both +readings**, *which is this document's rule and was a gap we filed against it +before it was.* + +**"The new copy is a candidate, not a replacement"** — *`internal/external/polad.go`.* +**A changed document is staged in the cart as a polad and nothing is overwritten**; +*applying is a separate act.* + +**"Reconciliation runs the other way"** — *`usagesNote` and `usagesFor` in +`internal/external/polad.go`*, **which print this kind of file beside a staged +change**, *because it names the code to go and fix.* **It says so when there is +none.* + +**The published surface is what `list` reads** — *`internal/external/list.go`,* +`.loom/published` **only.** *What is not exported is not hidden; it is simply not +what you depend on.* + +**The path is for a person** — *`localPath` in `internal/external/external.go`.* +*This document retracted "the path says where it came from"; **we record the +resolved origin in the lock instead**, and the path is `///.md` +with `--path` for when that guess is wrong.* + +## What we expected to find and did not + +**Nothing outstanding.** *The `404` gap we filed here was closed on 2026-09-07 and +the facet was deleted at reconciliation.* + +## Not yet implemented + +**"Confidentiality does not travel with the copy"**, *added 2026-09-07.* **Nothing +in `loomctl` detects it.** *The tool knows the half it can see — whether a fetch +needed a credential — and does not yet say so.* **Reference-only adoption, which +this document offers as the first way out, does not exist either.** + +*Recorded here rather than as a gap, because the gap is ours: **the document says +what to do and the tool does not do it yet.*** diff --git a/internal/external/external.go b/internal/external/external.go index a2ac422..62429bf 100644 --- a/internal/external/external.go +++ b/internal/external/external.go @@ -168,6 +168,7 @@ func Add(root, raw, override string, out io.Writer) error { fmt.Fprintf(out, "adopted %s\n", rel) fmt.Fprintf(out, " from %s\n", body.url) + warnIfNotPublic(cfg, body.url, out) if etag == "" { fmt.Fprintf(out, " etag (none served — check cannot ask conditionally)\n") } else { @@ -176,6 +177,37 @@ func Add(root, raw, override string, out io.Writer) error { return nil } +// warnIfNotPublic says so when a document could only be fetched with a +// credential. +// +// Adopting is copying, so a document from a repository somebody may not read +// ends up in a repository they may, and the publisher loses control of it at the +// moment of adoption. The tool can see half of that — whether this fetch needed +// a credential — and cannot see the other half, which is who can read the +// repository the copy is landing in. It reports the half it knows. +func warnIfNotPublic(cfg *config.Config, raw string, out io.Writer) { + req, err := http.NewRequest(http.MethodHead, raw, nil) + if err != nil { + return + } + if cfg.TokenFor(req.URL.Host) == "" { + return // no credential was used, so the fetch was already anonymous + } + resp, err := client.Do(req) + if err != nil { + return + } + defer resp.Body.Close() + if resp.StatusCode == http.StatusOK { + return + } + fmt.Fprintf(out, " WARN this needed a credential — anonymously it is %s.\n", resp.Status) + fmt.Fprintf(out, " Confidentiality does not travel with the copy: writing it into\n") + fmt.Fprintf(out, " .loom/externals/ republishes it to anyone who can read THIS\n") + fmt.Fprintf(out, " repository. Do not adopt from a source less readable than the\n") + fmt.Fprintf(out, " repository you are adopting into.\n") +} + type fetched struct { data []byte url string diff --git a/main.go b/main.go index 0b651ab..9b361ed 100644 --- a/main.go +++ b/main.go @@ -27,6 +27,10 @@ become — and somebody decides. Its exits are apply or discard; nothing there drifts into being kept. With no cart open, check says what moved and stages nothing, because opening a round is somebody's act and not a side effect. +add warns when a document could only be fetched with a credential: adopting is +copying, and confidentiality does not travel with the copy. It cannot see who +may read the repository the copy lands in, so it reports the half it knows. + add adopts what is not here yet, and refuses what is already adopted. Given a document that is present but unlocked — fetched by hand before this existed — it supplies the missing origin: identical bytes lock it, differing bytes are