Repository navigation
whoami and describe: answer the caller this app actually serves - #5
Conversation
whoami answered an uncredentialled caller with a bare 401 while the write path attached a `how` field to the identical challenge. The two are the same question asked in two places, and only one of them answered it. This is the 401 an agent is most likely to meet first. whoami is where a caller reads its grant BEFORE planning work — that is the whole point of the endpoint — so it is the first call an agent makes after being pointed at an app, and a bare challenge there tells it to go acquire a credential without saying where from. The default hint is now one constant used by both challenges, rather than a string literal duplicated at one of the two sites. Its comment records what the default actually is: an answer only the app's owner can act on, since it names a file on the host. An app serving anyone else sets `credentialHint`. Adds the Class A check that would have caught this. It fails against the previous adapter (59/60) and passes now (60/60) — the suite covered `has: [how]` on the write challenge and had no check at all for an uncredentialled whoami, which is why the gap survived.
whoami and describe were each telling an uncredentialled caller something the
app would not honour, in opposite directions.
`authorize` requires a credential for a write and for any operation not declared
`readOnly`, but reaches its anonymous context BEFORE the scope check, so on a
single-user app an uncredentialled caller reads records perfectly well. Measured
against a running app, that caller got:
read a record 200
write a record 401
whoami 401 ← stricter than the surface it reports on
describe access "full" ← claims the write that was just refused
Both are now the same answer as `authorize`:
whoami 200, credentialId "anonymous", scopes = reads + readOnly ops
describe access "read-only"
whoami asks `authorize` the question it exists to answer, instead of demanding a
credential of its own. That mattered most to the agent doing the right thing:
reading its grant BEFORE planning work is the documented order, and it stopped
at a gate it did not have to, never learning it could have read everything. The
401 that remains is the honest one — on a multi-user app, or for a write,
`authorize` really would refuse — and it still carries the `how` the write path
sends.
describe's over-report was the worse of the two. An agent reads `access` to know
what it may plan, so "full" for a caller that cannot write is not a cosmetic
mismatch: it is a plan built on permission that was never held, spent and
rejected a round trip later.
A caller without a grant was never scope-checked, so it has no scope set of its
own to report. Rather than inventing one, whoami renders the very `Access`
describe uses — one caller cannot be told two different things about itself,
because there is only one source.
NOTHING IS NEWLY PERMITTED. Enforcement is untouched: the write still answers
401, the destructive operation still answers 401. Only the reports changed, from
wrong to right.
Conformance replaces the check that asserted a 401 for any uncredentialled
whoami — on a single-user app that expectation was the bug — with four that pin
the truthful answer, the scope set, describe's agreement, and the write still
being refused. The multi-user side cannot live there (that harness boots one
app, and it is authMode "none"), so it is an adapter unit test alongside it.
64/64 conformance, and verified against a running app.
The gate-40 paragraph described whoami demanding a credential of its own, which was true when it was written and is the bug #5 fixes. An uncredentialled caller on a single-user app is now answered as `anonymous`, holding the reads and read-only operations it genuinely has — so reading the grant before planning is worth the round trip even with no credential in hand, which is the opposite of what this paragraph used to imply. Also says that `scopes` and describe's `access` are rendered from one source and cannot disagree, rather than naming `access` alone as the authority. That was only true while whoami could refuse the caller it was describing.
CI has been red on main for five commits. Three blueprints failed
`scaffold → canon → validate` with 43 unfilled `<REPLACE: …>` placeholders in
`reference/requirements.md`, and the suite read that as three broken blueprints.
It was one broken assertion and one genuinely missing spec.
The assertion said every blueprint scaffolds into an app that passes `validate`.
That is true of react-node, which ships a finished to-do starter — a View, a
model and operations. It is not true, and is not meant to be true, of two others:
blueprint-base framework files only, no runtime code at all: no
entities, no operations, a pipeline of placeholder
echoes. The author picks the stack.
blueprint-pocketbase-react adapter hooks only. Its `tasks` collection is
created by hand in PocketBase and `install` prints
"download the pocketbase binary".
For those two, `validate` refusing a fresh scaffold is `validate` WORKING. An
unauthored spec must not pass, or the check means nothing on the apps that are
finished. The only way to make the old assertion true would have been to invent
a spec for an app that does not exist — three fictional documents to turn a
truthful red into a decorative green. So the check is inverted for them instead,
and each carries the reason it is incomplete, in a string that should read
wrongly the day someone finishes the blueprint and forgets this line.
blueprint-python-fastapi was the real gap. It ships a complete, runnable agent
app — model, operations, adapter, working pipeline — and simply had no spec
written. It has one now, describing what it actually is rather than what the
other starter is: an app whose interface is the agent, with no browser View.
Q1–Q9 of the Quality Standard are recorded n/a with that reason rather than
quietly skipped, so an author who later adds a View knows those entries are
theirs to fill in. Its gate now passes end to end, framework files included.
Conformance: 57/60 with three failures, to 60/60.
…ivi-whoami-401-hint
| * default describes a file on the host, which is an answer only its owner can act | ||
| * on. | ||
| */ | ||
| const DEFAULT_CREDENTIAL_HINT = "Read the app's .agent-token file (mode 0600) in the project directory."; |
There was a problem hiding this comment.
No changes needed but need to create a todo to place agent context/prompt/hint to somewhere. To prevent context injected without our knowledge.
Restores `reference/requirements.md` and `reference/tasks.md` in blueprint-python-fastapi to the template as shipped, placeholders intact. Merging this overwrites what reached main. Those files were authored to make a red suite green. Wrong call: they are the blueprint's own spec, and a starter that hands its author a pre-written one has decided for them what the app is. The template is unfilled on purpose. The suite no longer asks for one. blueprint-python-fastapi joins the other two starting points, where `validate` refusing an unauthored spec is `validate` working — the same reason already recorded for base and pocketbase-react. Its runtime and model are complete; only the spec is deliberately not. Conformance stays 64/64, now by stating what the blueprints are rather than by supplying what they were missing.
|
Reverted, and the TODO from the review is tracked as #8.
One thing beyond a plain revert, flagged because it is a judgement call rather than a mechanical undo: reverting the spec alone puts the suite back to red, since it still expected blueprint-python-fastapi to pass Its runtime and model are complete; only the spec is deliberately not. If you would rather have the bare revert with no harness change, say so — that leaves CI red until the spec is written, which may be the honest state to sit in. 64/64, all checks green. The same revert is applied to #6 so it cannot reintroduce those files whichever order they merge. On the inline comment: no code change here, as you said. #8 records it, including the part worth naming — |
#5 landed on main and brought `DEFAULT_CREDENTIAL_HINT` — the same sentence this branch had already moved into `agentText.ts`. Both sides changed the same line, and keeping both would have left one string with two homes, which is the thing this change exists to fix. Resolved to the bounded call. The local constant had exactly one use, so it is now dead and is removed: the text lives in `agentText.ts`, where the rule for adding agent-facing text is written down, and the app-supplied part goes through `boundAppText` on its way into the reply. Nothing about #5's behaviour changes — the same sentence reaches the same 401, by the same fallback. What changes is that an app can no longer put unbounded multi-line text beside it. 73/73 conformance; whoami and agent-text both passing.
Two fixes to the same question: what does this app tell a caller it may do, and does it mean it?
1. whoami's 401 carried no hint
whoami answered an uncredentialled caller with a bare
401while the write path attached ahowfield to the identical challenge. The two are the same question asked in two places, and only one of them answered it. The default hint is now one constant used by both, and its comment records what that default actually is — an answer only the app's owner can act on, since it names a file on the host.2. whoami and describe were both lying to an uncredentialled caller
authorizerequires a credential for a write and for any operation not declaredreadOnly, but reaches its anonymous context before the scope check — so on a single-user app an uncredentialled caller reads records perfectly well.Measured against a running app:
200200401401whoami401— stricter than the surface it reports on200,credentialId: anonymousaccess"full"— claims the write just refused"read-only"whoami was stricter than the thing it describes. That hurt the agent doing the right thing: reading your grant before planning work is the documented order, and it stopped at a gate it did not have to, never learning it could have read everything. It now asks
authorizethe question it exists to answer. The 401 that remains is the honest one — on a multi-user app, or for a write,authorizereally would refuse.describe's over-report was the worse of the two. An agent reads
accessto know what it may plan, so"full"for a caller that cannot write is not a cosmetic mismatch: it is a plan built on permission that was never held, spent and rejected a round trip later.A caller without a grant was never scope-checked, so it has no scope set of its own to report. Rather than inventing one, whoami renders the very
Accessdescribe uses — one caller cannot be told two different things about itself, because there is only one source.Nothing is newly permitted
Enforcement is untouched. The write still answers
401; the destructive operation still answers401. Only the reports changed, from wrong to right.Verification
readOnlyops, nothing else), describe's agreement, and the write still being refused.authMode: "none". It covers the 401 standing where it should, the hint surviving, and a credentialled caller still reporting exactly its own grant.pnpm -r build,pnpm -r typecheck,pnpm -r testclean.Note
Independent of #4 and does not block it. #4's connect skill documents the current behaviour of a whoami
401; once this merges that paragraph is stale by one sentence, and I can update it in whichever order you merge.