Webpath — Conformance Guide & Test Cases

How to tell whether a host actually implements Webpath. Every case below is a black-box check over plain HTTP — no internal access, no SDK. Point it at any host that claims Webpath support and run it.

🧪 Runnable starter kit available; full suite coming soon. A drop-in script (conformance.sh) runs the L1 Core cases against any host — it joins a throwaway space, checks each rule, and tears the space down on exit:

WEBPATH_HOST=https://knower.cc ./conformance.sh

The complete L2/L3 suite (append, reserved-path projections, QR, search, sharing) is in progress. Every case below also carries a curl you can run by hand.

knower.cc is the reference host (it implements the spec as its native API); these cases pass against it.


How to read a case

Each case has an ID, the rule it asserts (the normative line from the spec), a probe (a request), and the expected result. Keywords MUST / MUST NOT / SHOULD are normative — a host failing a MUST is non-conformant.

# Set up once. Use a throwaway space per run.
export HOST=https://knower.cc          # the platform host under test
export NS=$(curl -s -X POST $HOST/join -d "{\"name\":\"wpconf-$(date +%s)\"}")
export SPACE=$(echo "$NS" | sed -E 's/.*"space":"([^"]+)".*/\1/')   # https://wpconf-….knower.cc/
export TOKEN=$(echo "$NS" | sed -E 's/.*"token":"([^"]+)".*/\1/')   # pth_…

Levels: L1 Core (the data plane + identity surface — required to claim conformance) · L2 Full (reserved-path projections, search, QR, sharing) · L3 Optional (versioning, subscriptions, custom domains — out of scope here).


1 · Identity & accounts (L1)

Email IS the account — no passwords. Sign-in never enumerates accounts.

IDRuleProbeExpect
WP-A1OTP request MUST always answer 204, account or not.POST /api/v1/auth/otp {"email":"x@y.z"}204, no body, whether or not x@y.z has an account
WP-A2Verify MUST exchange a correct code for a session and create the account on first verify.POST /api/v1/auth/verify {"email","code"}200, sets a session cookie, created:true first time
WP-A3A wrong/expired/locked code MUST fail closed.verify with a bad code401 otp-invalid, detail names attempts remaining
WP-A4OTP MUST be rate-limited.4th request inside 15 min for one email429 with a Retry-After header
WP-A5The session cookie MUST be host-scoped.inspect Set-Cookie__Host--prefixed, Secure, Path=/, no Domain — not sent to sibling spaces

2 · Spaces (L1)

A space IS a hostname. Joining mints exactly one label.

IDRuleProbeExpect
WP-S1/join MUST mint <name>.<host> and return a usable key.POST /join {"name":"foo"}201 with space, token (pth_), expires_at, llms_txt, paste
WP-S2A taken name MUST be refused with suggestions.join the same name twice409 space-name-taken, ≥1 suggestion
WP-S3An invalid/reserved name MUST be refused, not silently mangled.join "a.b", "www", "api"422 (space-name-invalid / reserved); a dot is rejected (one label only)
WP-S4A new space MUST start empty (nothing is seeded).GET $SPACE?list200, zero entries

3 · Keys & scope (L1)

Keys are scoped to a space, a path prefix, and a set of ops. Shown once.

IDRuleProbeExpect
WP-K1Minting MUST return the secret exactly once.POST /api/v1/tokens {scope}201 with pth_…, jti; subsequent GET /api/v1/tokens shows label+jti, never the secret
WP-K2Revocation MUST take effect quickly and globally.DELETE /api/v1/tokens/<jti>key stops working (≤60 s); a write with it → 401 token-revoked
WP-K3A key MUST NOT work outside its scope.use a /notes/-scoped key to write /other.md or another host403 out-of-scope, never a cross-scope write
WP-K4A key MUST NOT reach the control plane.call POST /api/v1/tokens with a pth_ key403 (admin op)
WP-K5revoke-all MUST invalidate every prior key at once (owner only).POST /api/v1/spaces/<ns>/revoke-all200, key_generation increments; old keys → 401, fresh key works; non-owner → 403

4 · Data plane — CRUD (L1)

The URL is the storage key. Verbs do exactly what HTTP says.

IDRuleProbeExpect
WP-D1PUT then GET MUST round-trip the bytes verbatim.PUT /notes/t.md then GET /notes/t.mdbytes identical; 201 (new) / 200 (replace); ETag on both
WP-D2Content-Type MUST be inferred from the extension.GET the file aboveContent-Type: text/markdown (not whatever the client declared)
WP-D3A declared Content-Type MUST NOT override the extension under a restricted policy.PUT notes.md with header Content-Type: text/htmlstored/served as text/markdown (no smuggled HTML)
WP-D4Directory GET (trailing slash) MUST list.GET /notes/?list200 JSON, includes t.md
WP-D5DELETE MUST remove the file; the next GET MUST 404.DELETE /notes/t.md then GET204, then 404 as application/problem+json
WP-D6Whole-space/prefix DELETE MUST require confirmation.DELETE $SPACE without ?confirm400 confirm-required; with ?confirm=<host> → proceeds

5 · Clean URL vs source (L1)

A clean URL is the rendered view; an explicit extension is verbatim source.

IDRuleProbeExpect
WP-R1A clean URL MUST resolve <path>.md (rendered) then <path>.html (served).GET /page (with page.md present)200 text/html, markdown rendered, Content-Location: /page.md
WP-R2An extension URL MUST return raw source, never rendered.GET /page.md · GET /page.html.md → text/markdown; .html → text/plain (browser does not execute it)
WP-R3Rendered markdown MUST be sanitized.render a .md containing <script>/onerror=scripts/handlers stripped from HTML; source bytes via .md stay verbatim
WP-R4A clean URL with no backing file MUST 404.GET /nothing404 problem+json

6 · Visibility & privacy (L1 — ship-blocking)

private (default) · unlisted · public. A private file MUST NOT leak its existence.

IDRuleProbeExpect
WP-V1New files MUST default to private.PUT /m.html (no ?visibility)?meta shows visibility:"private"
WP-V2A private file MUST be invisible to anonymous readers — same 404 as a missing file.GET /m incognito (no key)404 byte-identical to a truly-missing path (no existence leak)
WP-V3A valid key/owner session MUST read the private file.GET /m.html?token=$TOKEN200
WP-V4Visibility MUST be settable on write and flippable later.PUT …?visibility=public; PATCH ?meta {"visibility":"public"}public immediately; anonymous clean URL then renders
WP-V5Private resolution MUST NOT leak via the clean-URL fallback.private page.md, GET /page incognitodenied — never renders a private source

7 · Append / prepend (L2)

Line-safe accumulation without a read-modify-write round-trip.

IDRuleProbeExpect
WP-P1Append to a missing file MUST create it.PUT /log.jsonl?append (no file)201
WP-P2Line-oriented appends MUST insert a join newline when the prior tail lacks one.append {"b":2} to {"a":1} (no trailing \n)two distinct lines, never glued
WP-P3Concurrent appends MUST NOT lose data.two appends in parallelboth records present (CAS)
WP-P4Whole-file/binary formats MUST refuse append.?append on .json / .html / .png409 not-appendable, hint = GET→modify→PUT
WP-P5A malformed line body MUST write nothing.?append a broken JSON line to .jsonl400 invalid-append-body, file untouched
WP-P6?append and ?prepend together MUST be rejected.both flags on one request400

8 · Conditional requests & concurrency (L1)

ETags everywhere; preconditions prevent lost updates.

IDRuleProbeExpect
WP-C1Every response MUST carry an ETag.any GET/PUTETag header present
WP-C2A stale If-Match write MUST be rejected.PUT with an old ETag412, body links the current version
WP-C3A fresh If-Match write MUST succeed.re-read, retry with the new ETag200
WP-C4Conditional GET MUST support If-None-Match.GET with the current ETag304 Not Modified
WP-C5Range reads SHOULD be supported.GET with Range: bytes=0-3206 with the partial body

9 · Reserved paths — control plane = data plane (L2)

Config, links, audit, stats, the skill file are all files at reserved paths, synthesized on read so they can't drift. _-prefixed basenames are never user-writable.

IDRuleProbeExpect
WP-X1Every space MUST serve llms.txt, ≤1,500 bytes, no auth, naming its real host.GET /llms.txt200 text/plain, < 1500 bytes, contains the space hostname
WP-X2llms.txt MUST be read-only (it's generated).PUT /llms.txt405 llms-generated with a hint
WP-X3_config.json MUST mirror config (read) and accept only editable fields (PATCH).GET /_config.json; PATCH {"uploads":["md"]}; PATCH {"key_generation":9}read OK; edit OK; server-owned field → 422 field-not-writable; PUT → 405
WP-X4_links.json MUST be the owner-only links projection and refuse writes.GET /_links.json (owner) ; PUT200 index; write → 405; anonymous → 404
WP-X5.well-known/jwks.json MUST publish the verification keys.GET /.well-known/jwks.json200, a JWK set, public, no secrets
WP-X6An audit/stats feed, if present, MUST be owner-only.GET /_audit, /_stats anonymously404/401 — never expose actors or private paths

10 · Errors — RFC 7807 (L1)

Every non-2xx is application/problem+json with an agent-relayable detail.

IDRuleProbeExpect
WP-E1Every error MUST be application/problem+json.any failing requestContent-Type: application/problem+json, body has type/title/status/detail
WP-E2detail MUST be a full sentence a non-technical user can act on.revoke a key, then PUT …?token=<dead>401, detail names the space and how to get a new key
WP-E3type MUST be a stable URI.inspect any errortype is an absolute URL under a stable errors namespace
WP-E4A dead key on a public read MUST be ignored (still 200); on a write it MUST 401.GET public?token=<dead> ; PUT …?token=<dead>200 ; 401 token-revoked

11 · Token hygiene on the wire (L1)

A key carried in the URL must never persist or echo back.

IDRuleProbeExpect
WP-T1Any ?token= response MUST be private, no-store.GET …?token=$TOKEN -iCache-Control: private, no-store
WP-T2The token MUST NOT appear in any response body, Location, Content-Location, or links.grep the full response for the tokenabsent everywhere
WP-T3A header token and a query token that disagree MUST be rejected.Authorization: Bearer A + ?token=B400

12 · QR & search (L2)

IDRuleProbeExpect
WP-Q1?qr MUST render the URL as a QR on any path, with no storage read or auth.GET /anything?qr (text/html vs default)HTML shell + inline SVG for browsers; Unicode QR + URL for terminals; ?qr=svg/?qr=txt explicit; bad value → 400
WP-Q2?qr MUST NOT encode the token into the QR.GET /p?qr&token=$TOKENencoded URL is token-free; response private, no-store
WP-F1?search SHOULD rank results across the space (scope by prefix).GET /?search=<term>200, ranked matches over path/title/description

Pass criteria

GateBar
Conformant (L1 Core)All WP-A*, S*, K*, D*, R*, V*, C*, E*, T* green
Full (L2)Plus WP-P*, X*, Q*, F*
Privacy floorWP-V2, WP-V5, WP-E2, WP-T2 are non-negotiable — a failure here is a security defect, not a missing feature

If a host passes L1, an agent that knows only the API Reference can drive it unaided — which is the whole point.


→ API Reference · Build an App · Examples · Webpath home