Personal agents are already shopping, booking, and filing claims.

Companies still see two extremes.

Either the agent signs in as the person.

Or the company blocks it.

Neither gives anyone real control.

That gap got a name this week.

Personal Agent Protocol. Poppy for short.

  • On October 9, 2026, Sierra's Bret Taylor and Clay Bavor published Sharing a draft of Personal Agent Protocol. They had announced the protocol earlier that week with Meta and Genesys, Instinct, NiCE, Rocket, Shopify, Stripe, and Walmart. The Oct 9 post shared the draft and named 35 additional design partners, including Adyen, Bank of America, Cloudflare, ElevenLabs, Mastercard, Notion, Okta, OpenAI, PayPal, Plaid, Target, Visa, Zapier, Zendesk, and 1Password.
  • The same day, the draft 0.1 overview at personalagentprotocol.org was updated. It states the problem plainly: today is all or nothing. The agent passes as the user, or the company blocks it.
  • Poppy defines five building blocks: Discovery (/.well-known/poppy.json), Sessions (guest start, agent identifies itself), Sign-in (OAuth on the company page), One session across channels, and Getting the job done (website, APIs via OpenAPI and MCP, or a company agent).
  • It is built on OAuth, JWTs, HTTPS, OpenAPI, and MCP, licensed under Apache 2.0, and described as complementary to Universal Commerce Protocol (UCP).
  • At Sierra Summit, Rocket and Meta's Muse showed a live mortgage pre-approval flow through the protocol.

Same week, same direction elsewhere.

On October 8, 2026, Google Cloud wrote about a universal agent for work with plans, tools, cost controls, and governance. On October 7, 2026, OpenAI described GPT-6 Intelligent UI that composes responses with interactive UI components.

Different products.

Same pressure.

Agents need a way to show up as agents, not as people in disguise.

Here's the part I find most interesting in the Poppy overview.

Until the person signs in, the company knows them only by an ID that contains no personal information. The ID stays the same over time, so the company can recognize a returning user, but it is different at every company.

The agent always identifies itself.

The person stays private until they choose otherwise.

Access is limited to what was granted.

And it can be revoked.

Controls, not policy.

So let's build a tiny version of the discovery and scope idea.

By the end, you'll run one command:

npx tsx gate.ts 

And watch a mock company publish a discovery document, a personal agent start as a guest, escalate to read and write, then fail closed after revoke.

No API key.

No real OAuth.

No JWTs.

Just TypeScript.

One honesty note: this is not Personal Agent Protocol. It is a textbook toy inspired by draft 0.1. The discovery shape is simplified. Session tokens are plain strings. Acme Retail and the order numbers are example inputs.

Table of Contents

  1. What We Are Building
  2. Project Setup
  3. Step 1: Publish a Discovery Document
  4. Step 2: Identify the Personal Agent
  5. Step 3: Start a Guest Session
  6. Step 4: Enforce Scopes
  7. Step 5: Escalate, Then Revoke
  8. Step 6: Run the Timeline
  9. Where It Breaks Down
  10. The Bigger Idea

Code: github.com/bobbyhalljr/tiny-poppy-gate

What We Are Building

Discovery first. Then scopes.

A mock retailer publishes how personal agents may work with it.

The agent loads that document.

It starts a guest session and identifies itself.

Guest actions are allowlisted.

Account actions need read or write.

Missing scope fails closed.

It's also a small version of the architecture behind Roster: agents with lanes, permissions, and a clear answer when something is not allowed.

/.well-known/poppy.json ↓ personal agent identifies itself ↓ guest session (allowlist only) ↓ user Approves read ↓ user Approves write ↓ revoke → guest again 

Project Setup

You will need Node.js 18 or newer.

mkdir tiny-poppy-gate cd tiny-poppy-gate npm init -y npm install --save-dev typescript tsx @types/node 

Save the following blocks, in order, as gate.ts.

Step 1: Publish a Discovery Document

In Poppy, companies publish one file at a standard address.

We model a simplified shape.

// gate.ts: a tiny Poppy-inspired discovery + session scope gate. // Textbook toy inspired by Personal Agent Protocol draft 0.1 (Oct 9, 2026). // NOT an implementation of the real protocol. No OAuth. No JWTs. No network. // Mock session tokens are plain strings. Example company and actions only. // Run: npx tsx gate.ts type Channel = "website" | "api" | "agent"; type Scope = "guest" | "read" | "write"; type SignInMethod = "oauth"; type DiscoveryDocument = { /** Simplified shape inspired by /.well-known/poppy.json */ companyId: string; companyName: string; channels: Channel[]; guestAllowed: string[]; scopes: Scope[]; signIn: SignInMethod[]; }; const discovery: DiscoveryDocument = { companyId: "acme-retail", companyName: "Acme Retail (example company)", channels: ["website", "api", "agent"], guestAllowed: ["checkStock", "returnsPolicy"], scopes: ["guest", "read", "write"], signIn: ["oauth"], }; 

Three channels.

Two guest actions.

Three scopes.

One sign-in method.

That is enough to teach the ladder.

Step 2: Identify the Personal Agent

The overview is clear: a personal agent always identifies itself.

Until sign-in, the company sees a stable non-PII ID that differs per company.

type PersonalAgent = { agentId: string; /** Stable non-PII ID for this user at this company (toy: a fixed string). */ userPseudonym: string; }; const agent: PersonalAgent = { agentId: "muse-like-agent-example", // Different per company in a real protocol. Fixed here for the demo. userPseudonym: "usr_acme_7f3a9c", }; 

agentId says who is acting.

userPseudonym says which returning customer, without a name or email.

Step 3: Start a Guest Session

Guest is the first rung.

Enough to check stock or ask about returns.

Not enough to see orders or place one.

type Session = { token: string; companyId: string; agentId: string; userPseudonym: string; scope: Scope; createdAt: string; }; type Decision = "allow" | "deny"; type TimelineEntry = { at: string; action: string; scope: Scope; decision: Decision; reason: string; result?: string; }; const timeline: TimelineEntry[] = []; let clock = 0; function now(): string { clock += 1; const m = String(Math.floor(clock / 60)).padStart(2, "0"); const s = String(clock % 60).padStart(2, "0"); return `12:${m}:${s}`; } function log( action: string, scope: Scope, decision: Decision, reason: string, result?: string, ) { const entry: TimelineEntry = { at: now(), action, scope, decision, reason, result, }; timeline.push(entry); const mark = decision === "allow" ? "ALLOW" : "DENY "; const extra = result ? ` -> ${result}` : ""; console.log( `${entry.at} ${mark} scope=${scope.padEnd(5)} ${action}${extra}`, ); console.log(` reason: ${reason}`); } function loadDiscovery(): DiscoveryDocument { console.log("Discovery"); console.log(` GET https://acme.example/.well-known/poppy.json`); console.log(` company: ${discovery.companyName}`); console.log(` channels: ${discovery.channels.join(", ")}`); console.log(` guestAllowed: ${discovery.guestAllowed.join(", ")}`); console.log(` scopes: ${discovery.scopes.join(", ")}`); console.log(` signIn: ${discovery.signIn.join(", ")}`); console.log(""); return discovery; } function startGuestSession(doc: DiscoveryDocument, a: PersonalAgent): Session { // Mock token only. Not a JWT. Not OAuth. const session: Session = { token: `sess_guest_${a.userPseudonym}_${Date.now()}`, companyId: doc.companyId, agentId: a.agentId, userPseudonym: a.userPseudonym, scope: "guest", createdAt: now(), }; console.log("Session started (guest)"); console.log(` agentId: ${session.agentId}`); console.log(` userPseudonym: ${session.userPseudonym}`); console.log(` token: ${session.token} (mock string, not a JWT)`); console.log(` scope: ${session.scope}`); console.log(""); return session; } 

The company always knows it is talking to an agent.

That alone is progress over "looks like a browser."

Step 4: Enforce Scopes

The scope ladder is the policy

Every action has a minimum scope.

Guest also has to clear the published allowlist.

Unknown actions fail closed.

const SCOPE_RANK: Record<Scope, number> = { guest: 0, read: 1, write: 2, }; /** Action → minimum scope required (beyond the guest allowlist). */ const ACTION_SCOPE: Record<string, Scope> = { checkStock: "guest", returnsPolicy: "guest", seeOrders: "read", placeOrder: "write", changePayment: "write", }; function hasMinScope(current: Scope, required: Scope): boolean { return SCOPE_RANK[current] >= SCOPE_RANK[required]; } function attempt( session: Session, doc: DiscoveryDocument, action: string, runner: () => string, ): string | null { const required = ACTION_SCOPE[action]; if (!required) { log(action, session.scope, "deny", "unknown action; fail closed"); return null; } if (session.scope === "guest") { if (!doc.guestAllowed.includes(action)) { log( action, session.scope, "deny", `not on guestAllowed [${doc.guestAllowed.join(", ")}]`, ); return null; } } if (!hasMinScope(session.scope, required)) { log( action, session.scope, "deny", `needs scope=${required}; current=${session.scope}`, ); return null; } const result = runner(); log(action, session.scope, "allow", `min scope ${required} satisfied`, result); return result; } 

The order of the checks is the policy.

Unknown first.

Then guest allowlist.

Then minimum scope.

Then run.

Step 5: Escalate, Then Revoke

Real Poppy uses OAuth on the company page.

We mock the consent step as userApproves.

/** * Toy stand-in for "user Approves" after OAuth on the company page. * Real Poppy uses OAuth + scoped tokens. Here we just bump the scope. */ function userApproves(session: Session, next: Scope): Session { if (!discovery.scopes.includes(next)) { throw new Error(`company does not offer scope=${next}`); } if (SCOPE_RANK[next] < SCOPE_RANK[session.scope]) { console.log( `Approve skipped: already at ${session.scope}, requested ${next}`, ); return session; } const updated: Session = { ...session, scope: next, token: `sess_${next}_${session.userPseudonym}_${Date.now()}`, }; console.log(""); console.log(`User approved scope=${next} (mocked OAuth / consent)`); console.log(` new token: ${updated.token} (mock string)`); console.log(""); return updated; } function revokeToGuest(session: Session): Session { const updated: Session = { ...session, scope: "guest", token: `sess_guest_${session.userPseudonym}_${Date.now()}`, }; console.log(""); console.log("Scope revoked to guest (fail closed after revoke)"); console.log(` new token: ${updated.token} (mock string)`); console.log(""); return updated; } // --- mock company backends (example data only) --- function checkStock(): string { return "SKU-1001 in stock: 14 (example)"; } function returnsPolicy(): string { return "30-day returns with receipt (example)"; } function seeOrders(): string { return "2 open orders: #4812, #4819 (example)"; } function placeOrder(): string { return "order #4820 placed for SKU-1001 (example)"; } function changePayment(): string { return "payment method updated (example)"; } 

Read lets you see.

Write lets you change.

Revoke puts you back on the guest ladder.

Step 6: Run the Timeline

Wire the demo in order.

Guest wins on stock.

Guest loses on orders.

Read unlocks seeOrders.

Write unlocks placeOrder.

Revoke closes the door again.

function printTimeline() { console.log(""); console.log("Decision timeline"); console.log("-".repeat(72)); for (const e of timeline) { const mark = e.decision === "allow" ? "ALLOW" : "DENY "; console.log( `${e.at} ${mark} [${e.scope}] ${e.action} | ${e.reason}`, ); } const allowed = timeline.filter((e) => e.decision === "allow").length; const denied = timeline.filter((e) => e.decision === "deny").length; console.log("-".repeat(72)); console.log(`allowed=${allowed} denied=${denied} total=${timeline.length}`); } function main() { console.log("tiny-poppy-gate"); console.log( "Inspired by Personal Agent Protocol (Poppy) draft 0.1, Oct 9, 2026.", ); console.log( "This is a teaching toy. Not the real protocol. Not production security.", ); console.log(""); const doc = loadDiscovery(); let session = startGuestSession(doc, agent); console.log("ActionsSource:https://dev.to/bobbyhalljr/sierra-shared-personal-agent-protocol-build-a-tiny-discovery-gate-in-typescript-401f

Related posts