marmalade: dissolve the annotations, and the file is released
All five annotations incorporated and claude-marmalade.notes.md deleted, which reopens the daily. Dissolve is all-or-nothing, so this is a rewrite rather than an appended pass — the five appended passes are collapsed into one document, which is most of the point of dissolving. Three prompts are now answers. Subcommands yes, and no plugin boundary until somebody reports a gap, which is a better formulation than my reasoning was — the seam gets built when it is reported, not designed. No audience declaration, with his better future version recorded: project it from the other side by reading everybody's externals to derive who actually depends on you, which is registry's shape and registry is deliberately not built. And --help is the documentation surface because that is what a person or an agent actually runs, with a .loom/published for loomctl deferred rather than declined. Reframes the conversion-timing note. I had it as a defect in how cart phrases its question; he says the tolerance is the design, and that a mess made occasionally inside a cart is cheaper than rules strict enough to prevent it. The shelf existed for it and was not needed. Leaves one thing open with a fallback: where the credential lives, now that the config is not only a secret but how you talk to a host at all. Collects the three corrections I made this round in one place, because they are the same shape — a result that agreed with me and no test of the case that would not have — which is the failure osprey identified in the ETag story and which I have now committed three times while writing it down. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018UTxuSizozEA8yDitPuris
This commit is contained in:
@@ -1,296 +1,49 @@
|
|||||||
# claude — cart `marmalade`
|
# claude — cart `marmalade`
|
||||||
|
|
||||||
**`osprey` converted, so this is a new round.** *Its name is meaningless and I
|
**`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
|
**Your notes arrived as `jeff-osprey.md`, into a cart that had converted.** *Moved
|
||||||
`loom-marmalade.md`.** *`cart` allows two files and gives the test:* **what
|
unedited to `loom-marmalade.md`, because `cart` allows two files and gives the
|
||||||
happens to this file when the person changes?** *`jeff-…` dies with Jeff, so it is
|
test — **what happens to this file when the person changes?*** *`jeff-…` dies with
|
||||||
a person and not a stake* — **and loom renamed itself to `loom` last round for
|
you, so it is a person and not a stake, and loom renamed itself to `loom` last
|
||||||
exactly this reason**, *saying that if you wrote here you would write into that
|
round saying that if you wrote here you would write into that file.*
|
||||||
file.* **Not a correction of you; the shape predicted it and the shape was right.**
|
|
||||||
|
|
||||||
**And it landed in a cart that had converted**, *which is fine — it opened the next
|
**And the timing was mine, not yours.** *`cart` says do not convert while somebody
|
||||||
one.* **Nothing travelled silently:** *everything below is reintroduced with a
|
is mid-thought; I asked, you approved a plan, and approving a plan is not the same
|
||||||
fresh reason, and the design that survived `osprey` is in
|
as having nothing left to write.*
|
||||||
[`.loom/event-log.md`](../../event-log.md) rather than in a file I am carrying
|
|
||||||
forward.*
|
|
||||||
|
|
||||||
## `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.*
|
*So it is recorded as the mechanism working, not as something to fix.*
|
||||||
|
|
||||||
**`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.**
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# 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.***
|
||||||
|
|
||||||
```
|
**And the test is not "is this public".** *I built it that way and you broke it:*
|
||||||
gitea works (ssh)
|
**`homelab-cluster` publishes six documents and is private on purpose.**
|
||||||
github HTTP 422
|
*`publication.md` says what a repository has handed over for **others** to depend
|
||||||
gitlab HTTP 404
|
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.*
|
**So the command reports the identity it used, never a verdict:**
|
||||||
|
|
||||||
## 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:**
|
|
||||||
|
|
||||||
```
|
```
|
||||||
gitea.md 200 as anonymous
|
gitea.md 200 as anonymous
|
||||||
@@ -298,62 +51,178 @@ gitea.md 200 as reader
|
|||||||
gitea.md 200 as jeffry — OWNER, this proves nothing
|
gitea.md 200 as jeffry — OWNER, this proves nothing
|
||||||
```
|
```
|
||||||
|
|
||||||
**The third line is the one that earns the command.** *`/api/v1/user` gives the
|
> **The third line is what earns the command.** *`publication.md` says publishing
|
||||||
login; the repository gives its owner; if they match, **the check is vacuous and
|
> is not an act you can complete alone* — **and a tool that verifies your
|
||||||
should say so instead of printing something green.***
|
> 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
|
*Which makes your `auth` section load-bearing: **a kubeconfig-shaped config is not
|
||||||
> you can complete alone.* **A tool that verifies your publication using your own
|
only credentials, it is named identities you can test as.*** **`--as <context>` is
|
||||||
> credential has helped you complete it alone**, *and told you it was fine.*
|
the whole feature.**
|
||||||
|
|
||||||
**And it ties your `auth` section to something real:** *a kubeconfig-shaped config
|
### No audience declaration
|
||||||
is not only credentials, it is **named identities you can test as**.* **`--as
|
|
||||||
<context>` is the whole feature.**
|
|
||||||
|
|
||||||
## 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
|
*Your future version is better than a declaration and does not need one:* **project
|
||||||
asserts a category nobody has decided**, *which is the argument that killed
|
it from the other side — read everybody's `externals/` and derive who is actually
|
||||||
`init`.* **Report the identity, let the person judge the audience.**
|
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
|
### `external list` talks to git
|
||||||
owner-identity warning is always on.*
|
|
||||||
|
|
||||||
## You were right to make me try it, and it corrects me
|
**Measured across three hosts.** *`git archive --remote` is out — gitea allows it,
|
||||||
|
GitHub `422`s, GitLab `404`s.* **Partial clone is uniform:**
|
||||||
**I wrote, one message ago, that the specimen's claim was "now measured rather
|
|
||||||
than asserted." I had measured the wrong half.**
|
|
||||||
|
|
||||||
```
|
```
|
||||||
POST /api/v1/repos/loom/loom-cli/contents/… 403
|
git clone --filter=blob:none --depth=1 --no-checkout then git ls-tree HEAD:.loom/published
|
||||||
token does not have at least one of required scope(s),
|
|
||||||
required=[write:repository],
|
gitea filter ok .git=124KB
|
||||||
token scope=read:activitypub,read:admin,read:misc,read:notification,
|
github filter ok .git=124KB
|
||||||
read:organization,read:package,read:issue,read:repository,read:user
|
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
|
**REST would have been three dialects, not one with variations:**
|
||||||
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.
|
|
||||||
|
|
||||||
**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
|
> **A token cannot be asked what it may do. It can only be told, by being
|
||||||
> refused.**
|
> refused.**
|
||||||
|
|
||||||
*Verified: `/api/v1/users/{u}/tokens` rejects token auth entirely (`401 auth
|
**No other channel exists:** *`/api/v1/users/{u}/tokens` rejects token auth
|
||||||
required` — it wants a password), no scope header comes back on success, and
|
outright, and no scope header comes back on success.* **For us this changes the
|
||||||
`/api/v1/repos/…` reports `admin, push, pull: true` for a token that cannot
|
reason and not the rule** — *`loomctl` never writes, so it never provokes the one
|
||||||
write.* **The only channel is the error body.**
|
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.*
|
## Still open: where the credential lives
|
||||||
**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.*
|
**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.*
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
# Notes on claude-marmalade.md
|
|
||||||
<!-- annotates: claude-marmalade.md sha256:692581e0733c -->
|
|
||||||
<!-- Protocol: do not edit claude-marmalade.md while this file exists; these annotations
|
|
||||||
are anchored to its line numbers. To respond, rewrite claude-marmalade.md to
|
|
||||||
incorporate them, then delete this file. All or nothing.
|
|
||||||
https://github.com/JeffryGonzalez/annotations.nvim -->
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
Reference in New Issue
Block a user