Skip to content

whoami and describe: answer the caller this app actually serves - #5

Merged
CraftOS-dev merged 7 commits into
mainfrom
korivi-whoami-401-hint
Sep 16, 2026
Merged

CraftOS-dev merged 7 commits into
mainfrom
korivi-whoami-401-hint

Conversation

@korivi-CraftOS

@korivi-CraftOS korivi-CraftOS commented Sep 10, 2026 •

Copy link
Copy Markdown
Collaborator

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 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. 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

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:

Anonymous caller Before After
read a record 200 200
write a record 401 401
whoami 401 — stricter than the surface it reports on 200, credentialId: anonymous
describe access "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 authorize the question it exists to answer. The 401 that remains is the honest one — on a multi-user app, or for a write, authorize really would refuse.

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.

Verification

  • 64/64 conformance. The check that asserted a 401 for any uncredentialled whoami is replaced — on a single-user app that expectation was the bug — by four that pin the truthful answer, the exact scope set (reads + readOnly ops, nothing else), describe's agreement, and the write still being refused.
  • New adapter unit test for the multi-user side, which cannot live in conformance: that harness boots one reference app and it is authMode: "none". It covers the 401 standing where it should, the hint surviving, and a credentialled caller still reporting exactly its own grant.
  • Verified against a running app — the table above is measured, not reasoned.
  • pnpm -r build, pnpm -r typecheck, pnpm -r test clean.

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.

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.
@korivi-CraftOS korivi-CraftOS changed the title fix: whoami's 401 carries the credential hint whoami and describe: answer the caller this app actually serves Sep 10, 2026
korivi-CraftOS added a commit that referenced this pull request Sep 10, 2026
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.
* 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.";

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@korivi-CraftOS

Copy link
Copy Markdown
Collaborator Author

Reverted, and the TODO from the review is tracked as #8.

reference/requirements.md and reference/tasks.md in blueprint-python-fastapi are back to the template as shipped — 43 and 2 placeholders. Simulating this merge into main gives 0 conflicts and restores both files, so merging overwrites what reached main by mistake.

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 validate. It now sits with the other two starting points, where validate refusing an unauthored spec is validate working:

[ ok ] blueprint-base:              validate refuses (starting point, not a finished app)
[ ok ] blueprint-python-fastapi:    validate refuses (starting point, not a finished app)
[ ok ] blueprint-pocketbase-react:  validate refuses (starting point, not a finished app)
[ ok ] blueprint-react-node:        validate

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 — credentialHint is app-supplied and forwarded into the agent's context verbatim, which is the actual injection path. It is written up as a post-#5/#6 change because the constant it concerns is not on main yet.

@CraftOS-dev
CraftOS-dev merged commit b658018 into main Sep 16, 2026
3 checks passed
korivi-CraftOS added a commit that referenced this pull request Sep 16, 2026
#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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants