AIP-54: REF — ref/v1 (typed cross-AIP artifact reference)
One typed, cross-AIP reference shape — `{aip, id, version?}`, serialized as an `aip://<aip>/<id>[@version]` URI — that any AIP artifact can use to point at any other AIP artifact, resolved through per-family AIP-43 registries joined by a RefCatalog, plus the `ws://<collection>/<body>` scheme for pointing at resources in the world (files, URLs, identities, transactions) — superseding AIP-27, which owned both jobs separately. Replaces every per-primitive reference mechanism (AIP-18's collection-scoped `refKind`, inline doctype handles, bare id strings) with one discriminated, resolvable, loudly-failing reference.
| Field | Value |
|---|---|
| AIP | 54 |
| Title | REF — ref/v1 (typed cross-AIP artifact reference) |
| Status | Draft |
| Type | Schema |
| Requires | AIP-1, AIP-2, AIP-43 (REGISTRY — the resolution substrate) |
| Reference Impl | @agentproto/ref |
| Supersedes | AIP-27 (REF — absorbed whole; see Backwards Compatibility) |
| Resources | ./resources/aip-54 — REF.schema.json, WS-REF.schema.json |
Abstract
This AIP defines two sibling reference schemes:
aip://— the artifact scheme. One typed, cross-AIP reference — theref/v1shape:{aip, id, version?}, serialized in string contexts asaip://<aip>/<id>[@version]. A ref names an artifact by its owning AIP number and that AIP's registry key, optionally pinned to a version. Resolution delegates to per-family AIP-43 REGISTRY instances joined by aRefCatalogthat owns theaip → (family, registry)table. Unresolvable refs are loudly typed failures — a dangling id string never masquerades as a resolution.ws://— the resource scheme. Aws://<collection>/<body>URI points at a resource in the world — a workspace file, a URL, an identity, a transaction, a persona — discriminated by the normative collection→kind table (§Reference syntax), with kind-specific body grammars and a tenant-scope mandate.
Which to use when: use aip:// when the target is an AIP
artifact resolvable through a registry family; use ws:// when the
target is a resource in the world that no AIP registry owns. The two
are siblings, not alternatives — §Reference syntax states each
scheme's discriminator and resolution rules separately.
A ref IS NOT a handle. It is inert data pointing at one; the registry owns the handle. A ref IS NOT resolvable by itself — resolution is the host's catalog.
Motivation
Every AIP that references another artifact has invented its own mechanism, and none of them interoperate:
- AIP-18's
reffield is an id string scoped to exactly one named collection (refKindnames "the target collection'sname") with no shape guarantee at the schema level. It cannot cross AIPs. - Inline doctype handles (e.g. app-kit's
attach) serialize the whole referenced bundle, carry no reliable discriminator field, and cannot be rehydrated — nothing downstream resolves an id back to a handle. - Bare strings (AIP-42's
AnyRef, AIP-52's$resolver) are wire formats, not types; validity is call-site convention. - Per-primitive
xReffields are the tell: AIP-55 (pricing) neededappRefANDpackRefAND a knowledge-pack string, and every future primitive that references anything would add another. - AIP-27 (superseded) solved the resource
half —
ws://<collection>/<body>pointers at files, URLs, identities, transactions — with its own kind registry, compact form, and extension mechanism, but named no artifact scheme, and AIP-27'sresources:pointed at a directory that exists in neither repo. This AIP supersedes it and absorbs thews://scheme whole (§Reference syntax); theaip://artifact scheme is the half AIP-27 never had.
The gap is one level below any single primitive: there is no general, typed, cross-AIP reference. This AIP fills it with the smallest contract a third party can implement against: a three-field object, a URI form, and a catalog/registry resolution rule.
Specification
The ref shape
{ "aip": 42, "id": "book-companion", "version": "1.2.0" }| Field | Type | Req | Notes |
|---|---|---|---|
aip | integer ≥ 1 | yes | The referenced artifact's owning AIP number — the type discriminator that names the registry family (42 = app, 52 = pack, 14 = tool, …). |
id | string 1..512 | yes | The artifact's registry key in its family — the same key the family's keyBy derives. Hosts MUST use the family's keyBy; a ref id and its registry key MUST NOT be allowed to drift. |
version | semver | no | Advisory pin recording what was meant at authoring time. Absent = floating. AIP-43 registries do not version; pins are metadata for hosts. |
Full normative constraints: REF.schema.json.
URI serialization
In string-typed contexts (YAML frontmatter, query strings, prose) a ref serializes as:
aip://<aip>/<id>[@version] e.g. aip://42/book-companion@1.2.0The mapping is lossless and total: refToUri / refFromUri round-trip
every valid ref and reject malformed input.
Reference syntax
Three surfaces name things. They look alike and are not interchangeable:
| Form | What it is | Example |
|---|---|---|
aip://<aip>/<id>[@version] | the artifact scheme — an AIP artifact resolvable through a registry family (§Resolution) | aip://42/book-companion@1.2.0 |
ws://<collection>/<body> | the resource scheme — a resource in the world, with <kind>:<body> as its compact spelling (this section) | ws://operators/atlas |
<type>://<selector> | a matcher — NOT a ref | role://owner, user://current, * |
Which to use when. If the target is an AIP artifact — something
some AIP's defineX constructor produced and a family registry keys —
the ref is {aip, id, version?} and the URI spelling is aip://. If
the target is a resource in the world — a file, a URL, an identity, a
transaction — no registry family owns it, the discriminator is the
collection segment, and the URI spelling is ws://. A host MUST
NOT accept aip:// forms in ws:// fields or vice versa: the
discriminators are disjoint by construction (aip://'s first path
segment is an integer ≥ 1; ws://'s is a lowercase-alpha-dash
collection name).
ws:// — the resource scheme
ws-ref = "ws://" collection "/" body
collection = lowercase-alpha-dash ; the PLURAL catalog segment
body = kind-specific (see Per-kind bodies)ws://<collection>/<body> and <kind>:<body> denote the same
resource ref and MUST round-trip. Implementations MUST accept both on
input and MUST emit the compact form when serializing to a scalar.
The collection segment maps to a kind by table, never by string
manipulation. Stripping a trailing s is not the rule and does not
work: agencies, assemblies, policies, identities, and
offices all break it. The table is authoritative:
ws:// collection | kind | Doctype |
|---|---|---|
operators | operator | AIP-9 |
personas | persona | AIP-25 |
users | user | host user registry |
companies | company | AIP-6 |
collections | collection_doc | AIP-18 — see §Seams |
wikis | wiki | AIP-10 |
workspaces | workspace | AIP-34 — see §Seams |
skills | skill | AIP-3 |
identities | identity_workspace | AIP-23 — see §Seams |
playbooks | playbook | AIP-12 |
agencies | agency | AIP-21 |
tools | tool | AIP-14 |
roles | role | AIP-47 — see §Seams |
assemblies | assembly | AIP-24 |
actions | action | AIP-39 |
policies | policy | AIP-38 |
workflows | workflow | AIP-15 |
offices | office | AIP-22 |
avatars | avatar | AIP-25 asset |
orgs | org | host org registry |
guilds | guild | host guild registry |
Kinds above beyond the compact-grammar base set are registered by their owning AIP; this table is the reservation record. A consumer MUST NOT invent a collection — add a row here first (PR against this spec, per the AIP-1 process).
Per-kind bodies
Each kind's body is a slug or a multi-segment path per the owning
AIP's naming rules. The compact-grammar base set (a ws:// ref's
body reuses these grammars verbatim, and any <kind>:<body> form
MUST round-trip with its ws:// spelling):
local-body = path ; workspace-relative, no leading /, no .. escape
url-body = href ; http / https only
git-body = pct-encoded-url "@" ref [ ":" path ]
github-body = owner "/" repo [ "@" ref ] [ ":" path ]
ipfs-body = cid [ ":" path ]
email-body = address
operator-body = slug [ "@" workspace ] ; @workspace = cross-workspace disambiguator
user-body = id [ "@" workspace ]
persona-body = id
eth_tx-body = chainId ":" txHash ; EIP-155 chainId; 0x-prefixed lowercase hex
ots-body = inner-compact-ref ; e.g. ots:local:engagements/.../247.otsReserved characters in path/url bodies (@, :, #, %) MUST be
percent-encoded. Kinds MAY carry an optional sha256 content binding
as a #sha256=<64-hex> suffix on the compact form, MUST be ignored
during ws://↔compact round-tripping beyond that suffix, and paths
MUST be workspace-relative with escape attempts rejected at parse
time.
Base collections
Every kind belongs to one or more collections — the
cross-cutting categorization axis that lets manifests constrain a
field to "any kind of <category>" without enumerating kinds. The
base set is normative; new collection names SHOULD be agreed via the
AIP-1 process when widely shared. Membership is defined at the kind,
not the value, and is open: integrations MAY register kinds into new
collections.
| Collection | Members | Purpose |
|---|---|---|
file | local, url, git, github, ipfs | Resolvable to bytes; the artifact / content axis. |
identity | operator, user, persona, email | Names a principal (human or agent). Used by signer fields, assignees, mentions. |
anchor | eth_tx, ots | External tamper-evidence witnesses. |
chain | eth_tx | On-chain anchor subset of anchor — verification depends on chain RPC, not just file fetch. |
Multi-segment bodies
A body MAY carry more than one segment. This is not an exception: it
is the kind-specific body grammar github and git already use for
:path. Forms in use, all normative in consuming AIPs:
ws://wikis/<slug>/KNOWLEDGE.md ; doc inside a wiki
ws://workspaces/<slug>/WORK.md ; doc inside a workspace
ws://assemblies/<slug>/<member> ; member inside an assembly
ws://companies/<slug>/teams/<team> ; nested collection itemEach kind's body grammar declares its own segment structure.
Consumers MUST NOT assume ws://<collection>/<slug> is exhaustive.
ws:// resolution rules
The resource scheme's discriminator is the collection segment, resolved by table — never by string manipulation:
- Table dispatch. Take the segment up to the first
/as the collection, map it to a kind via the table above; a collection with no row is a parse-time failure (UnknownRefCollection), and the remainder is parsed against the kind's body grammar (InvalidRefBodyon mismatch). - Tenant-local by mandate. Hosts MUST resolve
ws://refs in the same tenant scope as the citing file and reject cross-tenant bindings (AIP-23 —identity_xref_unresolvable). - Loud failure. As with
aip://, an unresolvablews://ref is a typed failure, never a silent dangling string. - Source of truth is the target AIP. The
ws://ref is only a wire shape; the kind's owning AIP owns what exists (e.g. the AIP-10 wiki decides which pages exist).
The tenant-scope asymmetry is part of the contract. This is a
real asymmetry with the compact form, and the reason the two are not
blindly substitutable: operator:<slug>[@<workspace>] carries an
OPTIONAL cross-workspace disambiguator; ws:// has no such
affordance, and its consumers are required to refuse what that
disambiguator expresses. A host MUST NOT introduce an
@<workspace> suffix while rewriting a ws:// ref into compact
form — that silently widens a security envelope the citing spec
mandates be closed. The rewrite is lossless only when the suffix is
absent.
Matchers are not refs
role://owner, user://current, group://<name>, *, and the
wildcard forms used in appliesTo (ws://orgs/*, ws://tools/admin/*)
are matchers: they select over a set at evaluation time. They are
NOT refs, MUST NOT be registered as kinds, and MUST NOT be constructed
as refs by any implementation of this AIP.
The distinction is not stylistic. A ref denotes exactly one thing and
resolves to it (§Resolution). A matcher denotes a predicate,
evaluated by set intersection against a caller's identity tree.
user://current names nothing until a request exists — it is a
pronoun. role://owner is relative to a resource. * has no referent
by construction. The ws:// grammar admits no wildcard precisely
because a wildcard is not an address.
Matcher grammar belongs to the AIP that evaluates it — AIP-38 for policy principals. A field is either a ref field or a matcher field; a field accepting both MUST say so and MUST dispatch on form rather than guess.
Seams
Four places where one word does two jobs. Each is deliberate; none may be collapsed.
role.ws://roles/<slug>is an AIP-47 job template — a ref.role://owner|editor|vieweris an AIP-38 RBAC tier relative to a resource — a matcher. AIP-47 §Role vs Position vs Access role requires these stay distinct and calls conflating them a collapse of HR into security. They share a word and nothing else: different grammar, different spec, different layer. A host resolving one as the other has made the exact mistake AIP-47 forbids.identity. §Base collections registersidentityas a collection — the unionoperator|user|persona|email.ws://identities/<slug>addresses an AIP-23 identity workspace, a different thing. The kind is thereforeidentity_workspace.collection. "Collections" names kind groupings above (file,identity,anchor,chain).ws://collections/<name>addresses an AIP-18 typed collection doctype. The kind iscollection_doc, for the same reason.workspace. AIP-34 ids are globally addressable as@<owner-slug>/<workspace-slug>— two segments, not a bare slug. So the body is that whole id (workspace:@acme/marketing-ops), andws://workspaces/@acme/marketing-opsis well-formed. Hosts MUST NOT assume a workspace body is a single segment.
Resolution
This is the aip:// artifact scheme's resolution. (ws:// resource
refs resolve per §ws:// resolution rules — table dispatch plus the
tenant-scope mandate, not registry joins.) Resolution joins two
existing halves:
- Handles come from every AIP's
defineXconstructor. - Registries are per-family AIP-43
createRegistry<H>({ family, keyBy })instances.
The missing join is the catalog:
interface RefCatalog {
registerFamily<H>(aip: number, spec: { family: string; keyBy?: (h: H) => string },
registry: Registry): void
resolve(ref: ArtifactRef): { handle: unknown; family: string } | undefined
resolveStrict(ref: ArtifactRef): { handle: unknown; family: string } // throws
}Normative rules:
- keyBy identity. The
keyByused to derive a ref's id MUST be the same function the family's registry uses. - Unreferenceable handles. A handle with no registry key (no
id/provider/slug/name) MUST be refused at ref construction. Anonymous handles (e.g. an AIP-42 app with noid) are unreferenceable; hosts MUST refuse. - Loud failure. Resolution of an unknown
aipor unknownidreturnsundefined(lenient mode) or throws typedRefFamilyError/RefUnresolvableError(strict mode). A dangling ref MUST never masquerade as a resolution. - Ref is inert. Frozen at construction; carries no behavior.
- One family per aip. Re-registering replaces the binding
(hot-reload parity with AIP-43's
replace).
Deriving a ref from a handle
function refFor<A extends number, H>(
spec: { aip: A; keyBy?: (h: H) => string },
handle: H,
version?: string,
): ArtifactRef<A>refFor derives a frozen ref from a spec + handle. The spec's keyBy
MUST match the family registry's. This is the ONLY sanctioned way to
construct a ref from a handle — hand-built refs skip the
unreferenceable-handle guard.
Rationale
aipas the discriminator, not a family name string or aschemaliteral. Real handles don't reliably carryschema(verified: the in-memory AIP-42AppHandlenever carries theschema: "app/v1"literal thatemitwrites intoAPP.md), but every handle has an owning AIP by construction, and the AIP number is already the registry's namespace coordinate.- Not extending AIP-18's
refKind.refKindtargets one named collection's items — collection-scoped by design. Widening it to arbitrary AIPs would break the "Hosts validate that ref values point at items of this collection" contract every AIP-18 host implements. - Version pin on the ref, not the target. The referenced artifact evolves; the ref records what was meant at authoring time. This is what AIP-55's pricing capability needed — inherited here for every future consumer.
- Adjacency. MCP, A2A, AGNTCY, and AITP own transport, discovery, identity, and transactions respectively — none own intra-ecosystem artifact addressing, which is this spec's entire scope. Extend, not fork: refs are deliberately the smallest contract, layering on the existing AIP-43 registry instead of introducing a second catalog.
Reference Implementation
@agentproto/ref
— pure TypeScript, zero runtime dependencies beyond the workspace,
fully unit-tested (12 tests covering resolution to object identity
across four AIP families, strict/lenient failure modes, keyless-handle
refusal, URI round-trip, malformed-URI rejection).
Backwards Compatibility
Supersedes AIP-27, which is now marked Superseded.
AIP-27 defined the resource half of this spec's job — the ws://
scheme, its collection→kind table, per-kind compact body grammars,
base collections, the matcher boundary, and the tenant-scope mandate —
as ### Reference syntax, alongside a separate artifact-shaped
discriminated union that no consumer adopted and whose resources:
directory never existed. The two were written four months apart and
never referenced each other; this AIP is the consolidation, not a
deliberate two-primitive design.
What moved into this AIP unchanged, normatively: the ws:// grammar,
the collection→kind table (all 21 rows), the per-kind compact body
grammars, the base collections, multi-segment bodies, the
tenant-scope mandate and its @<workspace> asymmetry, the matcher
boundary, and the seams. What did not move: AIP-27's runtime
registerRefKind extension hook — new collections now land as rows
in this spec's table via the AIP-1 process; per-kind implementation
registration remains an internal concern of @agentproto/ref.
AIP-27's eleven-kind defineRef surface remains in the
implementation package for AIP-7/AIP-10 consumers during migration;
this spec does not re-specify it.
Security Considerations
- A ref is an unvalidated pointer; hosts MUST NOT treat resolution as authorization. Capability checks remain with the resolved artifact's own authority model.
- The
aip://URI form is plain text and MUST NOT carry credentials or capabilities — it names, it does not entitle. - Strict-mode resolution failures SHOULD be surfaced to operators: a ref that silently stops resolving is the cross-AIP version of a dangling foreign key.
Resources
Supporting artifacts for AIP-54. Links open the file on GitHub — markdown and JSON render natively in GitHub's viewer. Browse the full resource tree →
AIP-53: APP.md — app/v1 (agent app bundle + UI surface)
A markdown + frontmatter format for bundling one or more AIP-42 agents with the AIP-15 workflows they run, plus an optional static UI surface, into one shippable unit — `.agentproto/APP.md` on disk, `.agentapp` as a packaged, checksummed distributable. Defines the emitted bundle layout, the `window.McpApp` bridge contract a UI surface consumes, the tools-allowlist enforcement a host applies to UI-originated calls, and the three runtime modes (host / bridge / standalone) a UI's client library resolves between.
AIP-55: PRODUCT — product/v1 (pricing capability attached via AIP-54 ref)
A pricing capability — a one-time / prepaid-pool / pay-per-call price plus a billing-rail projection config — attached to ANY AIP artifact through an AIP-54 ref/v1. The target AIP needs zero pricing awareness. Minor units are normative; the billing rail is projection config, never the source of truth. Stripe and Autumn projections are normative appendices.