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/mcp

The 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 = the keyId on 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/a2a

PKCE 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 agent metadata you sent (it is echoed in the receipt's counterparty and subject.args.onBehalfOf), and for voice calls the call id bound by the upgrade code. The line's own disclosure to voice callers is stated in evidence.disclosure.
TCPA and PECR
A completed urgent_patch_through is the owner's express consent to be rung, recorded as a STEP_UP receipt from the agent followed by an ALLOW receipt with actor.kind: owner and reason owner approved on device. Keep both. A DENY or owner declined on device is 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.

  1. Disclose. Send agent.vendor, agent.name and agent.onBehalfOf on every request, and say the same on a voice call before anything else. A principal is a person or organisation, named; onBehalfOf is capped at 80 characters and shown to the owner verbatim.
  2. 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 quietUntil is not to be rung.
  3. 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.
  4. 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.ts and src/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-dmi repository (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.uk is 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.