rvm · capability-governed context

A URI that grants nothing

ruv:// names memories, resources, and skills across tenants and agents. Holding the name gets you exactly zero authority — the check happens somewhere else, and it happens first.

ruv://scheme, exact context.exampleauthority / acmetenant / agentsubject kind / researchersubject id / memorycollection / project-orionpath… ?view=abstractquery

One spelling per name. No percent-encoding, no fragments, no credentials, no port — so policy scopes, witness records, signatures, and caches can never disagree about whether two URIs are the same URI.

01 · Introduction

Names and rights are different things

Nearly every system you have used decides access the same way: you name a thing, the system works out who you are, and then it decides. Your authority is ambient — it hangs in the air around your identity and applies to everything you can name. Unix permissions work this way. So do database row filters, S3 bucket policies, and almost every RBAC layer ever shipped.

Ambient authority has a failure mode with a name, and it is nearly sixty years old: the confused deputy. A component that holds more authority than its caller gets talked into spending that authority on the caller's behalf. The deputy is not compromised and not buggy in any local sense — it simply cannot tell which of its powers the request was entitled to, because the request only carried a name, and names are free to write down.

This gets sharply worse with agents. An agent is a deputy by construction: it holds tools, credentials, and memory, and it takes instructions from text it just retrieved — text that may have been authored by someone else's user. If naming a resource is sufficient to reach it, then any string that reaches the model is a potential instruction to reach it.

Parsing a URI grants no rights. Resolving a skill does not execute it.

So ruv:// splits the two things that ambient systems fuse. The URI is inert — a pure name, canonical to a single spelling, useless on its own. The right to act on it arrives separately, as a capability handle that was deliberately handed over, can be narrowed on the way, and can be revoked. An untrusted request carries only that handle, an operation, and the canonical URI. It has no actor field and no timestamp, because those are precisely the fields a caller would want to forge; the actor is bound immutably when the runtime is constructed, and there is no setter.

The practical difference is that a prompt-injected agent that talks itself into naming another tenant's memory gets nothing. Not a filtered empty result — nothing, from a backend that was never consulted.

AMBIENT AUTHORITY CAPABILITY a name deputy decides using authority it already holds who you are any name it can reach, it can reach confused deputy lives here a name inert a handle handed over the handle carries the right narrowable · revocable · non-delegating naming it is not reaching it nothing to confuse the deputy with
The difference is one arrow. On the left, identity supplies the authority and the name selects the target — so a caller who can influence the name steers powers it was never given. On the right the name selects nothing on its own; the handle is the authority, and it only ever narrows.
Traditional approachIts failure modeWhat ruv:// does
Bearer token in the URL The URL is the credential — it leaks through logs, referrers, screenshots, support tickets Handles never appear in the URI or in ordinary logs
Check the ACL after resolving Existence leaks through timing, cache state, and errors that differ by cause Authorization precedes the resolver; four causes converge on one external shape
WHERE tenant_id = ? One forgotten clause is a breach, and every query still traverses everyone's data Separate physical index per exact scope — isolation is a different file, not a filter
Path-prefix multi-tenancy /project silently captures /project-archive Segment-wise matching; text prefixes are never used
Role-based access Authority follows identity, so it is hard to attenuate and delegation is all-or-nothing Attenuation is the default; no binding ever grants delegation rights
Soft-delete a row The bytes remain, and "deleted" is a flag anything can ignore Destroy the per-object key; the ciphertext becomes noise for everyone, including you

None of this is novel cryptography — capability systems date to the 1960s and the ideas here are older than most of the stacks they replace. What is new is the pressure: an agent that reads untrusted text and holds real tools is the confused deputy the literature warned about, running in production, at scale, by design.

ComponentAccepted valuesWhy it is closed
subject-kindagent · user · service · teamA closed set cannot be widened by a config edit
collectionmemory · resources · skillsThree kinds of thing, three retention stories
viewabstract · overview · contentDisclosure tiers, separately grantable
revsha256: + exactly 64 lowercase hexAn immutable citation to a byte stream
Queryabsent · rev · view · canonical rev&viewFour forms, so ordering can't fork the name

The limits are equally deliberate: 2,048 bytes for the whole URI, 253 for the authority, 63 per identifier, 128 per path segment, 32 segments, 1,024 bytes of path. A name that cannot be unbounded cannot be used to exhaust the thing that stores it.

02 · The mechanism

Authorization runs before the backend, not after it

This is the whole design in one arrow. The resolver and the vector index are never consulted on an unauthorized request — not consulted and filtered, not consulted at all. A denied request stops at the boundary and produces a witness record, and the storage layer never learns the name was asked for.

GRANTED REFUSED request authorize + witness scope resolver exact shard acme only request authorize + witness no call is made uniform error
Both lanes reach the same gate. On the refused lane the dashed line is the call that is never placed — the resolver and the shard are not asked, so nothing downstream can leak existence through timing, cache state, or an error that differs from any other error.

03 · Isolation

Physical shards, not a filtered index

The tempting way to build multi-tenant retrieval is one vector index with a tenant field, filtered after the search returns. That is a post-filter, and it means every query traverses a graph built from every tenant's data — latency, result quality, and memory pressure all become functions of objects the caller was never authorized to enumerate.

POST-FILTER EXACT SCOPE one graph, three tenants acme borl cyre traversal touches all three …then discard two acme searched borl untouched cyre untouched nothing to discard
Each exact scope gets its own physical index. Selection happens on structured namespace fields and path segments before any approximate-nearest-neighbour call, so a tenant's query cost never depends on another tenant's corpus.

A detail worth stealing regardless of whether you use rvm: the scope is matched on path segments, never on string prefix. …/project and …/project-archive are siblings, not parent and child, and no amount of clever naming makes one reachable from the other's grant.

04 · Disclosure

Three views, separately grantable

The same object can be exposed at three depths. These are capability-scoped representations, not interchangeable filenames — a grant can permit the abstract while denying the content, and an adapter must never silently broaden a grant to a larger representation.

abstract routing · relevance smallest disclosure overview structure · relations intermediate GRANT ENDS content full representation denied Search requires overview · Read requires content
When a view is derived, its provenance binds the source digest and the generation inputs — so changing the source, the generator, the model, the prompt, or the policy changes the derived commitment. A stale summary cannot masquerade as a current one.

05 · Silence

Four different failures, one indistinguishable answer

An error message is a side channel. If "forbidden" and "not found" differ, an unauthorized caller can enumerate your namespace by reading the difference. So four distinct internal outcomes converge on one external shape — unless the caller holds explicit audit authority, in which case they get the real reason.

INTERNAL TRUTH hidden forbidden missing tombstoned one external shape byte-identical audit authority sees the real reason
Convergence has to be byte-identical to be worth anything: a differing header, a differing length, or a measurably differing latency re-opens the channel the uniform body was meant to close.

06 · Practical applications

What this is actually for

Per-agent memory that can't bleed

Give each agent its own subject scope. Two agents on the same host, in the same process, searching the same collection name, never traverse each other's vectors — the isolation is a different file, not a filter clause.

ruv://context.example/acme/agent/researcher/memory

Team-shared skills, individually attenuated

Publish a skill once at team scope, then hand out grants that narrow it. A grant can permit resolve-and-read while denying execute, and no binding ever confers the right to delegate onward.

ruv://context.example/acme/team/platform/skills/web-search

Reproducible citations in agent output

When an agent cites what it read, it cites a pinned revision, not a mutable alias. The bytes under that revision are never overwritten, so a transcript stays checkable after the alias has moved on six times.

…/memory/project-orion?rev=sha256:2f9c…&view=content

Right-to-erasure that actually erases

Every object gets a fresh AES-256-GCM data key bound to its tenant and its pinned URI. Forgetting destroys the key rather than hunting down copies, and the durable purge outbox means a crash mid-erase resumes rather than silently stopping.

forget → key destroyed → ciphertext is noise

Cheap relevance triage

Route on ?view=abstract. A planner deciding which of forty memories is worth opening pays for forty abstracts, not forty full documents — and never holds authority for the content it decided against.

…/memory/project-orion?view=abstract

Audited tool surfaces

Expose the resolver through MCP or HTTP and every authorized call leaves a witness observation stamped by a runtime-owned clock. The audit trail is not something the caller can influence, because the caller never supplies the timestamp.

MCP · TLS HTTP · certificate-validating CLI

07 · Exotic

Uses the design permits but nobody asked for

Some of these are load-bearing ideas wearing an odd hat; a couple are genuinely strange. All of them fall out of properties the namespace already guarantees.

08 · Usage guide

Wiring it up without undoing it

Name something

Build the URI through the strict parser. A forgiving URL library will happily accept %2f, a trailing slash, an uppercase tenant, or a .. segment — all of which are different names that would resolve to the same object, which is precisely the ambiguity the canonical contract exists to prevent.

# canonical — the only accepted v1 spelling
ruv://context.example/acme/agent/researcher/memory/project-orion

# all rejected by RuvUri, none of them equivalent
ruv://context.example/ACME/agent/researcher/memory        InvalidTenant
ruv://context.example/acme/agent/researcher/memory/       trailing slash
ruv://context.example/acme/agent/researcher/memory/..     dot segment
ruv://context.example/acme/agent/researcher/memory//x     empty segment
ruv://user:pw@context.example/acme/…                      CredentialsNotAllowed
ruv://context.example:8443/acme/…                         PortNotAllowed

Grant something

Capabilities are declared out of band in the artifact's metadata, across fifteen classes. Two rules do most of the work, and both fail closed:

Capability classes
Memory · Filesystem · Network · Model · Mcpdata and reach
Process · Clock · Randomness · Gpuexecution and entropy
Sensor · Display · Audio · Clipboardthe human's machine
PersistentState · InterAgentMessagingdurability and reach between agents

Publish and advance

Bytes register under a pinned revision and are never overwritten. The alias is a separate, mutable pointer that advances only by compare-and-swap — so two writers racing produce one winner and one explicit failure, never a lost update. In a distributed alias store, that CAS has to be genuinely linearizable; a last-write-wins store silently reintroduces the race.

put     …/skills/web-search?rev=sha256:2f9c…   immutable, pinned required
advance …/skills/web-search  from=sha256:1a04…  CAS on the current revision
                                 to=sha256:2f9c…
 alias now points at 2f9c…; 1a04… still resolves forever

Before you expose it

  1. Keep capability handles out of the URI and out of ordinary logs.
  2. Parse with the strict parser, never a forgiving URL library.
  3. Call the resolver only through the authorization-and-witness path — never directly.
  4. Give each tenant a structured scope, not a text-prefix filter.
  5. Return uniform external errors for forbidden and hidden objects.
  6. Require pinned URIs for Put and Execute.
  7. Enforce the 4,096-byte query, 64-result and 16 MiB ceilings before allocation.
  8. Treat every retrieved byte as hostile input.
  9. Verify derived-view provenance before serving it.
  10. Drain receipts well before the witness ring can wrap.

The bundled local key manager requires an explicit development opt-in. Production embeds its own KMS and purge providers.

09 · Boundaries

What it deliberately is not

The reference crate establishes namespace, integrity, capability, and evidence boundaries. The hosted service adds encrypted persistence, cryptographic erasure, exact-scope indexes, durable outboxes, and a transactional receipt drainer. Everything below is outside that line, and saying so is part of the design:

  • Not a transport. ruv:// does not define how bytes move between hosts.
  • Not a filesystem mount, and not an ambient authority system.
  • Not semantic truth. A signed artifact is not automatically safe content.
  • Not prompt-injection defense. That is why retrieved context is treated as hostile.
  • Not federation. Network federation and deployment-specific KMS and replica adapters stay outside the generic implementation.
  • Not a database. The in-memory reference resolver is bounded local state, not durable or distributed storage.

A capability system that overclaims is worse than one that underclaims, because the overclaim is what gets quoted in the design review six months later.