馃Б CAP 1.0 Draft

Add login to your agent

Your agent already answers public questions. This guide adds the rest: a person logs in, their agent asks about their own account, and, if you allow it, your agent makes changes for them. Actions that are hard to undo wait for the person's OK on your page.

If your company already has user accounts, plan on an afternoon with the reference server, and a day or two to put your own sign-in in front of it. Most of the work is one function that checks a login and one that answers with an account.

The visiting agent never gets your keys, your tools, or the person's password. It gets a token for one account, bound to its own CAP-From, and the replies your agent writes.

Copies a short prompt with links to the spec and the reference server. Paste it into Claude, ChatGPT, Cursor, or any coding assistant open in your project. The same prompt is at /implement.txt.

Show the prompt
Implement the Concierge Agent Protocol (CAP) in this project, so other people's AI agents can ask our agent questions and get answers, instead of scraping our site. They never get our tools, keys, or data.

Read these first:
- Spec: https://conciergeprotocol.org/spec
- Usage, with request and response examples: https://conciergeprotocol.org/usage
- Reference server and examples: https://github.com/conciergeprotocol/cap

Then:
1. Find how this project can answer a question today: its content, search, database, or an existing AI agent. Tell me what you found and where CAP's answer should come from. Ask before you choose.
2. Serve POST /cap/v1/messages and GET /cap/v1/messages/{id}, as the spec describes. Use this project's own framework, or run the reference server with an answer function.
3. Publish /agent.txt at the site root:
   Protocol: CAP/1.0
   Name: {our agent's name}
   Chat: {a page where a person can chat with it}
   CAP: https://{our domain}/cap/v1
4. Keep secrets out. A reply never includes API keys, tools, prompts, or another person's data, and agent.txt lists none of them.
5. If people have accounts here and should ask about them, add login by following https://conciergeprotocol.org/login. Otherwise skip this step.
6. Test with curl: fetch /agent.txt, send a message, and read it back. Show me the responses.

Keep the change small, and match this project's existing code style.

What you build

Piece What it does Required
OAuth line in agent.txt Tells visiting agents where to log in Yes
OAuth metadata Lists your login endpoints at /.well-known/oauth-authorization-server/... Yes
Code login The agent shows the person a short code. They enter it on your page and allow access Yes
Token and revoke endpoints Issue, renew, and end logins Yes
Redirect login For agents that can open a browser and take a callback Optional
Write scope Lets the person allow changes, not only reads Optional
Confirmations Holds a risky action until the person approves it on your page With write

The reference server implements every row. The store example uses all of them in about 150 lines.

The fast path: the reference server

1. Turn on logins

Add an oauth block to cap.json. accounts is for a first test only.

JSON
{
  "agent": { "name": "Harbor Goods", "status": "active", "chat": "https://harborgoods.example/chat" },
  "oauth": {
    "accounts": [{ "account": "maya", "password_env": "MAYA_PASSWORD" }]
  }
}
Shell
cd reference
MAYA_PASSWORD=demo node start.mjs

agent.txt now has an OAuth line, and the server answers the metadata, the code login, the redirect login, the token endpoint, and revocation.

2. Check logins against your users

Start the server from your own code with boot. login receives what the person typed on the login page and returns your account id, or null.

JavaScript
import { boot } from './reference/start.mjs'

const app = await boot({
  config: {
    agent: { name: 'Harbor Goods', status: 'active', chat: 'https://harborgoods.example/chat' },
    oauth: {},
  },
  async login({ username, password }) {
    const user = await users.verify(username, password)
    return user ? user.id : null
  },
  answer,
})
app.server.listen(8088)

3. Answer with the account

answer runs for every message. account is the id login returned, or null without a token. Account questions without one return login_required, and the visiting agent asks the person to log in.

JavaScript
async function answer({ message, context, history, account, scope }) {
  if (/\border\b/i.test(message)) {
    if (!account) return { error: { code: 'login_required', message: 'Log in to Harbor Goods to ask about your order.' } }
    return { reply: await orders.statusFor(account) }
  }
  return { reply: await catalog.search(message, context) }
}

Look the account up from account only. A name or an order number in context is not a login.

If your answers live in another service, use the http handler instead of code. The server posts { message, context, history, account, scope } to your URL and expects { "reply": "..." }. See the reference server.

4. Deploy

Run it behind https, and set PUBLIC_URL to its public origin. Keep allow_http_clients off. Put the OAuth line in the agent.txt your company domain serves:

Text
Protocol: CAP/1.0
Name: Harbor Goods
Chat: https://harborgoods.example/chat
CAP: https://cap.harborgoods.example/cap/v1
OAuth: https://cap.harborgoods.example/oauth

From reference/, node check.mjs runs the conformance checks.

Allow changes

Set "write": true in the oauth block. Your metadata then lists scopes_supported: ["read", "write"].

answer receives scope. Check it in code before any change. A rule in a prompt is not enough.

JavaScript
if (/\bgift ?wrap\b/i.test(message)) {
  if (!account) return loginRequired
  if (scope !== 'write') return { error: { code: 'write_required', message: 'This login can read your order but not change it. Log in again and allow changes.' } }
  await orders.addGiftWrap(account)
  return { reply: 'Gift wrap added, at no charge.' }
}

Write access covers what the person could do themselves, and no more. Run each change as that person, with their own permissions.

Confirm actions that are hard to undo

A cancel, a delete, a payment, or anything else that is hard to take back should not run on a token alone. Return confirm instead of doing it:

JavaScript
if (/\bcancel\b/i.test(message)) {
  if (!account) return loginRequired
  const order = await orders.openOrderFor(account)
  return {
    confirm: {
      summary: `Cancel order ${order.id} (${order.item}, $${order.price}).`,
      authorization_details: [{ type: 'cancel_order', order: order.id }],
    },
  }
}

Then pass act to boot. It runs only after the person approves, once, with the account that approved:

JavaScript
async act({ account, authorization_details }) {
  const [action] = authorization_details
  if (action.type !== 'cancel_order') return { error: { message: 'Unknown action.' } }
  await orders.cancel(account, action.order)
  return { message: `Order ${action.order} is cancelled. The refund goes back to your card in 3 to 5 days.` }
}

What happens:

  1. The message is done. Its reply and confirmation give the person the summary and a link to your confirm page. The action has not run.
  2. The person opens the link, logs in, and approves or denies. Only the account the request is for can approve it.
  3. On approval, act runs once. The visiting agent reads GET /cap/v1/confirmations/{id} and tells the person the result.
  4. A request no one decides expires after 10 minutes.

Write summary from your own records, not from the visiting agent's words, so the person approves what will actually happen. authorization_details is your own description of the action, in the style of RFC 9396. It is stored as data, so an approval survives a restart.

A read login cannot ask for a confirmed action at all. The server answers write_required.

With the http handler, your hook returns { "reply": "...", "confirm": { ... } }, and after approval it receives { "confirmed": { "account", "authorization_details", "caller" } } and returns { "message": "..." }.

Use your own sign-in

The reference login pages ask for a username and password. If your users sign in with SSO, a social login, passkeys, or two-factor, put your own pages in front instead. Visiting agents never see the difference, as long as the endpoints behave the same.

Keep exactly as the spec says:

Replace with your own pages:

If your site already has a signed-in session, these pages can be ordinary pages in your app. The person is usually signed in already, so approving takes one click.

The reference oauth.mjs is a working map of every state: pending and decided codes, one-time code exchange, token rotation, and the confirm claim. Copy its data model even if you replace its pages.

Before you launch

Try it

The store example runs every step on your machine: a public product search, an order lookup after login, gift wrap with a write login, and a cancel that waits for your OK.

Shell
cd examples/store
node start.mjs

Open http://127.0.0.1:8810, click Log in to make changes, and try Cancel my order.