DMI · Operator pack
Reach a line correctly.
Everything an agent operator needs to reach a Qlaas DMI line with no guesswork: discovery, authentication, the five skills over MCP and A2A, receipts and the conduct the verified lane expects. Hosted lines will live at dmi.qlaas.co.uk/@handle; that host is not live yet, so test against a self-hosted line until it is.
Quick start.
A line is one origin. Read its manifest, pick the top-ranked execution path, authenticate, call a skill, keep the receipt. The simulator in the qlaas-dmi repository does the whole sequence against any line and prints each step.
Ten lines
# 1. Read the line (one origin per line; the handle page is dmi.qlaas.co.uk/@demo)
curl https://dmi.qlaas.co.uk/.well-known/dmi.json | jq '.executionPaths[0], .capabilities[].id'
# 2. Stand in for your agent: discovery → registration → owner consent → MCP
# (from a checkout of UIE-founder/qlaas-dmi; the package is not on npm)
npx qlaas-dmi agent --line https://dmi.qlaas.co.uk --as "Your Assistant" --for "Your principal"
# 3. Or point any MCP client at the endpoint; it registers itself (RFC 7591)
# and the owner approves in their app
MCP_URL=https://dmi.qlaas.co.uk/agentline/mcpThe agent command takes --line <origin>, --as <agent name>, --for <principal> and, for your own line only, --owner-token. It is a stand-in for Muse, Instinct or any MCP client. Nothing here charges anything.
From claude.ai
Add the line as a custom connector: open Connectors, choose Add custom connector, paste https://dmi.qlaas.co.uk/agentline/mcp and add it. Claude registers itself with the line (RFC 7591), the line sends you to its consent page, and the line owner approves on the device where their app is open. Custom connectors are available on paid claude.ai plans; see Anthropic's support article. The same steps work for any MCP client that implements the OAuth 2.1 authorization-code flow with PKCE.
Discovery.
Three documents, all public, all unauthenticated, all cached for 30 seconds with an ETag. Fetch them with If-None-Match when you poll. Public reads are limited to 300 per minute per IP; a 429 carries Retry-After.
/.well-known/dmi.json- The signed manifest: resource, state, capabilities with JSON Schema contracts and live authority, ranked execution paths, the upgrade field, the receipt key and discovery pointers. Profile
worldauth.dmi.communications/0.1. /.well-known/agent-card.json- An A2A 1.0 agent card: the JSON-RPC interface, the OAuth security scheme, the skills, and an extension block with the MCP URL, the receipt key and the signed-request instructions. Signed with the same key (detached Ed25519 over the canonical card).
/.well-known/playbakk.json- The line's federation descriptor: handle, name, door, agent card, identity key. Lines use it to verify each other.
/.well-known/playbakk/receipt-key.json- A JWKS with the line's Ed25519 public key (
kid= thekeyIdon every signature). The path keeps the repository's name; there is no Qlaas-named alias yet.
Manifest, abridged
{
"dmi": "worldauth.dmi.communications/0.1",
"generatedAt": "2026-10-01T09:14:32.000Z",
"resource": {
"kind": "person.line",
"principal": { "name": "Demo", "handle": "demo" },
"line": "https://dmi.qlaas.co.uk",
"identity": "https://dmi.qlaas.co.uk/.well-known/playbakk.json",
"agentCard": "https://dmi.qlaas.co.uk/.well-known/agent-card.json",
"addresses": { "door": "https://dmi.qlaas.co.uk/@demo", "federated": "demo@dmi.qlaas.co.uk" }
},
"state": { "asOf": "…", "evidence": "declared", "preferredForMachines": "agentline" },
"capabilities": [
{ "id": "leave_message", "contract": { "input": { "type": "object", "required": ["text"], "…": "…" } },
"authority": { "default": "ALLOW", "rules": [{ "effect": "ALLOW" }] }, "paths": ["mcp", "a2a"], "evidence": "receipt" },
{ "id": "propose_meeting",
"authority": { "default": "STEP_UP", "rules": [
{ "effect": "ALLOW", "when": { "maxMinutes": 30, "tiers": ["verified"] } }, { "effect": "STEP_UP" } ] } },
{ "id": "urgent_patch_through", "authority": { "default": "STEP_UP", "…": "…" } },
{ "id": "voice_call", "paths": ["webrtc", "websocket-media"], "for": "people" }
],
"executionPaths": [
{ "rank": 1, "id": "mcp", "url": "https://dmi.qlaas.co.uk/agentline/mcp", "for": "machines" },
{ "rank": 2, "id": "a2a", "url": "https://dmi.qlaas.co.uk/agentline/a2a", "for": "machines" },
{ "rank": 3, "id": "webrtc", "for": "people" }, { "rank": 4, "id": "websocket-media", "for": "people" },
{ "rank": 5, "id": "pstn", "note": "only when a phone line is attached" }
],
"upgrade": { "field": "metadata.upgradeCode" },
"evidence": { "receipts": { "alg": "EdDSA", "canonicalization": "JCS", "chained": true,
"keyId": "pbk-…", "key": { "kty": "OKP", "crv": "Ed25519", "x": "…" },
"jwks": "https://dmi.qlaas.co.uk/.well-known/playbakk/receipt-key.json" } },
"signatures": [{ "keyId": "pbk-…", "alg": "EdDSA", "canonicalization": "JCS", "signature": "…" }]
}Execution paths
Paths are ranked shortest first and marked for: machines or for: people. Take rank 1 (MCP) unless your runtime only speaks A2A; never take a people path with an agent. The cdo figures (hops, model invocations, human interventions) are declared design estimates, flagged evidence: declared, not measurements. The pstn path appears only when a phone line is attached, and no hosted line has one today.
Authority in the manifest
Each capability carries authority.default and the owner's rules, derived live from their Mandate. The default is the effect of the last matching rule, so a rule list of ALLOW when verified ≤ 30 min then STEP_UP reads as default STEP_UP with a faster path for verified callers. If state.evidence is observed the owner opted in to coarse presence (acceptingCalls, ownerReachableNow, quietUntil); without it you only get declared state. Treat quietUntil as a reason not to send urgent_patch_through.
Verifying the signature
Verify the manifest
import { createPublicKey, verify } from "node:crypto";
const doc = await (await fetch("https://dmi.qlaas.co.uk/.well-known/dmi.json")).json();
const { signatures, ...unsigned } = doc;
const { keys } = await (await fetch(doc.evidence.receipts.jwks)).json();
const jwk = keys.find(k => k.kid === signatures[0].keyId);
const ok = verify(null, Buffer.from(jcs(unsigned)), createPublicKey({ key: jwk, format: "jwk" }),
Buffer.from(signatures[0].signature, "base64url"));
// jcs(): RFC 8785 canonical JSON — sorted keys, no whitespace, undefined dropped.The same procedure verifies the agent card. The manifest's signatures array is removed, the rest is canonicalised (RFC 8785, as the repository implements it: sorted keys, undefined dropped, no whitespace) and checked against the key from the JWKS.
DNS discovery
Someone who owns a domain can publish their line on it. Resolve name@example.com by reading one TXT record; the handle in the record wins over the local part of the address. The record name is still _playbakk in the code; a _dmi alias is planned but not implemented.
TXT record
_playbakk.example.com. TXT "v=pbk1; line=https://dmi.qlaas.co.uk; handle=demo"
# resolves demo@example.com → https://dmi.qlaas.co.uk (handle demo). The line origin is the
# unit of discovery: /.well-known/dmi.json lives on the origin, not under /@demo.Authentication.
The line distinguishes three tiers. Declared: you said who you are in metadata and nothing checked it. Verified: the owner approved you through OAuth, or you signed the request with a key the owner trusts. Only verified callers reach the faster rules in a Mandate (for example auto-accepted short meetings). By default anonymous callers are accepted at tier declared; a line started with QLAAS_AGENTLINE_REQUIRE_AUTH=1 refuses them with 401 and a WWW-Authenticate: Bearer resource_metadata=… header, which is how you discover the authorization server.
OAuth 2.1, as implemented
Flow
GET https://dmi.qlaas.co.uk/.well-known/oauth-authorization-server
→ { issuer, authorization_endpoint: "/oauth/authorize", token_endpoint: "/oauth/token",
registration_endpoint: "/oauth/register", revocation_endpoint: "/oauth/revoke",
code_challenge_methods_supported: ["S256"], token_endpoint_auth_methods_supported: ["none"],
grant_types_supported: ["authorization_code","refresh_token"], scopes_supported: ["agentline"] }
GET https://dmi.qlaas.co.uk/.well-known/oauth-protected-resource (also …/oauth-protected-resource/agentline/mcp)
→ { resource: "https://dmi.qlaas.co.uk/agentline/mcp", authorization_servers: ["https://dmi.qlaas.co.uk"], bearer_methods_supported: ["header"] }
POST https://dmi.qlaas.co.uk/oauth/register (JSON body; RFC 7591)
{ "client_name": "Your Assistant", "redirect_uris": ["https://agent.example/oauth/callback"] }
→ 201 { client_id: "pbk_…", token_endpoint_auth_method: "none", … }
GET https://dmi.qlaas.co.uk/oauth/authorize?response_type=code&client_id=pbk_…&redirect_uri=…
&code_challenge=<S256>&code_challenge_method=S256&state=…&scope=agentline[&resource=https://dmi.qlaas.co.uk]
→ 302 /consent?req=… the OWNER approves in their app; you are redirected back with code, state, iss
POST https://dmi.qlaas.co.uk/oauth/token (form or JSON)
grant_type=authorization_code&code=…&client_id=pbk_…&redirect_uri=…&code_verifier=…
→ { access_token, token_type: "Bearer", expires_in: 3600, refresh_token, scope: "agentline" }
Authorization: Bearer <access_token> on /agentline/mcp and /agentline/a2aPKCE S256 only; plain is refused. Public clients only (no client secret). Redirect URIs must be https, or http on localhost, with no fragment, at most five. Authorization codes last 60 seconds and are single use, even on a failed exchange. Access tokens last one hour; refresh tokens 30 days and rotate — replaying a spent refresh token revokes the whole grant family. Only the scope agentline exists.
Resource binding (RFC 8707). Pass resource on the authorize and token requests if your client sends it. The line checks the token's audience against the exact endpoint URL and the line origin. A token bound to …/agentline/mcp is valid only on that endpoint; bind to the origin (https://dmi.qlaas.co.uk) if you intend to use both MCP and A2A with one grant, or leave resource out.
Consent. The authorize endpoint parks your request and sends the browser to /consent, where the owner sees your client_name, the redirect host and the scope, and approves or declines in their app. Your redirect receives code, state and iss (RFC 9207), or error=access_denied. The owner can disconnect your client at any time; every token it holds stops working at once. Revoke your own tokens at /oauth/revoke with token=….
Identity on the wire. An OAuth caller is identified by its registered client_name (as vendor and label) and the key id oauth:<client_id> on receipts. You may still declare onBehalfOf per request; it is the only metadata field honoured for authorised callers.
Signed requests for verified operators
Operators on the verified lane may skip OAuth: the owner registers your Ed25519 public key on their line under a key id and vendor name (see the verification process), and you sign each request.
Headers
Signature-Agent: <keyId the owner registered for you>
Signature-Created: <unix seconds; accepted within ±300 s>
Signature: base64url( Ed25519( "${Signature-Created}.${rawBody}" ) )
# Each signature is accepted once. A forged or stale signature falls back to tier "declared"
# and never inherits your key id. This is the repository's Web Bot Auth-style scheme, not RFC 9421.Skills.
Five skills, the same over MCP (tools/call) and A2A (message/send). Arguments are validated strictly before any authority decision; an invalid argument is a DENY with the reason, never a 400. The schemas are published in the manifest, the agent card and tools/list. A sixth skill, share_contact, exists only to be denied: the line never discloses the owner's contact details.
leave_message- Arguments text (≤2000) · from, onBehalfOf, callback (≤80)
Default mandate ALLOW
Result{ delivered: true, ref } request_callback- Arguments reason (≤500) · urgency low|normal|high · callback, onBehalfOf
Default mandate ALLOW
Result{ delivered: true, ref } check_availability- Arguments durationMin 5–240 · days 1–14
Default mandate ALLOW
Result{ slots: [{start,end}], tz: "UTC", disclosure: "free/busy only" } propose_meeting- Arguments start (ISO date-time) · durationMin 5–240 · topic (≤200) · onBehalfOf
Default mandate ALLOW if verified, ≤30 min and free; otherwise STEP_UP
Result{ confirmed: true, start, durationMin } urgent_patch_through- Arguments reason (≤500) · onBehalfOf, callback
Default mandate STEP_UP
Result{ ringing: true, note }
Allow, Step up, Deny
ALLOW executes at once; the task is completed and the result is in the artifact (A2A) or structuredContent.result (MCP). STEP_UP creates an approval in the owner's app and returns a working task, shown as input-required on A2A; poll tasks/get on the A2A endpoint until it becomes completed or rejected. The step-up happens on the owner's device and never inside your conversation: do not ask the owner for approval through text, and do not retry a pending task. DENY returns a rejected task with the last reason in the status text and isError: true on MCP. A denial is final for that request; a denial for rate or pending limits says so.
Limits that produce a DENY: five messages (leave_message, request_callback) per declared caller per ten minutes, thirty when verified; at most three unverified urgent_patch_through requests waiting for the owner at once; an expired Mandate; propose_meeting outside free time steps up even for verified callers. Both endpoints are also limited to 60 requests per minute per IP (JSON-RPC error -32029).
MCP
Streamable HTTP, single JSON response per request, protocol version 2025-06-18. Methods: initialize, ping, tools/list, tools/call; notifications get 202. Declare yourself in params._meta.agent (vendor, name, onBehalfOf); after a voice call, put the code you heard in params._meta.upgradeCode.
tools/call
POST https://dmi.qlaas.co.uk/agentline/mcp
{ "jsonrpc": "2.0", "id": 3, "method": "tools/call",
"params": { "name": "propose_meeting",
"arguments": { "start": "2026-10-06T10:00:00Z", "durationMin": 20, "topic": "Collab intro", "onBehalfOf": "Jordan Lee" },
"_meta": { "agent": { "vendor": "your-vendor", "name": "Your Assistant", "onBehalfOf": "Jordan Lee" },
"upgradeCode": "482913" } } }
→ { "jsonrpc": "2.0", "id": 3, "result": {
"content": [{ "type": "text", "text": "Awaiting Demo's approval (task task_…)." }],
"structuredContent": { "taskId": "task_…", "state": "working", "decision": "STEP_UP",
"result": null, "receipts": ["rcpt_…"] },
"isError": false } }check_availability is annotated readOnlyHint; everything else writes. openWorldHint is false on all tools.
A2A
JSON-RPC 2.0 at /agentline/a2a. Methods: message/send (alias SendMessage) and tasks/get (alias GetTask). Send a data part { skill, args }. A message with only a text part becomes leave_message: free text from another agent is treated as a message to the owner, never as an instruction to the line. Declare yourself in message.metadata.agent; the upgrade code goes in message.metadata.upgradeCode. Streaming and push notifications are not supported.
message/send and tasks/get
POST https://dmi.qlaas.co.uk/agentline/a2a
{ "jsonrpc": "2.0", "id": 1, "method": "message/send",
"params": { "message": { "role": "user", "messageId": "m1", "contextId": "ctx_…",
"parts": [{ "kind": "data", "data": { "skill": "request_callback",
"args": { "reason": "Delivery window for the new mic", "urgency": "normal" } } }],
"metadata": { "agent": { "vendor": "your-vendor", "name": "Your Assistant", "onBehalfOf": "Jordan Lee" } } } } }
→ { "jsonrpc": "2.0", "id": 1, "result": {
"kind": "task", "id": "task_…", "contextId": "ctx_…",
"status": { "state": "completed", "message": { "role": "agent", "parts": [{ "kind": "text", "text": "Done." }] } },
"artifacts": [{ "artifactId": "task_…-result", "parts": [{ "kind": "data", "data": { "delivered": true, "ref": "msg_…" } }] }],
"metadata": { "decision": "ALLOW", "receipts": ["rcpt_…"] } } }
POST https://dmi.qlaas.co.uk/agentline/a2a { "jsonrpc": "2.0", "id": 2, "method": "tasks/get", "params": { "id": "task_…" } }Upgrading from voice
If your agent reached a line by phone and identified itself as an AI (or pressed *), the front desk read out the AgentLine address and a six-digit code, one digit at a time. Present it as upgradeCode on your first structured request: the task is bound to the call in progress and the receipt carries the callId. A code binds once, only on a request that was not denied, and the line locks code guessing after twenty failures in ten minutes. Voice is the fallback; the line itself steers machines off it.
Example request and response files, matching the repository's test fixtures: leave_message, request_callback, check_availability, propose_meeting, urgent_patch_through.
Receipts.
Every decision writes a receipt before anything executes, and a step-up writes a second one when the owner decides. Receipt ids come back on every task (metadata.receipts on A2A, structuredContent.receipts on MCP). The id is an unguessable capability: whoever holds it can fetch and verify the receipt, nobody can enumerate them.
Public verifier
GET https://dmi.qlaas.co.uk/receipts/rcpt_Xk2m9pQ7vL1a
{
"receipt": {
"payload": {
"v": "playbakk.receipt/0.1", "id": "rcpt_Xk2m9pQ7vL1a", "at": "2026-10-01T09:14:32.000Z",
"prev": "<sha256(JCS(previous signed receipt)), base64url> | null",
"actor": { "kind": "agent", "id": "playbakk-agent" },
"counterparty": { "id": "oauth:pbk_…", "kind": "agent", "tier": "verified" },
"action": "agentline.propose_meeting", "decision": "STEP_UP",
"subject": { "taskId": "task_…", "args": { "start": "…", "durationMin": 20, "topic": "Collab intro" } },
"reasons": ["STEP_UP by mandate-default for propose_meeting", "outside your free time → needs your approval"]
},
"signature": "<base64url Ed25519 over JCS(payload)>", "keyId": "pbk-…"
},
"hash": "<sha256(JCS(receipt)), base64url>", "verified": true, "keyId": "pbk-…",
"jwks": "https://dmi.qlaas.co.uk/.well-known/playbakk/receipt-key.json", "line": "https://dmi.qlaas.co.uk/.well-known/playbakk.json"
}The line re-verifies the signature on every fetch and reports it as verified. Verify it yourself too: fetch the JWKS, check the Ed25519 signature over JCS(payload), then check that sha256(JCS(receipt)) equals hash. The chain is continuous across restarts; the owner's line checks it on boot and exposes the result to the owner.
Hash chain
prev is the SHA-256 (base64url) of the canonical JSON of the whole previous signed receipt (payload, signature, keyId), or null for the first. Changing one receipt breaks every later link; the owner exports the chain as JSONL or CSV from their line and can prove its integrity to a third party without you.
What to keep
- Always
- The receipt id, the task id, the full receipt JSON as returned by the verifier, the JWKS document at the time, and your own request body. Together these prove what you asked, what the line decided, by which rule, and when.
- EU AI Act, Article 50
- Your disclosure obligation is yours. Keep evidence that your agent identified itself as an AI and named its principal: the
agentmetadata you sent (it is echoed in the receipt'scounterpartyandsubject.args.onBehalfOf), and for voice calls the call id bound by the upgrade code. The line's own disclosure to voice callers is stated inevidence.disclosure. - TCPA and PECR
- A completed
urgent_patch_throughis the owner's express consent to be rung, recorded as aSTEP_UPreceipt from the agent followed by anALLOWreceipt withactor.kind: ownerand reasonowner approved on device. Keep both. ADENYorowner declined on deviceis a do-not-contact signal for that request.
Conduct.
The verified lane is for operators the owner can trust by default. Keeping it depends on four things.
- Disclose. Send
agent.vendor,agent.nameandagent.onBehalfOfon every request, and say the same on a voice call before anything else. A principal is a person or organisation, named;onBehalfOfis capped at 80 characters and shown to the owner verbatim. - Honour DENY. A rejected task is the owner's answer. Do not resend the same request, rephrase it, switch skill to get around it, or fall back to the phone number. A line that is
quietUntilis not to be rung. - Stay inside the rate classes. The line enforces 60 requests per minute per IP, 5 or 30 messages per caller per ten minutes, and three pending urgent requests for declared callers. Fair use on the verified lane is 20,000 interactions per operator per month across all lines; the line meters this through its receipt count and does not cut you off, but we review it.
- Follow the ranking. Machines take MCP or A2A. Use the door or the phone only for a human principal who is present, and never to bypass a structured answer.
A lane is suspended for undisclosed agents, ignored denials, repeated urgent requests without a real reason, requests that attempt to extract contact details or calendar contents, replayed or forged signatures, or sustained traffic above the fair-use allowance. Suspension removes your key from the lines that trust it and your OAuth clients from lines that connected you; you are told why and how to appeal. The process is set out in the operator verification document. The first 10 operators get the lane free for 90 days; pricing after that is not yet published.
Reference.
- OpenAPI 3.1
- /dmi/openapi.yaml: the HTTP surface above, derived from the repository's
src/server.tsandsrc/agentline/*. - Examples
- One JSON file per skill under /dmi/examples/, each with the MCP and A2A request and the response the tests expect.
- Verification
- docs/dmi/OPERATOR_VERIFICATION.md: what you submit, what we check, how keys are registered, suspension and the best-efforts service language.
- Source
- The line is the
UIE-founder/qlaas-dmirepository (private until the self-host story goes public). Where this page and the code disagree, the code is right; tell us and we will fix the page. - Status
dmi.qlaas.co.ukis not provisioned. Values on this page describe what a hosted line will serve; test against a self-hosted line (npx qlaas-dmi serve) until the host is live.