@agenticprimitives/agent-resolution
v0.0.0-alpha.2
Published
Provider-neutral agent resolution primitives: agent discovery policy, signed sequenced AgentServicePublication with anti-rollback verification, private resolution grants, connection invitations, audience projection, and resolution provider ports. Resoluti
Downloads
256
Maintainers
Readme
@agenticprimitives/agent-resolution
Every agent has a canonical identity. Naming is optional. Public listing is optional. Inbound addressability is optional. Authorized private resolution is a first-class capability.
Provider-neutral primitives for answering "may this party discover how to reach this agent, and where is it right now?" — with a hard wall between that question and "may this party use it?"
Spec: [specs/338](https://github.com/agentictrustlabs/agenticprimitives/blob/master/specs/338-private-agent-resolution-and-service-publication.md) ·
Doctrine: ADR-0056
The rule
A resolution grant answers: "May this party DISCOVER how to reach this agent?"
It NEVER answers: "May this party USE this agent?"Successful resolution gives you an endpoint and a verified peer identity. Invocation still requires the whole chain, re-run at the origin:
delegation ∩ entitlement ∩ tool/action policy ∩ exact invocation proof ∩ revocation ∩ auditResolution is one of four orthogonal concerns with custody ("who signed"), admission ("may these bytes enter"), and authority ("what may this principal do"). A yes to one is never a yes to another.
Four independent dimensions
"Nameless" = no public label only. Listing, resolution, and inbound are modelled separately.
| Posture | naming | listing | resolution | inbound |
| ------------------------------ | --------------- | -------- | --------------- | --------------------- |
| Public commercial agent | public | public | public | delegation-required |
| Public name, protected details | public | public | authenticated | delegation-required |
| Private partner agent | private-label | none | invitation | known-peer |
| Pairwise treasury agent | private-label | none | pairwise | delegation-required |
| Unnamed internal worker | none | none | invitation | known-peer |
| Temporary direct agent | none | none | direct | delegation-required |
| Background outbound worker | none | none | none | outbound-only |
import { discoveryPolicyFromPreset, validateDiscoveryPolicy } from '@agenticprimitives/agent-resolution';
const policy = discoveryPolicyFromPreset('private-partner');
validateDiscoveryPolicy(policy); // [] — combinations that cannot be enforced are rejectedEvery path resolves to the same canonical identity: eip155:<chainId>:<SmartAgentAddress>.
Worked flow — Agent A reaches a nameless treasury (Agent B)
Scenario: Agent A (caller SA for a person/org) must call Agent B (treasury service SA) over
A2A. B has no public ANS name (naming: none, listing: none). Reachability is invitation /
private-grant resolution — not a public label.
Where the A2A URL comes from: it is not invented by the private resolver and not looked
up by name. It is a field on a signed AgentServicePublicationV1:
publication.surfaces[] → entry with protocol: 'a2a' → uri (e.g. https://xyz.example/a2a/v1).
The resolver only admits Alice’s grant and selects a pre-signed publication from B’s owner
vault (selectPresignedPublication — select, never edit). Alice extracts the URI after
verifyPublication on her side.
Diagrams
1. Conceptual flow — nameless Agent A → treasury Agent B (terms used everywhere below):

Agent A → nameless treasury B. Vault holds signed publication (A2A uri in surfaces[]),
subject-bound grant, publication authority. Steps: issue grant → invitation
(initialPublication + rendezvous) → first dial or resolver refresh
(selectPresignedPublication) → A2A + delegation. Resolver never forges the endpoint.
2. Local architecture — same terms → monorepo Workers:

demo-web-pro / demo-sso-next = Agent A; demo-resolver :8795 = private resolver;
demo-publications :8796 = pub store; demo-a2a :8787 = B’s A2A host (URI in publication).
Optional demo-edge. Steps match diagram 1. Live treasury may still use a known URL + delegation;
private resolve is the parallel Ring-1 path.
| Step | What moves | A2A endpoint? |
| --- | --- | --- |
| 1 | B issues PrivateResolutionGrantV1 (subject-bound to A) | no — grant = may discover |
| 2 | Invitation to A = initialPublication + grant + rendezvous | yes — URI already inside initialPublication.surfaces[] |
| 3a | First connect: A verifies that publication locally | URI from verified publication (no resolver hop) |
| 3b | Refresh: A → resolver → selectPresignedPublication → A verifies | yes — URI from the selected publication’s surfaces[], not from resolver logic |
| 4 | A2A task + delegation ∩ entitlement ∩ policy | use, not resolve |
Nameless = no public label. Publication authority = endpoint/session-key rotation is an ops publish, not a custody ceremony. Delegation = what lets A invoke treasury skills after the URL is known.
sequenceDiagram
autonumber
participant B as Agent B treasury SA
participant Vault as B owner vault
participant R as Private resolver
participant A as Agent A
participant A2A as B A2A host<br/>xyz…/a2a/v1
B->>Vault: Store signed AgentServicePublicationV1<br/>surfaces: [{protocol:a2a, uri}]
B->>Vault: Issue PrivateResolutionGrantV1 subject=A
B-->>A: Invitation = initialPublication + grant + rendezvous
Note over A: Resolve — endpoint is in the publication
A->>A: verifyPublication(initialPublication)
A->>A: a2aUrl = surfaces.find(p=>p.protocol==='a2a').uri
opt Endpoint moved — same grant
A->>R: Present grant + PoP
R->>Vault: Admit + selectPresignedPublication
R-->>A: Pre-signed publication (includes new uri)
A->>A: verifyPublication → read surfaces[].uri again
end
A->>A2A: Open channel to a2aUrl
A->>A2A: Task + delegation (other packages)
alt Authority OK
A2A-->>A: Result
else Resolution grant only
A2A-->>A: Denied — resolution ≠ use
endLocal apps — term → Worker map
| Conceptual term | Local app |
| --- | --- |
| Agent A app | demo-web-pro; invite inspect demo-sso-next /visibility |
| Agent B A2A host | demo-a2a :8787 — /api/a2a, A2aTaskDO |
| Private resolver | demo-resolver :8795 — /v1/private/{challenge,resolve} |
| B vault (demo stand-in) | demo-publications :8796 — /v1/publications |
| A2A uri | Inside publication surfaces[] → e.g. http://127.0.0.1:8787/api/a2a |
| Optional edge | demo-edge — /api/a2a/<handle> |
| Use (not resolve) | Delegation gates in demo-a2a |
Worked sequence — DNTLS as a Ring-1 resolution provider
DNTLS is not a dependency. A Ring-1 AgentResolutionProvider (method: 'dntls') maps a Service
Record → AgentServicePublicationV1; createComposedResolver verifies SA binding (spec 338 §18).
Namespace membership may be a policy predicate, never the grant. Bidirectional binding
required. DNTLS alone never grants identity, delegation, MCP/A2A permission, entitlement, or custody.
sequenceDiagram
autonumber
participant D as DNTLS resolver<br/>(external)
participant Adapt as dntls provider<br/>(Ring-1)
participant AP as createComposedResolver
actor A as Agent A
participant A2A as B A2A host
A->>AP: resolve({ kind:'external', method:'dntls', identifier })
AP->>Adapt: resolve(reference, context)
Adapt->>D: Resolve name / commitment
D-->>Adapt: Service Record + proof
Adapt->>Adapt: Map → AgentServicePublicationV1
Adapt-->>AP: UnverifiedResolutionBundleV1
AP->>AP: verifyPublication + SA↔DNTLS binding<br/>+ authorityCheck + continuity
alt Fail
AP-->>A: resolution failure
else OK
AP-->>A: VerifiedAgentResolutionV1<br/>(reachability only)
end
A->>A2A: Open surface URI + AP delegation
A2A->>A2A: delegation ∩ entitlement ∩ policy
alt Authority OK
A2A-->>A: Result + receipt
else Resolution-only
A2A-->>A: Denied — resolution ≠ use
endIllustrative adapter (ports are real; DNTLS client/wire is external and not shipped):
import {
createComposedResolver,
type AgentResolutionProvider,
type AgentServicePublicationV1,
} from '@agenticprimitives/agent-resolution';
// Ring-1: map DNTLS Service Record → AgentServicePublicationV1
// (agentId, channelId, sequence, surfaces[].uri/protocol, transportKeyBinding,
// publishedBy, publicationAuthorityRef, digest, proofs[] — then ERC-1271 sign)
declare function mapDntlsRecord(record: unknown): AgentServicePublicationV1;
declare const externalDntlsClient: { resolve(id: string): Promise<{ record: unknown; proof: unknown }> };
const dntlsProvider: AgentResolutionProvider = {
method: 'dntls',
supports: (ref) => ref.kind === 'external' && ref.method === 'dntls',
async resolve(ref, _ctx) {
if (ref.kind !== 'external') throw new Error('unsupported');
const { record, proof } = await externalDntlsClient.resolve(ref.identifier);
return {
method: 'dntls',
referenceKind: 'external',
publication: mapDntlsRecord(record),
providerEvidence: [proof],
servedAt: new Date().toISOString(),
};
},
};
const resolve = createComposedResolver({
providers: [dntlsProvider],
verifier: publicClient,
// authorityCheck should also require AP facet → DNTLS commitment (bidirectional)
authorityCheck,
minAssurance: 'signed-publication',
});
const result = await resolve(
{ kind: 'external', method: 'dntls', identifier: 'treasury.ops.acme.dntls' },
{ requester: 'eip155:84532:0xaaa…', now: new Date().toISOString() },
'eip155:84532:0xbbb…',
);
// result.ok ⇒ VerifiedAgentResolutionV1 — still no spend / tool permissionFull audit: dntls-comparison.md.
A hash proves unmodified. It does not prove current.
A signed card proves nobody tampered with a snapshot — not that it is still in force. So every
publication carries a monotonic sequence, previousDigest, status, and expiresAt.
Verification is fail-closed (rollback, equivocation, expiry, unauthorized publisher, missing
endpoint-control proof, out-of-window transport keys):
import { verifyPublication } from '@agenticprimitives/agent-resolution';
const result = await verifyPublication(publication, {
expectedAgentId: 'eip155:84532:0xacme…',
now: new Date().toISOString(),
verifier: publicClient, // any { verifyHash } — a viem PublicClient fits
authorityCheck, // "was this publisher allowed to publish for this agent?"
known: continuity, // what we already accepted on this channel
requireEndpointProof: true,
maxExposure: 'private',
});
if (!result.ok) {
// Precise codes for YOUR logs; result.failures[].wireCode for anything you return to a caller.
if (result.freshness.conflict) escalate('publisher equivocation — possible key compromise');
}Equivocation — two validly signed publications at the same sequence — is reported, not merely rejected. It is evidence of publisher-key compromise, not a transient error.
Rotation is cheap; identity never moves
Endpoint change, transport-key rotation, regional failover, protocol bump, and provider migration
all republish at sequence + 1. No grant is reissued and the canonical agent id does not change.
Private resolution grants, not private names
Salted private names are rejected (low-entropy secret, name becomes load-bearing). Grants are
opaque high-entropy capabilities (apd1_…), recipient-bound:
| Mode | Use |
| --- | --- |
| subject-bound (default) | Presenter proves subject SA possession — a copy is useless to others |
| pairwise | Distinct grant/projection/history per relationship |
| bearer-bootstrap | Short-lived only until the recipient has an SA |
const admission = await admitGrantPresentation(presentation, deps, {
resolverAudience: 'https://resolve.acme.example/v1/private',
});
admission.projectionProfile; // 'partner' — what may be DISCLOSED, never invokedWire denials all use resolution_denied (no enumeration oracle); precise codes stay local.
Bootstrap, resolver, storage
Invitation = initial publication + grant + rendezvous + expected bindings (no re-invite on endpoint
rotation). Org rendezvous reveals only that a private resolution service exists — replaceable, not
the publication authority, cannot forge target-signed pubs or grant use. findOverDisclosure
catches over-disclosure.
Owner vault (authoritative): pubs, grants, revocation, aliases. Resolver vault (ops only): keyed grant index, nonces, budgets — never the sole copy of anything that matters.
const digest = await deriveGrantDigest(grantId, resolverIndexSecret); // HMAC; 32-byte secretScope
Owns: discovery policy · publication verify · freshness · grants · invitations · projection ·
errors · provider ports. Not: transport, storage, clocks, authZ, naming, directory, search,
profile, surface-catalog. Ring-0 (types only); ANS / rendezvous / DNTLS / DID are Ring-1.
Install
pnpm add @agenticprimitives/agent-resolutionLicense
MIT
