diff --git a/.loom/cart/current/claude-marmalade.md b/.loom/cart/current/claude-marmalade.md index d616486..0883f65 100644 --- a/.loom/cart/current/claude-marmalade.md +++ b/.loom/cart/current/claude-marmalade.md @@ -1,296 +1,49 @@ # claude — cart `marmalade` **`osprey` converted, so this is a new round.** *Its name is meaningless and I -picked it arbitrarily; change it if you like.* +picked it arbitrarily.* + +**Rewritten to dissolve your annotations.** *The prompts below are answered rather +than asked, and what is still open says so.* --- -## Two mechanical things before the design +## How the round opened -**Your text arrived as `jeff-osprey.md` and I have moved it, unedited, to -`loom-marmalade.md`.** *`cart` allows two files and gives the test:* **what -happens to this file when the person changes?** *`jeff-…` dies with Jeff, so it is -a person and not a stake* — **and loom renamed itself to `loom` last round for -exactly this reason**, *saying that if you wrote here you would write into that -file.* **Not a correction of you; the shape predicted it and the shape was right.** +**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 it landed in a cart that had converted**, *which is fine — it opened the next -one.* **Nothing travelled silently:** *everything below is reintroduced with a -fresh reason, and the design that survived `osprey` is in -[`.loom/event-log.md`](../../event-log.md) rather than in a file I am carrying -forward.* +**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.* -## `loomctl`, and the noun that is missing +> **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.* -**Subcommands, yes.** *And your grouping exposes something the log got wrong.* - -**`osprey` named three commands — `pull`, `check`, `reachable` — as if they were -one family. They are two.** - -``` -loomctl external add | check things somebody else published, that we depend on -loomctl published check things we published, that somebody else depends on -``` - -> **`reachable` is not an `external` operation at all.** *It looks at -> `.loom/published/`, which is our surface and not anybody's dependency.* **Under -> your grouping it has no home, and that is the grouping doing its job.** - -*`publication.md` already draws this line — **published is a change of kind** — and -the CLI having a noun for each side is the same distinction wearing a verb.* - -**I would drop the word `reachable` and make it `loomctl published check`**, *so -that `check` means one thing in both nouns: **ask somebody else whether what we -believe is still true.*** *Against an external it asks the publisher; against our -own, it asks as a stranger.* - -## `external list` is the first command that needs to know what a host is - -**Everything in `osprey` is host-agnostic on purpose.** *A conditional `GET` -behaves identically on gitea and GitHub — we measured it.* **`list` is not like -that.** *Enumerating `.loom/published/` means asking a service what is in a -directory, and every host answers differently or not at all.* - -**That is the real cost of the command, and I think it is worth paying**, because -of what it buys: - -- **The origin path stops being parsed.** *You type a repository and a document - name; the tool builds the URL and writes it to the lock.* **The problem that ate - two passes of `osprey` disappears by never arising.** -- **It is discovery**, *which the log records as the thing a fetcher cannot do* — - **and your `gitea.md` story is exactly the failure it fixes.** *I could not have - pulled that document, because I did not know it existed.* - -> **You said there is no registry yet, so you are the registry.** *`external list` -> is the smallest thing that stops that being true.* - -## Select: what does `external list` talk to? - -- **each host's REST API** — *gitea `/api/v1/repos/{o}/{r}/contents/.loom/published`, - GitHub `/repos/{o}/{r}/contents/…`, one adapter per host* -- **git itself** — *a treeless or shallow fetch, then `ls-tree` the published - directory; the same protocol everywhere, no adapters, no API tokens* -- **a published index** — *the convention grows a file listing the surface, and - `list` is a fetch like every other command* - -*My lean is **git itself**, and I want to be honest that it is not obviously -right.* **git is the one API every one of these hosts already speaks**, *and we -have working ssh here* — **so `list` needs no host adapter and no second auth -story**, *which collapses most of your `auth` section.* **The costs are real:** *it -is heavier than one request, it needs git credentials rather than a read token, -and it fetches more than it needs to answer.* - -**Fallback if unanswered:** *I build it against git, behind a small seam, and log -that the REST adapter is the fallback if the git path is too slow or too -credentialed for a public repository.* - -## Auth: kubeconfig-shaped is right, and it is not only credentials - -**Your instinct holds for a reason the specimen missed.** *`osprey` assumed one -token because it assumed one host.* **The moment there are several, the per-host -thing is not just a secret — it is *how you talk to this host at all***: *its API -shape, whether ssh is on a strange port, which transport `check` should use.* - -**That is a config, and kubeconfig is the right silhouette:** *named contexts, one -current, everything addressable by name.* - -**Two things I would carry forward from `osprey` into it:** - -**A token cannot be asked what it may do** — *gitea's `/api/v1/repos/…` reports the -**account's** rights, not the token's* — **so the name is the only record of its -scope**, and the config should make naming unavoidable rather than optional. - -**And it lives in `~`, never in a repository.** *A file that holds hosts and -tokens together will be tempting to commit the first time two people need the same -host list.* **Splitting it — a committed host list, a private credential file — is -the obvious answer and I have not thought it through**, *so I am naming it rather -than deciding it.* - -## Affirm: subcommands now, plugins not yet? - -**I would not build a plugin boundary before `beads` exists.** - -*`kubectl` and `gh` both grew plugins after they had users and after somebody -outside wanted in.* **Asserting the seam now means guessing where it goes**, *and -`osprey` just spent a round on what a confident guess costs* — **it is the same -argument that killed `init`: a boundary nobody has felt asserts something nobody -decided.** - -**Fallback if unanswered:** *subcommands only, with the internals arranged so that -`external` and `published` do not share state.* **If that stays true, the plugin -seam is discovered rather than designed.** - -## Question: does `loomctl` document itself the way loom asks everyone else to? - -**Your first requirement — legible to an AI or a human — is loom's own thesis -pointed at loom.** *So the test is available: **this repository has a -`.loom/published/`, and `loomctl published check` can be run against it.*** - -*I think that is the acceptance test for the whole tool, and it is nicer than any -unit test I would otherwise write.* **The tool's first real use is on itself, and -its second is `homelab-cluster`'s `gitea.md`** — *the document I needed and could -not read.* - -**Fallback if unanswered:** *I write `.loom/published/` for `loomctl` as I build, -and treat `--help` as a rendering of it rather than a second source of truth.* - -## Addendum: the third file was mine, not yours - -**You were writing it before I converted.** *Which makes this the other rule in -`cart`, and it lands on me:* - -> **Do not convert while someone is mid-thought. Ask whether they are ready. If -> you convert alone anyway, say so.** - -**I did ask, and you said the plan was good, and you were mid-thought anyway.** -*So the check passed and was still wrong* — **approving a plan is not the same as -having nothing left to say**, and the question as `cart` phrases it invites the -first answer when it wants the second. - -*Worth reporting upward: **"are you ready for me to convert?" and "is there -anything you are still writing?" are different questions**, and only the second one -would have caught this.* - -**Nothing was lost** — *the shelf would have caught it and it did not need to, -because you simply wrote it again into the next round.* **Which is the mechanism -working.** +*So it is recorded as the mechanism working, not as something to fix.* --- -# Measured: git, and your instinct was right +## Settled this round -**Taken: `published check`.** *`reachable` is gone as a word.* +### `published check`, and why it cannot report a verdict -## `git archive --remote` is out +**`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.*** -``` -gitea works (ssh) -github HTTP 422 -gitlab HTTP 404 -``` +**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.* -*One round trip and no clone, and only one of three hosts allows it.* - -## Partial clone is uniform across all three - -**`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 -``` - -**Same flags, same command, same output shape.** *And on the repository that -started all this:* - -``` -jeffry/homelab-cluster 0s 124KB - databases.md gitea.md private-access.md - public-access.md recovering-data.md storage.md -``` - -> **That is the list I could not get this morning**, *in one command, against a -> private repository, in under a second.* - -## What REST would have cost - -``` -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 -``` - -**Three URL shapes, three JSON shapes, three auth schemes** — *and the private -gitea repository returns `404` anonymously, so REST fails on exactly the case that -matters.* **Your sense was right and it is now measured rather than felt.** - -## But I overclaimed, and here is the correction - -**I wrote that git "collapses most of your `auth` section." It does not.** - -*`list` discovers names over git. **`add` and `check` still 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.* - -**And raw HTTP against a private repository is `404` without a token.** *So the -honest account is:* - -| | 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, anonymous | *none, deliberately* | - -**Two credentials per host, not one.** *The argument for git is **consistency**, -not thrift* — **one mechanism against three hosts instead of three adapters** — -*and that argument stands on its own.* **The auth saving was me liking my answer.** - -*Untested: whether a gitea read token actually works against raw HTTP on a private -repository.* **It is the specimen's assumption and nobody has run it.** - -## The failure mode to build for - -**`--filter` is a server capability.** *All three hosts here allow it; an older -self-hosted gitea or gitlab may not, and **git's fallback is silent** — it warns -`filtering not recognized by server` and downloads everything.* - -**`--depth=1` bounds the damage to one commit**, *which is why both flags are -there and not just the filter.* **Detect the warning and say so**, rather than -letting `list` quietly become a full clone of somebody's monorepo. - ---- - -# The token, and `published check` was wrong - -**Done with the token — delete it whenever you like.** *It is in no file, no -commit and no config; it lived in a shell variable in a scratch directory.* - -## Three things it settled - -**The specimen's assumption holds.** *A read token against **raw** HTTP on a -private repository:* `404` *anonymous,* **`200` with `Authorization: token`** *(and -with `Bearer` — both forms work).* **Conditional requests work authenticated: -`If-None-Match` returned `304`.** *So `add` and `check` work on private -repositories, which is the whole of `homelab-cluster`.* - -**The gitea coincidence holds on private repositories too** — *`ETag` -`530c5bef…` equals the blob hash.* **Which changes nothing**, *because we do not -use it.* - -**And the specimen's sharpest claim is now measured rather than asserted:** - -``` -GET /api/v1/repos/jeffry/homelab-cluster - permissions: {admin: true, push: true, pull: true} ← with a READ token -``` - -> **A token cannot be asked what it may do.** *The API reports the **account's** -> rights.* **Scope is set once at creation and invisible afterwards, so the name -> is the only record** — *and that is now a thing we have seen rather than a thing -> we were told.* - -## You are right, and it breaks my design rather than dents it - -**I built `published check` on "can somebody anonymous fetch this", and -publication is not the same as being public.** *`homelab-cluster` publishes six -documents and is private on purpose, and that is not a defect — **it is what -`publication.md` actually says:*** - -> **What a repository has handed over for **others** to depend on.** - -***Others*, not *everyone*.** *I read one word as the other and built a test on -it.* - -## What the check has to be instead - -**The question is not "is this public". It is "can the audience I published for -fetch it".** *And the audience is not a thing the tool can know.* - -**So `published check` must never report success. It reports the identity it -used:** +**So the command reports the identity it used, never a verdict:** ``` gitea.md 200 as anonymous @@ -298,62 +51,178 @@ gitea.md 200 as reader gitea.md 200 as jeffry — OWNER, this proves nothing ``` -**The third line is the one that earns the command.** *`/api/v1/user` gives the -login; the repository gives its owner; if they match, **the check is vacuous and -should say so instead of printing something green.*** +> **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.** -> **That is `publication.md`'s own sentence, enforced:** *publishing is not an act -> you can complete alone.* **A tool that verifies your publication using 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 ` is +the whole feature.** -**And it ties your `auth` section to something real:** *a kubeconfig-shaped config -is not only credentials, it is **named identities you can test as**.* **`--as -` is the whole feature.** +### No audience declaration -## Affirm: 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`.** -**I do not want to add a field where a repository states who it published for.** +**`--as` defaults to anonymous and the owner warning is always on.** -*It would let the tool compare intent to result, which sounds good* — **and it -asserts a category nobody has decided**, *which is the argument that killed -`init`.* **Report the identity, let the person judge the audience.** +*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.* -**Fallback if unanswered:** *no declaration; `--as` defaults to anonymous, and the -owner-identity warning is always on.* +### `external list` talks to git -## You were right to make me try it, and it corrects me - -**I wrote, one message ago, that the specimen's claim was "now measured rather -than asserted." I had measured the wrong half.** +**Measured across three hosts.** *`git archive --remote` is out — gitea allows it, +GitHub `422`s, GitLab `404`s.* **Partial clone is uniform:** ``` -POST /api/v1/repos/loom/loom-cli/contents/… 403 - token does not have at least one of required scope(s), - required=[write:repository], - token scope=read:activitypub,read:admin,read:misc,read:notification, - read:organization,read:package,read:issue,read:repository,read:user +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 ``` -**The refusal names the scope.** *So "a token cannot be asked what it may do" is -too strong* — **and I confirmed it enthusiastically without testing the one case -that would have shown it wrong**, which is the failure mode this project keeps -finding in itself. +**REST would have been three dialects, not one with variations:** -**What is actually true, and it is sharper:** +``` +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.** -*Verified: `/api/v1/users/{u}/tokens` rejects token auth entirely (`401 auth -required` — it wants a password), no scope header comes back on success, and -`/api/v1/repos/…` reports `admin, push, pull: true` for a token that cannot -write.* **The only channel is the error body.** +**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.* -**For us that changes the reason and not the rule.** *`loomctl` never writes, so it -will never provoke the one response that would tell it anything.* **The name -remains the only record we will ever see** — *but because we never ask, not -because nobody answers.* +--- -**One concrete consequence:** *gitea's error bodies carry the useful part.* -**Surface them rather than mapping a `403` to a sentence of our own** — *we would -be throwing away the only scope information the host ever 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.* diff --git a/.loom/cart/current/claude-marmalade.notes.md b/.loom/cart/current/claude-marmalade.notes.md deleted file mode 100644 index 73f5251..0000000 --- a/.loom/cart/current/claude-marmalade.notes.md +++ /dev/null @@ -1,38 +0,0 @@ -# Notes on claude-marmalade.md - - - -## L109 - -> ## Affirm: subcommands now, plugins not yet? - -Yep - subcommands now. We'll wait for subcommands until the gap is reported. ;) - -## L123 - -> ## Question: does `loomctl` document itself the way loom asks everyone else to? - -I'm not sure - if I handed you an unfamiliar CLI and said "check this out", you'd probably do --help or something. But absolutely, at some point, we should consider that this repo should have a .loom/published with some docs or something? - -## L150-152 - -> *Worth reporting upward: **"are you ready for me to convert?" and "is there -> anything you are still writing?" are different questions**, and only the second one -> would have caught this.* - -BTW, this is exactly how this is supposed to work - there is a built in buffer that we would rather mess up sometimes *in the cart* than have overly restrictive rules, etc. I messed up by not waiting until the cart was closed, etc. no biggy. - -## L248 - -> **Done with the token — delete it whenever you like.** *It is in no file, no - -it is deleted - we'll decide where it should live soon, right? - -## L313 - -> ## Affirm: no audience declaration? - -This is correct - no audience. In the future we could project from all the repositories externals who they are using, etc. but nothing now.