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
- What We Are Building
- Project Setup
- Step 1: Publish a Discovery Document
- Step 2: Identify the Personal Agent
- Step 3: Start a Guest Session
- Step 4: Enforce Scopes
- Step 5: Escalate, Then Revoke
- Step 6: Run the Timeline
- Where It Breaks Down
- The Bigger Idea
Code: github.com/bobbyhalljr/tiny-poppy-gate
What We Are Building
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
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("Actions");
console.log("");
attempt(session, doc, "checkStock", checkStock);
attempt(session, doc, "returnsPolicy", returnsPolicy);
attempt(session, doc, "placeOrder", placeOrder);
attempt(session, doc, "seeOrders", seeOrders);
session = userApproves(session, "read");
attempt(session, doc, "seeOrders", seeOrders);
attempt(session, doc, "placeOrder", placeOrder);
session = userApproves(session, "write");
attempt(session, doc, "placeOrder", placeOrder);
attempt(session, doc, "changePayment", changePayment);
session = revokeToGuest(session);
attempt(session, doc, "placeOrder", placeOrder);
attempt(session, doc, "seeOrders", seeOrders);
attempt(session, doc, "checkStock", checkStock);
printTimeline();
}
main();
Run it:
npx tsx gate.ts
You should see a timeline like this (tokens and timestamps will differ):
Discovery
GET https://acme.example/.well-known/poppy.json
company: Acme Retail (example company)
guestAllowed: checkStock, returnsPolicy
scopes: guest, read, write
Session started (guest)
agentId: muse-like-agent-example
userPseudonym: usr_acme_7f3a9c
scope: guest
12:00:02 ALLOW scope=guest checkStock -> SKU-1001 in stock: 14 (example)
12:00:03 ALLOW scope=guest returnsPolicy -> 30-day returns with receipt (example)
12:00:04 DENY scope=guest placeOrder
reason: not on guestAllowed [checkStock, returnsPolicy]
12:00:05 DENY scope=guest seeOrders
reason: not on guestAllowed [checkStock, returnsPolicy]
User approved scope=read (mocked OAuth / consent)
12:00:06 ALLOW scope=read seeOrders -> 2 open orders: #4812, #4819 (example)
12:00:07 DENY scope=read placeOrder
reason: needs scope=write; current=read
User approved scope=write (mocked OAuth / consent)
12:00:08 ALLOW scope=write placeOrder -> order #4820 placed for SKU-1001 (example)
12:00:09 ALLOW scope=write changePayment -> payment method updated (example)
Scope revoked to guest (fail closed after revoke)
12:00:10 DENY scope=guest placeOrder
12:00:11 DENY scope=guest seeOrders
12:00:12 ALLOW scope=guest checkStock -> SKU-1001 in stock: 14 (example)
allowed=6 denied=5 total=11
Same agent.
Different scopes.
Different outcomes.
Where It Breaks Down
This is a teaching gate. Here is what a real Poppy integration would still need.
Tokens Are Not Tokens
Our session string is a label.
Real Poppy rides on OAuth and JWTs.
You need issuance, expiry, audience, and revocation that a verifier can trust.
Discovery Is Richer
/.well-known/poppy.json in the draft covers more than our five fields.
Channels, sign-in options, and how to reach website, API, or company agent all need real endpoints.
One Session Across Channels
The protocol keeps one visit across website, API, and company agent.
Our toy never leaves a single process.
Policy Is Not Only Scope Names
An airline might allow any agent to search flights, signed-in agents to see trips, and require approval before buying.
That is product policy on top of the protocol.
The protocol offers controls.
You still choose the rules.
The Bigger Idea
On September 19, in The Computer Is Becoming an API for AI, I predicted that 2027 is when agents get identities: dedicated accounts, credentials, budgets, permissions, and audit trails.
Poppy is an early, public attempt to make that boring and standard.
Not "agent pretends to be a shopper."
Not "company blocks every bot."
Something in between.
┌─────────────────────────────────────────────┐
│ Personal agent × company │
│ │
│ Discovery ──→ Identify ──→ Guest session │
│ ↓ │
│ Sign-in / approve │
│ ↓ │
│ read / write scopes │
│ ↓ │
│ website · API · company agent │
│ ↓ │
│ revoke / expire │
└─────────────────────────────────────────────┘
Discovery provides the menu.
Identity provides honesty.
Scopes provide the ladder.
Revocation provides the off switch.
The model provides the work.
The company provides the channels.
All or nothing was never a protocol. It was a stalemate.
Try Roster
I'm building Roster around this idea: AI employees with real responsibilities, tools, memory, schedules and computer access. They work inside a lane, ask before crossing it, and leave an audit trail you can actually read.
If the same follow-ups, handoffs, and waiting loops keep eating your week, give them to an AI employee.


Top comments (0)