Files
loom-cli/.loom/cart/current/claude-marmalade.md
T
jeffryandClaude Opus 5 a419222477 marmalade: decline published check, choose Go, and open a wal
Adds .loom/cart/current/wal.md as staging for the event log, so conversion moves
entries rather than rewriting them.

published check is declined on the registry argument rather than on not needing it
yet: the command simulates a consumer and a registry with named consumers has real
ones, so an observed fetch beats a synthetic probe and building the probe first
builds a worse version of what is coming. Two falsifiers recorded — registry never
materialising, or registry recording declared consumers rather than observed
fetches, since who says they depend on us is not who can actually read us.
Dropping it also removes the only thing complicating the credential story, because
--as existed almost entirely to serve it.

The insight is kept separately because it outlives the command: a check performed
with your own credential proves nothing, and whoever builds registry's consumer
view needs that before they design it, since the tempting implementation is for
the publisher to verify their own publication.

Go, for a single static binary with no runtime, on a tool that gets installed on
other people's machines and run casually by humans and in loops by agents.
Recorded with its own falsifier: if it only ever runs in one or two places, the
choice was made on a cost nobody pays. And the consequence — list shells out to
git, so git must be on PATH, which makes list the seam if that ever bites.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018UTxuSizozEA8yDitPuris
2026-09-07 14:40:20 -04:00

329 lines
14 KiB
Markdown

# claude — cart `marmalade`
**`osprey` converted, so this is a new round.** *Its name is meaningless and I
picked it arbitrarily.*
**Rewritten to dissolve your annotations.** *The prompts below are answered rather
than asked, and what is still open says so.*
---
## How the round opened
**Your notes arrived as `jeff-osprey.md`, into a cart that had converted.** *Moved
unedited to `loom-marmalade.md`, because `cart` allows two files and gives the
test — **what happens to this file when the person changes?*** *`jeff-…` dies with
you, so it is a person and not a stake, and loom renamed itself to `loom` last
round saying that if you wrote here you would write into that file.*
**And the timing was mine, not yours.** *`cart` says do not convert while somebody
is mid-thought; I asked, you approved a plan, and approving a plan is not the same
as having nothing left to write.*
> **You called that the design rather than the defect, and you are right.** *There
> is a deliberate buffer — **better to make a mess occasionally, inside the cart,
> than to carry rules strict enough to prevent it.*** **The shelf exists for this
> and did not even need to be used**, *because you simply wrote it again into the
> next round.*
*So it is recorded as the mechanism working, not as something to fix.*
---
## Settled this round
### `published check`, and why it cannot report a verdict
**`reachable` is gone as a word.** *The command is `loomctl published check`, so
that `check` means one thing on both sides: **ask somebody else whether what we
believe is still true.***
**And the test is not "is this public".** *I built it that way and you broke it:*
**`homelab-cluster` publishes six documents and is private on purpose.**
*`publication.md` says what a repository has handed over for **others** to depend
on* — **others, not everyone**, *and I read one word as the other.*
**So the command reports the identity it used, never a verdict:**
```
gitea.md 200 as anonymous
gitea.md 200 as reader
gitea.md 200 as jeffry — OWNER, this proves nothing
```
> **The third line is what earns the command.** *`publication.md` says publishing
> is not an act you can complete alone* — **and a tool that verifies your
> publication with your own credential has helped you complete it alone, and told
> you it was fine.**
*Which makes your `auth` section load-bearing: **a kubeconfig-shaped config is not
only credentials, it is named identities you can test as.*** **`--as <context>` is
the whole feature.**
### No audience declaration
**Confirmed: no field where a repository states who it published for.** *It would
let the tool compare intent to result, and it asserts a category nobody has
decided* — **the argument that killed `init`.**
**`--as` defaults to anonymous and the owner warning is always on.**
*Your future version is better than a declaration and does not need one:* **project
it from the other side — read everybody's `externals/` and derive who is actually
depending on you.** *Nothing now; it is `registry`'s shape, and `registry` is
deliberately not built.*
### `external list` talks to git
**Measured across three hosts.** *`git archive --remote` is out — gitea allows it,
GitHub `422`s, GitLab `404`s.* **Partial clone is uniform:**
```
git clone --filter=blob:none --depth=1 --no-checkout then git ls-tree HEAD:.loom/published
gitea filter ok .git=124KB
github filter ok .git=124KB
gitlab filter ok .git=144KB
jeffry/homelab-cluster 0s 124KB over ssh, private
databases.md gitea.md private-access.md
public-access.md recovering-data.md storage.md
```
**REST would have been three dialects, not one with variations:**
```
gitea /api/v1/repos/{o}/{r}/contents/{path}
github /repos/{o}/{r}/contents/{path} (api.github.com)
gitlab /api/v4/projects/{url-encoded}/repository/tree
```
**The argument for git is consistency, not thrift.** *One mechanism against three
hosts instead of three adapters* — **and REST `404`s on the private repository,
which is the case that matters.**
**The failure mode to build for:** *`--filter` is a server capability and git's
fallback is **silent***, warning `filtering not recognized by server` and
downloading everything. **`--depth=1` bounds the damage, which is why both flags
are there.** *Detect the warning and say so, rather than letting `list` quietly
become a full clone of somebody's monorepo.*
### Transports, honestly
| | transport | credential |
|---|---|---|
| **`external list`** | git | *ssh key or https creds you already have* |
| **`external add`, `external check`** | HTTP | **a read token, for private repos** |
| **`published check`** | HTTP | ***not* the owner's — that is the point** |
**Two credentials per host, not one.** *I claimed earlier that git collapsed most
of the `auth` story; it does not.* **`add` and `check` want HTTP because freshness
is a conditional request** — *a `304` is a few hundred bytes and a partial clone is
124KB, so checking eight externals over git would cost a megabyte to learn
nothing.*
### Subcommands now; plugins when somebody reports a gap
**`loomctl external …`, `loomctl published …`, and `loomctl beads …` when it
exists.**
**No plugin boundary yet**, *and your framing is better than my reasoning was:*
**the seam gets built when it is a reported gap, not a designed one.** *`kubectl`
and `gh` both grew plugins after somebody outside wanted in* — **and a gap is
exactly how loom says that arrives**, *with a name attached and a workaround
recorded beside it.*
### `--help` is the documentation surface; `.loom/published/` is later
**You are right about what actually happens:** *hand somebody an unfamiliar CLI and
they run `--help`.* **That is the discovery surface, so it is the one that has to
be good** — *for a person and for an agent, which mostly means the same thing:
say what a command does and what it will not do.*
**A `.loom/published/` for `loomctl` is deferred, not declined.** *The distinction
matters, because `init` was declined and this is not.* **When somebody depends on
this tool's behaviour rather than reading its help, that is when it has a contract
worth publishing** — *and `published check` gets to be run against its own
repository, which is a nicer acceptance test than anything I would write.*
---
## Measured, so nobody re-derives it
**A read token, issued for one session and since deleted.**
**It authenticates raw HTTP on a private repository.** *`404` anonymous, `200` with
`Authorization: token` **or** `Bearer`, and `If-None-Match` returns `304`
authenticated.* **So `add` and `check` work on private repositories** — *the
assumption the specimen made and nobody had run.*
**The gitea `ETag`/blob coincidence holds on private repositories too.** *Which
changes nothing, because we do not use it.*
**And the specimen's sharpest claim needed correcting, by you making me try the
write:**
```
POST …/contents/… 403
required=[write:repository]
token scope=read:activitypub,…,read:repository,read:user
```
*`/api/v1/repos/{o}/{r}` reports `admin, push, pull: true` for a token that cannot
write — those are the **account's** rights.* **But the refusal names the scope.**
> **A token cannot be asked what it may do. It can only be told, by being
> refused.**
**No other channel exists:** *`/api/v1/users/{u}/tokens` rejects token auth
outright, and no scope header comes back on success.* **For us this changes the
reason and not the rule** — *`loomctl` never writes, so it never provokes the one
response that would tell it anything.* **Surface gitea's error bodies rather than
replacing them**, *since they carry the only scope information the host emits.*
---
## Still open: where the credential lives
**The token is deleted and we said we would decide this soon. It is the one thing
this round has not answered.**
**What `osprey` fixed and still holds:** *it lives in `~`, never in a repository,
and it is named for its scope because the name is the only record.*
**What is new, and what makes it harder:** *the config is no longer only a secret.*
**It is how you talk to a host at all** — *API shape, ssh on a strange port, which
transport `check` should use, and now **named identities to test as***. *Some of
that is not secret and several people would want the same values.*
> **The obvious answer is to split it:** *a host list that can be committed, and a
> credential file that cannot.* **I have not thought it through and I am naming it
> rather than deciding it.**
**Fallback if unanswered:** *one file in `~`, kubeconfig-shaped, holding both — and
an entry recording that the split is expected and was not done yet.* **Nothing in
the repository, and nothing that reads from the repository**, *so that the mistake
we would most regret is the one that cannot happen by accident.*
---
## Corrections I made this round
**Kept together, because there are three and the pattern is the point.**
**I claimed git collapsed the auth story.** *It does not — I liked my answer.*
**I confirmed the token claim enthusiastically without testing the case that would
show it wrong.** *You made me try the write and the refusal named the scopes.*
**I built `published check` on anonymous fetchability.** *Publication has an
audience and the audience is not always everyone.*
> **All three are the same shape: a result that agreed with me, and no test of the
> case that would not have.** *It is also exactly what `osprey` found in the
> `ETag` story* — **a verified fact is much louder than a rule** — *and I have now
> done it three times in a day while writing that sentence down.*
*Not an apology. **A thing to point a test at**, and the reason the log carries a
belief rather than a conclusion.*
---
## Reviewed: what needs ssh, and what needs write
**Short answer: ssh, nothing. Write access, nowhere — including here.**
### Every operation, and what it actually needs
| | what it does | transport | credential |
|---|---|---|---|
| **`external list`** | *partial clone, `ls-tree` the published dir* | **git over HTTPS** | *read PAT* |
| **`external add`** | *`GET` the raw file, record URL + `ETag`* | **HTTPS** | *read PAT* |
| **`external check`** | *conditional `GET`, `If-None-Match`* | **HTTPS** | *read PAT* |
| **`published check`** | *`GET` our own published file* | **HTTPS** | ***not ours*** |
| **everything it writes** | *files in the working tree* | **none** | **none** |
**Measured just now: `git clone --filter=blob:none --depth=1 --no-checkout` over
`https://` on gitea** — *filter accepted, 124KB, `ls-tree` lists the published
directory.* **I had built and measured `list` over ssh out of habit, and it never
needed it.**
### The tool never writes over the network, so it needs no write credential
**Including in this repository.** *`loomctl` puts files in the working tree — a
fetched document, a lock, a facet — and stops.* **Committing and pushing are yours,
under your own credentials.**
> *That is the same reason `cart` gives for an agent committing and not pushing:*
> **the consequences of a push land on people it cannot experience.** *A tool that
> can push is a tool that can publish, and publishing is supposed to be an act
> somebody performs.*
**So `write:repository` should never appear on any token this tool is given**, *and
a token that has it is a token we would rather not hold.*
### The one thing a single PAT cannot do
**`published check` needs an identity that is not the owner's, and yours is
yours.** *That is not an ssh problem and it is not solvable by scoping — **it is
the command's entire point.***
**Two workable identities, both HTTPS:**
- **anonymous** — *the right answer when the audience is everyone*
- **a second credential belonging to the actual audience** — *a reader account, or
a token from somebody in the group you published for*
*And if you hold neither, the honest output is **"checked as owner, proves
nothing"**, which is still worth printing.*
### One thing untested, and it is the load-bearing one
**Whether a gitea PAT scoped `read:repository` authenticates *git over HTTPS*, not
just the API and raw files.** *They are different paths in gitea, and the deleted
token was only ever measured against the second.* **Measured: an unauthenticated
HTTPS clone of the private repository returns `Failed to authenticate user`**, *so
something is required; nobody has confirmed that a read PAT is enough.*
**First thing to run against the next token.** *If it turns out git-over-HTTPS
needs a broader scope than raw HTTP, that is an argument for keeping ssh for `list`
after all* — **and it would be the only argument.**
### An aside that is too neat not to record
**`:2222` cost me three tool calls and a guess, and the tool does not need it.**
*Every transport here is HTTPS on 443.* **The fact that bit me is a fact for
people cloning repositories, not for `loomctl`** — *which is its own small lesson
about who a published document is for.*
### Implementation note: keep the token out of the URL
**Do not build `https://<token>@host/…`.** *git writes the remote into
`.git/config`, and it turns up in logs and in `git remote -v` afterwards.*
**Pass it per-invocation instead:**
```
git -c http.extraHeader="Authorization: token <t>" clone --filter=blob:none …
```
*Nothing persisted, nothing in the repository.* **Which is the same rule as the
credential file: the mistake we would most regret is the one that happens by
accident.**
---
## `published check` is declined, and there is a `wal.md` now
**Staged in [`wal.md`](wal.md) rather than here**, *so conversion is a move and not
a rewrite.* **Four entries: the decline and its two falsifiers, the insight that
outlives the command, Go, and `git`-on-`PATH` as the consequence of `list`.**
**Your registry argument is the one that carries it**, *not "we do not need it
yet".* **A probe simulates a consumer; a registry with named consumers has real
ones** — *and an observed fetch beats a synthetic one.*
*Not a rationalisation, on one condition: **that the falsifiers are written down
where somebody will hit them**, which is what makes it a decline rather than a
thing we talked ourselves out of. **They are in the `wal`.***