Authentication
The Researcher Profiles specification does not require authentication. A server MAY serve profiles publicly with no credentials. This is the norm for static file hosts (S3, R2, GitHub Pages).
A server MAY require authentication on any endpoint. When it does, it MUST use bearer tokens:
Authorization: Bearer <token>An unauthenticated request to a protected endpoint MUST receive 401 with
{"detail": "invalid or missing bearer token"}.
A request for a profile or artifact above the caller’s access level returns
404, indistinguishable from a nonexistent resource.
Viewer tiers
Section titled “Viewer tiers”A server MAY implement viewer tiers to control which artifacts a caller can see. Three tiers are defined:
| Tier | Sees |
|---|---|
public | Public artifacts only |
internal | Public + internal artifacts |
restricted | All artifacts |
An unauthenticated caller defaults to the public tier. A server that does not
implement tiers treats all callers as public.
When viewer tiers are active, responses SHOULD include an X-RP-Viewer-Tier
header indicating the caller’s effective tier.
The key model
Section titled “The key model”A server that mints credentials SHOULD use prefixed, opaque bearer tokens so a key names its own family on sight. Two families are defined.
| Prefix | Family | Held by | Purpose |
|---|---|---|---|
rpk_ | app | An application, a script, or a person’s own command line | Read, push, and other whole-application powers |
rpa_ | agent | An assistant acting for one person, on one profile | Narrow, named write scopes |
A single key MUST NOT mix family-exclusive write scopes. An rpa_ key MUST NOT
carry app-only scopes (match, persona, push, etc.), and an rpk_ key MUST NOT
carry agent write scopes (profile:metadata, profile:narrative, etc.). The read
scope is family-neutral and MAY appear on either prefix. A server MUST reject a
mint request that mixes exclusive scopes. On data endpoints the prefix is a
label, never an authorization input: scopes and grants decide, and a key of
either family is judged by what it holds. Only on the management surfaces (such
as GET /api/manage/agent/whoami)
MUST a server refuse a key of the other family with 400 rather than 401,
naming the endpoint that would have worked.
A key’s plaintext MUST be shown exactly once, at mint time. A server MUST store only a hash of it, never the plaintext. A server SHOULD store a non-secret display prefix so a person can recognize a key in a list without revealing it.
A server MUST reject a key that is unknown, revoked, or past its expiry, and MUST make those three cases indistinguishable in the response, so an attacker learns nothing from probing.
Shared scopes
Section titled “Shared scopes”These are family-neutral and MAY appear on either rpk_ or rpa_ keys.
| Scope | Grants |
|---|---|
read | Read profiles at the key’s viewer tier |
App scopes
Section titled “App scopes”These attach to rpk_ keys. A server MAY define others.
| Scope | Grants |
|---|---|
match | Cross-profile matching |
persona | The persona endpoints |
push | Upload any profile |
push_own | Upload only profiles the key’s owning person holds an owner or editor role on |
resolve | Resolve a person descriptor to a rid, minting an internal stub on a miss |
push_own is what the command-line login flow
mints, and it is why the flow is safe to expose to any person with an account: a
key that escapes can still only touch profiles that person already controls.
Agent scopes
Section titled “Agent scopes”These attach to rpa_ keys. They are the vocabulary published by
GET /api/manage/agent/scopes. Every
one is a write scope; the read capability is the shared read scope above.
There is no wildcard, and neither family implies the other.
| Scope | Covers | Fields | Dangerous | Default |
|---|---|---|---|---|
profile:metadata | PATCH /profiles/{slug}/metadata | field, subfields, summary, expertise, interests, not_interests | no | on |
profile:history | PATCH /profiles/{slug}/metadata | training, career | no | on |
profile:identity | PATCH /profiles/{slug}/metadata | name, affiliation, job_title, same_as | no | off |
profile:narrative | PUT /profiles/{slug}/soul | no | on | |
profile:visibility | PATCH /profiles/{slug}/visibility | yes | off |
profile:visibility is marked dangerous because an agent holding it on a
published profile can put content behind a wall. It MUST allow narrowing only. A
request that would raise a tier (restricted to internal, or internal to
public) MUST be refused with 403 naming the artifact, its current tier, and
the requested tier.
Acts no agent key may hold
Section titled “Acts no agent key may hold”A server MUST NOT define a scope that delegates any of the following. These are decisions belonging to the person the profile describes.
| Act | Why |
|---|---|
| Publish or unpublish | Publishing is a decision by the person the profile describes. |
| Delete the profile | Only the owner may delete a profile. |
| Grant or revoke access | Only the owner may grant access, including minting another agent. |
| Mint or revoke an agent key | A leaked key must not be able to mint a successor. |
| Widen visibility | Making something more visible is an exposure decision. |
A server SHOULD publish this list at
GET /api/manage/agent/whoami as
never_delegable, so an agent can tell its user what it cannot be given.
Insufficient scope
Section titled “Insufficient scope”A request whose key is valid but lacks the required scope MUST return 403 with
this body, not 401 and not a bare {"detail": ...}:
| Field | Type | Description |
|---|---|---|
error | string | The literal "insufficient_scope" |
required | list[string] | Scopes this request needs |
granted | list[string] | Scopes the key holds |
missing | list[string] | required minus granted |
hint | string | One sentence naming the blocked fields and the fix |
{ "error": "insufficient_scope", "required": ["profile:identity"], "granted": ["profile:metadata", "profile:history", "profile:narrative"], "missing": ["profile:identity"], "hint": "The fields ['name'] require scope(s) ['profile:identity']. Ask your owner to add them."}A client MUST NOT retry on this response. It should report the missing scope and stop.