quince: reconcile through the tool, write the facets, and adopt gitea.md
The whole loop ran on a real change with a cart open for the first time: check staged externals.md as a polad, apply moved it and its lock, and the new confidentiality rule arrived through the tool rather than through somebody saying so. Answers loom's completeness case. A 200 says the document moved and nothing about whether the casting still covers it — but the mechanism exists and we had not built the thing it needs. check prints the document's .usages.md when it stages a polad, and for an agreement that is the file the roles are cast in. It failed here because externals.md had no facet at all. So both are written: externals.usages.md naming which Go file implements which rule, and a v1 section on cart.usages.md recording the casting, that we got it wrong before it was written down, and that osprey and marmalade stay in history unrewritten. Implements the adopted confidentiality rule as far as it can be implemented. add warns when a fetch needed a credential, and says plainly that it cannot see who may read the repository the copy lands in. Reference-only adoption is recorded as not yet implemented rather than as a gap, because the gap is ours. Adopts gitea.md, now that homelab-cluster is public — the document that cost three tool calls and a guess this morning, with a facet recording that its :2222 fact is a fact for people and not for the tool, whose every transport is HTTPS on 443. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018UTxuSizozEA8yDitPuris
This commit is contained in:
Vendored
+2
-1
@@ -1,6 +1,7 @@
|
|||||||
# loomctl locks — one record per adopted document.
|
# loomctl locks — one record per adopted document.
|
||||||
# path<TAB>url<TAB>etag The url is resolved: a short form would follow
|
# path<TAB>url<TAB>etag The url is resolved: a short form would follow
|
||||||
# whatever the default branch is at the time you ask.
|
# 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/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/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"
|
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/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/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/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"
|
||||||
|
|||||||
@@ -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/<org>/<repo>.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, <name>!" 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/
|
||||||
|
```
|
||||||
+25
@@ -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.*
|
||||||
@@ -33,3 +33,34 @@ revert to.*
|
|||||||
|
|
||||||
*Which is the argument for stating a belief rather than a preference: **ours was
|
*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.*
|
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.***
|
||||||
|
|||||||
@@ -24,6 +24,28 @@ in the document.*
|
|||||||
you fetch may refer to others; follow one when you hit something you do not know.
|
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`.***
|
**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
|
## Two facets beside it
|
||||||
|
|
||||||
- **`.usages.md`** — *what we use, and **which of our artifacts depend on it***
|
- **`.usages.md`** — *what we use, and **which of our artifacts depend on it***
|
||||||
|
|||||||
@@ -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 `<host>/<owner>/<repo>/<name>.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.***
|
||||||
Vendored
+32
@@ -168,6 +168,7 @@ func Add(root, raw, override string, out io.Writer) error {
|
|||||||
|
|
||||||
fmt.Fprintf(out, "adopted %s\n", rel)
|
fmt.Fprintf(out, "adopted %s\n", rel)
|
||||||
fmt.Fprintf(out, " from %s\n", body.url)
|
fmt.Fprintf(out, " from %s\n", body.url)
|
||||||
|
warnIfNotPublic(cfg, body.url, out)
|
||||||
if etag == "" {
|
if etag == "" {
|
||||||
fmt.Fprintf(out, " etag (none served — check cannot ask conditionally)\n")
|
fmt.Fprintf(out, " etag (none served — check cannot ask conditionally)\n")
|
||||||
} else {
|
} else {
|
||||||
@@ -176,6 +177,37 @@ func Add(root, raw, override string, out io.Writer) error {
|
|||||||
return nil
|
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 {
|
type fetched struct {
|
||||||
data []byte
|
data []byte
|
||||||
url string
|
url string
|
||||||
|
|||||||
@@ -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
|
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.
|
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
|
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 —
|
document that is present but unlocked — fetched by hand before this existed —
|
||||||
it supplies the missing origin: identical bytes lock it, differing bytes are
|
it supplies the missing origin: identical bytes lock it, differing bytes are
|
||||||
|
|||||||
Reference in New Issue
Block a user