Run Agent Door from your own code
Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page sets the agent class by surface matrix, reads robots.txt per vendor token, runs the reachability gate, checks the agent-safe API basics, and deletes and ranks the rungs with the execution path per class, then hands it all to the run as facts. The run writes the plan or the rollout; the page then holds it to its own facts (agreply.js).
Base URL and the response shape
Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response has one shape:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "..." } }
The token is minted for this app (the guest call takes {"slug":"agent-door"}), so no slug header is needed afterwards. Send it as Authorization: Bearer YOUR_TOKEN. The run body is the input object, sent bare.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The body is not a JSON object, or a required key (task, product, agents, facts) is missing. The page always sends all five of task, product, agents, carry, facts; carry is an empty string for plan. |
| 401 | unauthorized | No token, or a stale one. Mint a guest token or sign in again. |
| 402 | insufficient_credits | The balance is under min_credits. Price with /estimate first. |
| 403 | forbidden | A guest token tried to run: both lanes are metered and need a personal sign-in. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. Retry with the same Idempotency-Key so a retry never double-bills. |
The task field comes first
Two lanes, chosen by task, in the order the work runs - decide who gets in and how, then ship it and prove it worked:
plan- identity and delegation per agent class (identified by, authority, credential, scope, lifetime, sponsor, revocation, approval), the execution path per class, the signup path, and the rungs with owners.rollout- a fix per failed gate, the measurement plan (the agent-class field, a baseline, the funnel), one step per scheduled rung and the exit criteria.
A missing or unknown task is answered as plan. Both lanes send the same five fields. The facts are the page's checks, the matrix and the rungs, one row per line, and a line starting "must:" is a rule the reply has to hold. The page's handoff carries the plan into carry for the rollout lane.
The input
| Field | Required | What |
|---|---|---|
task | yes | plan or rollout. If it is missing or neither, do plan and set "task": "plan". |
product | yes | the product, what agents must do, the surfaces that exist, how humans get in and what agents use today, the edge, agent signals and measurement, what an agent can break, the deadline, one-off or standing, the effort ceiling and the plan date; "not given" means not given. |
agents | yes | the agent classes in scope, the surface paths and training calls, the pasted robots.txt and any probe results. |
carry | send it (empty for plan) | rollout task only: the plan decisions when the plan task ran first, or a sentence saying no plan was carried over. Either way, never mention this input or any input by its name in your reply. |
facts | yes | the page's checks, one line each as area | id | status | evidence, the matrix, the classes with their execution paths, the signup path, and the rungs as rung | decision | order | owning skill | moved by | notes. A line starting "must:" applies to your reply. |
retry_note | no | optional; if present, your previous reply was unusable - follow it. |
The rules the model applies
From the source skill, @samber/agent-integration-surfaces - the only standard the run applies:
- Agents are not one audience. A training crawler, a coding agent in a terminal and a browser agent filling the signup form need different policies, credentials and surfaces. Most "not agent-ready" complaints trace to an edge block or a human-only login, not a missing MCP server.
- The reachability gate is not a rung: until the allow cells answer for the agents meant to use them, every rung scores zero. Enforce agent policy at the edge, never in robots.txt alone - some user-triggered fetchers may ignore it. robots.txt tokens are per class: the training crawler's token does not cover the same vendor's search indexer or user-triggered fetcher.
- A user-agent string is a claim, not an identity: confirm with the vendor's IP list or a verified signature. Identity is not delegation: a signed request proves the operator and carries no user grant; delegation needs a user token.
- Every agent gets its own credential, never a human's long-lived key or session: scoped no broader than the user's grant, short-lived and never longer-lived than the user token it derives from, bound to the agent instance where the client supports it, with a sponsoring human, per-agent revocation and the actor chain in the logs. CLIs and headless agents log in with the device grant. The one exception is a coding agent driving the human's locally logged-in CLI: it inherits the per-install session, so bound that session with refresh rotation, an absolute lifetime and narrow scopes, and list it, since per-agent revocation is impossible there.
- Irreversible or money-moving actions wait for a human approval the agent cannot answer itself, sent outside the agent's channel - a push, an email, a second device - never a confirm button in the session a browser agent drives.
- Execution path per class: coding agents with a shell get an Agent Skill that pilots a --json CLI; shell-less clients and per-user remote access get MCP; fleets get the API plus SDKs. JSON on stdout only, diagnostics on stderr, no prompt without a TTY, next-step hints as data, never as commands to run.
- Where agents create resources, provision-then-claim beats letting agents fill human forms: one unauthenticated call returns the resource, a single-use expiring claim URL and an expiry; unclaimed resources expire under small quotas and hold no personal data; claiming moves the resource into the human's account and billing and accepts the terms there.
- Measure before any rung ships: an agent-class field on every surface, with unknown as a value. Report agent share with its method and as a floor ("at least N%, from env-var detection"), never one headline number, and never sum crawler hits with API calls.
- Never build against an expired or individual draft: build on the final RFCs (token exchange, device grant, protected resource metadata) and borrow drafts' rules, not their wire format.
House rules
- Use only the input. Never invent a company, a vendor rule, a figure, a percentage or a date - except a value a field below asks you to propose (a scope, a lifetime, a sponsor role, an owner), always labelled as proposed when the input does not give it.
- The page's matrix, gate results, paths, signup decision, rungs and checks are settled: never reorder or re-decide a rung, change a path, or call a failed gate fine. Where the input shows a gap, write what replaces it; the gap itself belongs in a caveat.
- Ids are exactly as the facts give them. Class ids: crawler, coding, chat, browser, fleet. Path ids: edge, skillcli, mcp, api, ui. Rung ids: A to G. Signup decisions: claim, deferred, deleted.
- No web links (http or https).
- Write in the language of the input; keep the JSON keys and the ids as given.
- Never use backticks and never write the input's field names as labels in your text. Terms of art from the rules above are free to use; what you never do is paste a whole sentence from this prompt, or state a rule with no fact of this product in it.
- Never write a gendered pronoun for anyone; write they, the team or the owner.
- Reply with ONLY one JSON object - no prose before or after, no code fences.
The replies
plan
Write the identity and delegation model, the execution paths, the signup path and the ranked rungs.
summary- one sentence, at most 40 words: the first thing that must change, and the first rung.identity- one object per class the facts list, in that order:class(its id),identified_by(crawlers and browser agents: a vendor IP list or a verified signature, never the user-agent alone),authority(own, or the user's - delegated),credential(its type, never a human's long-lived key),scope,lifetime(one that expires - never "until revoked"),sponsor(the accountable human role),revocation(per agent, or per install for the bounded CLI session),approval(which actions need a human, and how the approval travels; "none" when nothing is irreversible).paths- one object per class, in the same order:class,path(the page's path id),why(one sentence for this product).signup-decision(the page's),detail(one sentence: the claim flow's shape, or why it was deleted or deferred).rungs- one object per rung in the page's list, in its order:rung(its id),decision(the page's),owner(the team that owns it - a team the input names, or a team you propose, labelled proposed; a skill id is never an owner),skill(the page's owning skill),why(one sentence: which answer moved or removed it, in this product's terms).caveats- at most 4 short strings;[]when there is nothing to say.
{
"task": "plan",
"summary": "one sentence",
"identity": [ { "class": "coding", "identified_by": "how", "authority": "delegated", "credential": "a type", "scope": "named scopes", "lifetime": "hours or days", "sponsor": "a role", "revocation": "per agent", "approval": "which actions, and how" } ],
"paths": [ { "class": "coding", "path": "skillcli", "why": "one sentence" } ],
"signup": { "decision": "claim", "detail": "one sentence" },
"rungs": [ { "rung": "A", "decision": "scheduled", "owner": "a team", "skill": "the page's owning skill", "why": "one sentence" } ],
"caveats": [ ]
}
The page holds it to its own facts (check() in agreply.js; any warning makes the reply flagged): the reply answers the task it was sent; no web link; no backticks; no gendered pronoun; a summary of at most 40 words; one identity row per class in the page's order, every field filled, a lifetime that expires, never a human's key or session, crawlers and browser agents identified by an IP list or a signature, per-agent revocation, and - when an agent can move money or destroy data - an approval outside the agent's channel; the page's path per class; the page's signup decision with a reason; the page's rungs and decisions in order, each with its owning skill, an owner the input names or labelled proposed, and why.
rollout
Write the gate fixes, the measurement plan, one step per scheduled rung and the exit criteria.
summary- one sentence, at most 40 words.gate_fixes- one object per gate the facts mark fail, in the facts' order:gate(its id),fix(one sentence: the change that makes it pass),owner(the team that makes the change - a team the input names, or a team you propose, labelled proposed; the skill that designs a fix, written "designed by" in the facts, is never the owner);[]when no gate fails.measurement-tags(short strings: the agent-class field and where it is logged, with unknown as a value),baseline(one sentence: what is recorded before the first rung ships),funnel(at least three short strings, in order).steps- one object per scheduled rung, in the page's order, no others:rung(its id),first_step(one sentence),done_when(one sentence: the observable result).exit- at least three short strings: the exit criteria the plan is checked against, in this product's terms.caveats- at most 4 short strings;[]when there is nothing to say.
{
"task": "rollout",
"summary": "one sentence",
"gate_fixes": [ { "gate": "reach", "fix": "one sentence", "owner": "a team" } ],
"measurement": { "tags": [ "a tag" ], "baseline": "one sentence", "funnel": [ "a step" ] },
"steps": [ { "rung": "A", "first_step": "one sentence", "done_when": "one sentence" } ],
"exit": [ "a criterion" ],
"caveats": [ ]
}
The page requires: the reply answers the task it was sent; the same text rules; one fix per failed gate in the page's order, each with an owner the input names or labelled proposed; an agent-class field among the tags with unknown as a value, a baseline and at least three funnel steps; any agent share reported as a floor; one step per scheduled rung in order, each with a first step and when it is done; at least three exit criteria.
Worked examples
Three runs from the page's free samples (invented companies): the plan for a database agents are blocked from at the edge, the rollout for the same product carrying that plan, and the plan for a payments platform with browser agents and a demo date. Each input is exactly what the page sends; each reply is the saved reply the sample replays, shipped unedited - but the facts and long strings are shortened in this display only, and every cut is listed under the example. The last line is the page's own check of the full reply.
plan - Vanecroft - agents blocked at the door
The page read it as: agents meant to get in are blocked - fix the gate first; classes crawler, coding, chat; rungs A scheduled, C scheduled, F scheduled, B scheduled, D scheduled, E scheduled, G scheduled; 0 of 6 gates; failing checks: reach, robots, edge, keys, measure, idempotency, destructive, apisafe.
Input (facts is 4630 characters; the first 1397 are shown):
{
"task": "plan",
"product": "Product: Vanecroft\nWhat it does: A serverless Postgres service that developers - and the coding agents working for them - provision and query from a terminal.\nAgents must: read docs, call the API for a user, create resources or sign up\nSurfaces: public API, OpenAPI spec, CLI, SDKs\nHumans get in by: self-serve signup with a free tier; agents today use: a human's API key or session\nEdge: AI-bots preset on; CAPTCHA on signup yes; deny cells enforced at the edge: unknown\nAgent signals: agent user-agents in logs, agent-created signups, shell agents failing at login or prompts; measurement: none\nWhat an agent can break: destructive writes\nDeadline: none; a standing agent channel; effort ceiling: one engineer for a quarter; plan date: 2026-11-02",
"agents": "Agent classes in scope: crawler (Crawlers and AI search), coding (Coding agents with a shell), chat (Chat assistants without a shell)\nPaths: marketing /, docs /docs/, signup /signup, app /console/; training on marketing: allow; training on docs: allow\n\nrobots.txt:\nUser-agent: *\nDisallow: /console/\n\nUser-agent: GPTBot\nUser-agent: ClaudeBot\nUser-agent: Claude-User\nUser-agent: CCBot\nDisallow: /\n\nProbe results (class | surface | status):\n- Claude-User | docs | 403 | Cloudflare AI bots preset -> fail\n- browser | signup | 200 | Turnstile challenge shown -> fail",
"carry": "",
"facts": "The page's checks (8 fail, 4 warn, 2 pass, 3 n/a); verdict: agents meant to get in are blocked - fix the gate first:\n- Reachability gate | classes | na | Crawlers and AI search, Coding agents with a shell, Chat assistants without a shell\n- Reachability gate | reach | fail | a CDN \"block AI bots\" preset is on - it blocks AI search and user-triggered fetchers on allow cells too; a CAPTCHA on signup blocks legitimate browser agents and lets determined bots through; robots.txt disallows 4 tokens on allow cells; 2 probes on allow cells failed (Claude-User | docs | 403 | Cloudflare AI bots preset; browser | signup | 200 | Turnstile challenge shown)\n- robots.txt, token by token | robots | fail | GPTBot is disallowed on marketing site and docs (Disallow: /); ClaudeBot is disallowed on marketing site and docs (Disallow: /); Claude-User is disallowed on marketing site and docs and signup - blocking it stops answers fetching your page (Disallow: /); CCBot is disallowed on marketing site and docs (Disallow: /)\n- robots.txt, token by token | unstated | warn | signup: OAI-SearchBot, Claude-SearchBot, Google-Extended, PerplexityBot and 2 more - robots.txt allows them; state the preference and enforce it at the edge\n- robots.txt, token by token | ignored | warn | ChatGPT-User, Google-Agent, Perplexity-User may ignore robots.txt - a deny for them needs a CDN rule\n- Reachability gate | edge |\n\n[... 3233 more characters cut from this display ...]"
}
Reply (long strings shortened for display - see the cuts below):
{
"task": "plan",
"summary": "First switch off the CDN AI-bots preset and the Turnstile CAPTCHA on /signup and drop the Disallow: / group for GPTBot, ClaudeBot, Claude-User and CCBot, then ship rung A, agent-class tags on every Vanecroft surface.",
"identity": [
{
"class": "crawler",
"identified_by": "the vendor's published IP list or a verified request signature checked at the CDN, never the GPTBot, ClaudeBot or Claude-User user-agent string alone",
"authority": "own - a crawler or user-triggered fetcher reads marketing and docs and carries no user grant",
"credential": "none issued; the verified signature or IP match proves the operator only",
"scope": "read of / and /docs/ only; /signup and /console/ stay denied for training and search tokens",
"lifetime": "per request - each signature check expires with the request; vendor IP lists refreshed daily (proposed)",
"sponsor": "proposed: the platform engineer who owns the CDN rules",
"revocation": "per agent - remove one vendor's token or IP range from the edge allow rule without touching the others",
"approval": "none - crawlers only read public pages"
},
{
"class": "coding",
"identified_by": "a device-grant login from the Vanecroft CLI that registers each agent instance by name",
"authority": "the user's - delegated",
"credential": "an agent-labelled OAuth access token from the device grant, replacing the human's API key or session agents use today",
"scope": "proposed: databases:read, databases:create, query:run on the user's own projects; no billing or org admin scopes",
"lifetime": "proposed: 1-hour access token, refresh never longer-lived than the user's own token",
"sponsor": "proposed: the developer who approved the device-grant code",
"revocation": "per agent - each CLI agent token is listed and revocable without logging the human out",
"approval": "dropping a database, deleting a branch or any other destructive write waits for an email or push to the sponsoring developer, outside the terminal session"
},
{
"class": "chat",
"identified_by": "the OAuth client registration of the chat assistant, with protected resource metadata on the Vanecroft API",
"authority": "the user's - delegated",
"credential": "a user OAuth token issued through the MCP authorization flow, bound to that assistant's client",
"scope": "proposed: databases:read and query:run read-only by default; databases:create on request",
"lifetime": "proposed: 1-hour access token, refresh capped at the user token's lifetime",
"sponsor": "proposed: the Vanecroft user who granted the assistant access",
"revocation": "per agent - the user revokes one assistant's grant from account settings",
"approval": "destructive writes on a database wait for a push or email to the user, never a confirm prompt inside the chat"
}
],
"paths": [
{
"class": "crawler",
"path": "edge",
"why": "AI search and fetchers only need the allow cells on / and /docs/ answering, which today the AI-bots preset and robots.txt block."
},
{
"class": "coding",
"path": "skillcli",
"why": "Vanecroft already has a CLI and logs show shell agents failing at login or prompts, so an Agent Skill piloting a --json CLI with device-grant login fits."
},
{
"class": "chat",
"path": "mcp",
"why": "Chat assistants have no shell to run the Vanecroft CLI, so they need an MCP server, which does not exist yet."
}
],
"signup": {
"decision": "claim",
"detail": "One unauthenticated call creates a small free-tier Postgres database and returns it with a single-use expiring claim URL and an expiry; unclaimed databases expire holding no personal data, and claiming moves the database into the [... 63 more characters cut from this display]"
},
"rungs": [
{
"rung": "A",
"decision": "scheduled",
"owner": "proposed: the platform team that owns Vanecroft logging",
"skill": "this plan (MCP telemetry: mcp-server-offering)",
"why": "Vanecroft has no agent measurement at all, so an agent-class field must exist before any later rung can show it worked."
},
{
"rung": "C",
"decision": "scheduled",
"owner": "proposed: the API and auth team",
"skill": "api-auth-key-management, oauth2-provider-design",
"why": "Agents use a human's API key or session today on a standing channel where destructive writes are possible, so per-agent delegated tokens come early."
},
{
"rung": "F",
"decision": "scheduled",
"owner": "proposed: the growth and signup team",
"skill": "this plan",
"why": "Self-serve developers are the funnel and agents already create Vanecroft accounts, so provision-then-claim replaces the CAPTCHA form once write-safety lands."
},
{
"rung": "B",
"decision": "scheduled",
"owner": "proposed: the developer docs team",
"skill": "coding-agent-docs-optimization",
"why": "Kept in the default order: Markdown docs and a Context7 claim help coding agents read /docs/ once the edge lets them in."
},
{
"rung": "D",
"decision": "scheduled",
"owner": "proposed: the CLI team",
"skill": "cli-interface-design, agent-skill-offering",
"why": "Logs show shell agents failing at the Vanecroft CLI login and prompts, and a CLI already exists, so the effort stays low."
},
{
"rung": "E",
"decision": "scheduled",
"owner": "proposed: the API and auth team",
"skill": "mcp-server-offering",
"why": "Kept in the default order for chat assistants, gated behind write-safety because destructive writes are still unguarded."
},
{
"rung": "G",
"decision": "scheduled",
"owner": "proposed: the platform team",
"skill": "this plan",
"why": "Kept last in the default order as a background task alongside the OpenAPI spec, not an agent-readiness milestone."
}
],
"caveats": [
"Whether the CDN enforces the deny cells on /signup and /console/ is unknown; read the CDN rules for each deny cell.",
"Idempotency and destructive-operation checks fail, and rate limits and untrusted-input handling were not checked.",
"ChatGPT-User, Google-Agent and Perplexity-User may ignore robots.txt, so any deny for them needs a CDN rule.",
"The effort ceiling is one engineer for a quarter, so all seven rungs may not fit."
]
}
Cut from this display only (the saved reply and the check use the full text): signup.detail (292 characters, 229 shown).
What step 5 prints for it: A: scheduled | C: scheduled | F: scheduled | B: scheduled | D: scheduled | E: scheduled | G: scheduled
The page's check: kept - nothing to flag
rollout - Vanecroft - agents blocked at the door
The input carries the saved plan (R.laneForm fills carry from the plan reply), as the page's handoff does. The page's rungs: A scheduled, C scheduled, F scheduled, B scheduled, D scheduled, E scheduled, G scheduled.
Input (facts is 4429 characters; the first 1397 are shown):
{
"task": "rollout",
"product": "Product: Vanecroft\nWhat it does: A serverless Postgres service that developers - and the coding agents working for them - provision and query from a terminal.\nAgents must: read docs, call the API for a user, create resources or sign up\nSurfaces: public API, OpenAPI spec, CLI, SDKs\nHumans get in by: self-serve signup with a free tier; agents today use: a human's API key or session\nEdge: AI-bots preset on; CAPTCHA on signup yes; deny cells enforced at the edge: unknown\nAgent signals: agent user-agents in logs, agent-created signups, shell agents failing at login or prompts; measurement: none\nWhat an agent can break: destructive writes\nDeadline: none; a standing agent channel; effort ceiling: one engineer for a quarter; plan date: 2026-11-02",
"agents": "Agent classes in scope: crawler (Crawlers and AI search), coding (Coding agents with a shell), chat (Chat assistants without a shell)\nPaths: marketing /, docs /docs/, signup /signup, app /console/; training on marketing: allow; training on docs: allow\n\nrobots.txt:\nUser-agent: *\nDisallow: /console/\n\nUser-agent: GPTBot\nUser-agent: ClaudeBot\nUser-agent: Claude-User\nUser-agent: CCBot\nDisallow: /\n\nProbe results (class | surface | status):\n- Claude-User | docs | 403 | Cloudflare AI bots preset -> fail\n- browser | signup | 200 | Turnstile challenge shown -> fail",
"carry": "Identity: crawler - none issued; the verified signature or IP match proves the operator only, per request - each signature check expires with the request; vendor IP lists refreshed daily (proposed), sponsor proposed: the platform engineer who owns the CDN rules; coding - an agent-labelled OAuth access token from the device grant, replacing the human's API key or session agents use today, proposed: 1-hour access token, refresh never longer-lived than the user's own token, sponsor proposed: the developer who approved the device-grant code; chat - a user OAuth token issued through the MCP authorization flow, bound to that assistant's client, proposed: 1-hour access token, refresh capped at the user token's lifetime, sponsor proposed: the Vanecroft user who granted the assistant access\nPaths: crawler -> edge; coding -> skillcli; chat -> mcp\nSignup: claim - One unauthenticated call creates a small free-tier Postgres database and returns it with a single-use expiring claim URL and an expiry; unclaimed databases expire holding no personal data, and claiming moves the database into the developer's account and billing, where the terms are accepted.\nRungs: A scheduled (proposed: the platform team that owns Vanecroft logging); C scheduled (proposed: the API and auth team); F scheduled (proposed: the growth and signup team); B scheduled (proposed: the developer docs team); D scheduled (proposed: the CLI team); E scheduled (proposed: the API and auth team); G scheduled (proposed: the platform team)",
"facts": "The page's checks (8 fail, 4 warn, 2 pass, 3 n/a); verdict: agents meant to get in are blocked - fix the gate first:\n- Reachability gate | classes | na | Crawlers and AI search, Coding agents with a shell, Chat assistants without a shell\n- Reachability gate | reach | fail | a CDN \"block AI bots\" preset is on - it blocks AI search and user-triggered fetchers on allow cells too; a CAPTCHA on signup blocks legitimate browser agents and lets determined bots through; robots.txt disallows 4 tokens on allow cells; 2 probes on allow cells failed (Claude-User | docs | 403 | Cloudflare AI bots preset; browser | signup | 200 | Turnstile challenge shown)\n- robots.txt, token by token | robots | fail | GPTBot is disallowed on marketing site and docs (Disallow: /); ClaudeBot is disallowed on marketing site and docs (Disallow: /); Claude-User is disallowed on marketing site and docs and signup - blocking it stops answers fetching your page (Disallow: /); CCBot is disallowed on marketing site and docs (Disallow: /)\n- robots.txt, token by token | unstated | warn | signup: OAI-SearchBot, Claude-SearchBot, Google-Extended, PerplexityBot and 2 more - robots.txt allows them; state the preference and enforce it at the edge\n- robots.txt, token by token | ignored | warn | ChatGPT-User, Google-Agent, Perplexity-User may ignore robots.txt - a deny for them needs a CDN rule\n- Reachability gate | edge |\n\n[... 3032 more characters cut from this display ...]"
}
Reply (long strings shortened for display - see the cuts below):
{
"task": "rollout",
"summary": "Vanecroft first turns off the CDN AI-bots preset and Turnstile on the allow cells, rewrites robots.txt per token, then tags every request by agent class before agent credentials, the claim signup and the CLI ship.",
"gate_fixes": [
{
"gate": "reach",
"fix": "Turn off the Cloudflare AI-bots preset on marketing and docs and replace the Turnstile challenge on /signup with verified-signature or IP-list checks, so the Claude-User docs probe returns 200 and verified browser agents reach signup.",
"owner": "proposed: the platform engineer who owns the CDN rules"
},
{
"gate": "robots",
"fix": "Remove the Disallow: / group for GPTBot, ClaudeBot, Claude-User and CCBot, keep only the /console/ disallow and add explicit groups for the signup deny, since training is allowed on marketing and docs.",
"owner": "proposed: the developer docs team that owns the public site"
},
{
"gate": "edge",
"fix": "Read the CDN rules for each deny cell (training and search on /signup and /console/) and add an edge rule wherever a deny relies on robots.txt alone, then probe each one.",
"owner": "proposed: the platform engineer who owns the CDN rules"
},
{
"gate": "keys",
"fix": "Stop coding agents from running on the developer's API key or session by issuing agent-labelled device-grant tokens from the CLI and OAuth tokens through the MCP authorization flow, each revocable per agent.",
"owner": "proposed: the API and auth team"
},
{
"gate": "measure",
"fix": "Add an agent-class field to the API, CLI, docs and signup logs, with unknown as a value, and record a baseline before rung C ships.",
"owner": "proposed: the platform team that owns Vanecroft logging"
},
{
"gate": "apisafe",
"fix": "Close the four open items: idempotency keys on database create and write calls, a checked rate-limit policy, untrusted-input handling for query text, and a human approval sent outside the agent session before destructive writes such as [... 21 more characters cut from this display]",
"owner": "proposed: the API and auth team, with the CLI team for destructive confirmations"
}
],
"measurement": {
"tags": [
"agent_class on every API request log: crawler, coding, chat or unknown",
"agent_class on CLI telemetry, set from env-var detection, unknown when absent",
"agent_class on docs and signup edge logs from verified signature or IP list, unknown otherwise",
"agent_class on MCP session logs once the MCP server exists"
],
"baseline": "Before rung A closes, record per class the docs fetch count, CLI login failures, API calls on human keys and agent-created signups, with crawler hits kept apart from API calls.",
"funnel": [
"docs page fetched by an agent",
"CLI or MCP login completed by device grant or OAuth",
"free-tier database provisioned by an agent",
"claim URL opened and database claimed into a developer account",
"first query run on the claimed database"
]
},
"steps": [
{
"rung": "A",
"first_step": "Add the agent-class field with unknown as a value to the API gateway and CLI telemetry and start logging it.",
"done_when": "Every API and CLI log line carries an agent class and a dated baseline per class is recorded."
},
{
"rung": "C",
"first_step": "Add the OAuth device grant to the Vanecroft CLI so a coding agent receives its own agent-labelled access token instead of the developer's API key.",
"done_when": "A coding agent logs in by device grant, its token is revoked alone without logging out the developer, and the actor chain shows in the logs."
},
{
"rung": "F",
"first_step": "Ship the unauthenticated create call that returns a small free-tier database, a single-use claim URL and an expiry, behind the destructive-write approval.",
"done_when": "An agent provisions a database with no form or Turnstile, a developer claims it into their account and billing, and unclaimed ones expire."
},
{
"rung": "B",
"first_step": "Publish markdown versions of the /docs/ pages and claim the Vanecroft library on Context7.",
"done_when": "A coding agent fetches a docs page as markdown and resolves Vanecroft on Context7."
},
{
"rung": "D",
"first_step": "Add a --json flag to the existing CLI with JSON on stdout, diagnostics on stderr and no prompt without a TTY, then wrap it in an Agent Skill.",
"done_when": "Logs stop showing shell agents failing at login or prompts and the Skill drives provision and query end to end."
},
{
"rung": "E",
"first_step": "Build the MCP server over the public API with the MCP authorization flow, gated behind the write-safety fixes.",
"done_when": "A chat assistant connects with the user's OAuth grant, runs a query, and a destructive write waits for an approval outside the chat."
},
{
"rung": "G",
"first_step": "Publish an api-catalog entry pointing at the OpenAPI spec alongside the spec work.",
"done_when": "The api-catalog and agent card resolve and list the current OpenAPI spec."
}
],
"exit": [
"Every allow cell answers for its class: Claude-User reaches docs and verified browser agents reach signup without Turnstile",
"No agent runs on a developer's API key or session; every agent token is revocable alone",
"Destructive writes on a Vanecroft database wait for an out-of-band human approval",
"Agent share is reported per class with its method and as a floor, never as one headline number",
"Agent-provisioned databases are claimed into developer accounts, and unclaimed ones expire"
],
"caveats": [
"No MCP server exists yet, so the chat path waits for rung E.",
"Whether the edge enforces the deny cells is unknown until the CDN rules are read.",
"Signup stays gated behind the write-safety fixes."
]
}
Cut from this display only (the saved reply and the check use the full text): gate_fixes[5].fix (256 characters, 235 shown).
What step 5 prints for it: A: Every API and CLI log line carries an agent class and a dated baseline per class is recorded. | C: A coding agent logs in by device grant, its token is revoked alone without logging out the developer, and the actor chain shows in the logs. | F: An agent provisions a database with no form or Turnstile, a developer claims it into their account and billing, and unclaimed ones expire. | B: A coding agent fetches a docs page as markdown and resolves Vanecroft on Context7. | D: Logs stop showing shell agents failing at login or prompts and the Skill drives provision and query end to end. | E: A chat assistant connects with the user's OAuth grant, runs a query, and a destructive write waits for an approval outside the chat. | G: The api-catalog and agent card resolve and list the current OpenAPI spec.
The page's check: kept - nothing to flag
plan - Quillbarrow - money moves, a demo date
The page read it as: open to agents, with gaps to close; classes crawler, browser, fleet; rungs A scheduled, C scheduled, B scheduled, E deferred, G deferred, D deleted, F deleted; 4 of 6 gates; failing checks: measure, untrusted, spend, apisafe.
Input (facts is 3677 characters; the first 1399 are shown):
{
"task": "plan",
"product": "Product: Quillbarrow\nWhat it does: Accounts-payable automation for mid-market finance teams: invoices in, approvals, and supplier payments out.\nAgents must: read docs, call the API for a user, operate the web app, pay\nSurfaces: public API, OpenAPI spec, SDKs\nHumans get in by: sales-led accounts; an OAuth provider exists (human-only); agents today use: credentials of their own\nEdge: AI-bots preset off; CAPTCHA on signup no; deny cells enforced at the edge: yes\nAgent signals: agent user-agents in logs, RFP lines; measurement: tags, no baseline\nWhat an agent can break: destructive writes, money movement, regulated data\nDeadline: a hard date; a standing agent channel; effort ceiling: one engineer for a quarter; plan date: 2026-11-02",
"agents": "Agent classes in scope: crawler (Crawlers and AI search), browser (Browser or computer-use agents), fleet (Customer agent fleets)\nPaths: marketing /blog/, docs /developers/, signup /demo, app /app/; training on marketing: deny; training on docs: allow\n\nrobots.txt:\nUser-agent: GPTBot\nUser-agent: ClaudeBot\nUser-agent: CCBot\nUser-agent: Google-Extended\nUser-agent: Applebot-Extended\nUser-agent: meta-externalagent\nDisallow: /blog/\nDisallow: /demo\nDisallow: /app/\n\nUser-agent: OAI-SearchBot\nUser-agent: Claude-SearchBot\nUser-agent: PerplexityBot\nDisallow: /demo\nDisallow: /app/\n\nUser-agent: *\nDisallow: /app/\n\nProbe results (class | surface | status):\n- GPTBot | blog | 403 | WAF rule -> enforced\n- OAI-SearchBot | developers docs | 200 -> pass\n- browser | app | 200 | signed agent allowed -> pass",
"carry": "",
"facts": "The page's checks (4 fail, 1 warn, 9 pass, 2 n/a); verdict: open to agents, with gaps to close:\n- Reachability gate | classes | na | Crawlers and AI search, Browser or computer-use agents, Customer agent fleets\n- Reachability gate | reach | pass | no AI-bot preset, no CAPTCHA-only signup; 2 probes passed\n- robots.txt, token by token | robots | pass | every allow cell is open to the tokens of its class (3 groups read)\n- robots.txt, token by token | ignored | warn | ChatGPT-User, Google-Agent, Perplexity-User may ignore robots.txt - a deny for them needs a CDN rule\n- Reachability gate | edge | pass | the CDN or WAF enforces the deny cells; 1 probe confirms it\n- Identity and credentials | keys | pass | agents get credentials of their own\n- Identity and credentials | measure | fail | tags exist but no baseline - no rung can show it worked\n- Agent-safe API basics | errors | pass | passes\n- Agent-safe API basics | idempotency | pass | passes\n- Agent-safe API basics | limits | pass | passes\n- Agent-safe API basics | untrusted | fail | fails - designed by: this plan\n- Agent-safe API basics | spend | fail | fails - designed by: this plan\n- Agent-safe API basics | sandbox | pass | passes\n- Agent-safe API basics | destructive | pass | passes\n- Agent-safe API basics | apisafe | fail | 2 items open: untrusted, spend\n- The page's plan | rungs | na | 1. A, 2. C, 3. B; E deferred, G deferred,\n\n[... 2278 more characters cut from this display ...]"
}
Reply:
{
"task": "plan",
"summary": "First record a baseline on the agent tags Quillbarrow already logs, so rung A, measurement, can show change before agent-labelled keys and out-of-band approval for supplier payments ship in rung C.",
"identity": [
{
"class": "crawler",
"identified_by": "the vendor's published IP list or a verified request signature at the WAF, never the GPTBot or OAI-SearchBot user-agent string alone",
"authority": "own - search indexers and fetchers read public pages and hold no customer grant",
"credential": "none issued; the verified signature or IP match proves the operator only",
"scope": "search and fetch of /blog/ and /developers/, training on /developers/ only; /demo and /app/ stay denied for training and search tokens",
"lifetime": "per request - each signature check expires with the request; vendor IP lists refreshed daily (proposed)",
"sponsor": "proposed: the web platform engineer who owns the WAF rules",
"revocation": "per agent - drop one vendor's token or IP range from the WAF rule",
"approval": "none - crawlers only read public pages"
},
{
"class": "browser",
"identified_by": "a verified request signature from the agent operator, as the signed-agent probe on /app/ already shows, never the user-agent alone",
"authority": "the user's - delegated",
"credential": "an agent-labelled delegated access token issued by token exchange against the finance user's grant, held by the agent alone",
"scope": "proposed: invoices:read and approvals:prepare in the user's own entity; no supplier bank-detail edits",
"lifetime": "proposed: 15-minute token, never longer than the user token it derives from",
"sponsor": "proposed: the finance team member who delegated the task",
"revocation": "per agent - one browser agent's token is revoked while the finance user stays signed in",
"approval": "releasing a supplier payment or deleting an invoice waits for a push to a second device of an approving finance user, never a confirm button in the /app/ session the agent drives"
},
{
"class": "fleet",
"identified_by": "an agent-labelled API key issued to each customer agent, with the actor chain in the logs",
"authority": "the user's - delegated",
"credential": "an agent-labelled scoped API key exchanged for short-lived access tokens, never a human's long-lived key",
"scope": "proposed: invoices:read, invoices:write and payments:propose; payments:release withheld from agents",
"lifetime": "proposed: 1-hour access token, key rotated every 90 days",
"sponsor": "proposed: the customer's finance controller who created the key",
"revocation": "per agent - each fleet agent's key is revoked on its own",
"approval": "every supplier payment and any destructive write on invoices or regulated data waits for an email or push approval to a finance approver, outside the API channel"
}
],
"paths": [
{
"class": "crawler",
"path": "edge",
"why": "The WAF already enforces the deny cells, so crawlers only need /developers/ and the search cells on /blog/ to keep answering."
},
{
"class": "browser",
"path": "ui",
"why": "Agents must operate the Quillbarrow web app, and the signed-agent probe shows /app/ already admits verified browser agents."
},
{
"class": "fleet",
"path": "api",
"why": "Customer agent fleets call the public API with the SDKs Quillbarrow already ships, not a CLI or MCP server."
}
],
"signup": {
"decision": "deleted",
"detail": "Quillbarrow accounts are sales-led with no self-serve path, so there is nothing for an agent to provision and claim; the /demo form stays a human step."
},
"rungs": [
{
"rung": "A",
"decision": "scheduled",
"owner": "proposed: the data and analytics team",
"skill": "this plan (MCP telemetry: mcp-server-offering)",
"why": "Tags already exist but no baseline, so no later rung on Quillbarrow can show it worked until one is recorded."
},
{
"rung": "C",
"decision": "scheduled",
"owner": "proposed: the identity and payments platform team",
"skill": "api-auth-key-management, oauth2-provider-design",
"why": "Agents move supplier money on a standing channel, so inside the hard date this shrinks to agent-labelled scoped keys plus the out-of-band approval gate, with the device grant waiting."
},
{
"rung": "B",
"decision": "scheduled",
"owner": "proposed: the developer relations team",
"skill": "coding-agent-docs-optimization",
"why": "Kept in the default order so agents building on the API read /developers/ as Markdown."
},
{
"rung": "E",
"decision": "deferred",
"owner": "proposed: the API platform team",
"skill": "mcp-server-offering",
"why": "An MCP server does not ship inside the hard date and stays gated behind write-safety."
},
{
"rung": "G",
"decision": "deferred",
"owner": "proposed: the API platform team",
"skill": "this plan",
"why": "Discovery files are a background task beside the OpenAPI spec and do not ship inside the hard date."
},
{
"rung": "D",
"decision": "deleted",
"owner": "proposed: none needed while deleted",
"skill": "cli-interface-design, agent-skill-offering",
"why": "No coding agents with a shell are in scope for Quillbarrow, and it has no CLI."
},
{
"rung": "F",
"decision": "deleted",
"owner": "proposed: none needed while deleted",
"skill": "this plan",
"why": "Quillbarrow accounts are sales-led with no self-serve path, so provision-then-claim has nothing to provision."
}
],
"caveats": [
"Untrusted-input handling and spend controls both fail and are owned by this plan; agents should not pay until they pass.",
"ChatGPT-User, Google-Agent and Perplexity-User may ignore robots.txt, so any deny for them needs a WAF rule.",
"The hard date is not given, so the minimal rung C scope is the planning bound."
]
}
What step 5 prints for it: A: scheduled | C: scheduled | B: scheduled | E: deferred | G: deferred | D: deleted | F: deleted
The page's check: kept - nothing to flag
Build the input yourself
Load the engine in node (global.window = global; require("./agkit.js"); require("./agreply.js")), then:
const K = AgentKit, R = AgentReply;
const f = K.form(Object.assign(K.blank(), { name: "Acme", c_coding: "yes", s_api: "yes", s_cli: "yes", preset: "on",
robots: "User-agent: ClaudeBot\nUser-agent: Claude-User\nDisallow: /" }));
// f.matrix (the class by surface policy), f.robotsRead (each token on each path), f.rungs, f.paths, f.gates
// K.robotsTxt(f), K.matrixCsv(f) - a robots.txt that states the matrix, and the matrix sheet
const b = K.buildInput(f, "plan"); // or "rollout" (R.laneForm(form, "rollout", plan) fills carry)
const wire = K.toWire(b.input); // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output), "plan");
// R.check(reply, b) // {status: kept or flagged, notes}
Get a token for the steps below from /tokens.html.
Step 1 — a tiny client helper
One function that sends JSON and unwraps the response. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="YOUR_TOKEN" # from https://agent-door.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # from https://agent-door.skillsafe.ai/tokens.html
def api(method, path, body=None, headers=None):
req = urllib.request.Request(BASE + path, method=method,
data=None if body is None else json.dumps(body).encode(),
headers={"Content-Type": "application/json", **({"Authorization": "Bearer " + TOKEN} if TOKEN else {}), **(headers or {})})
with urllib.request.urlopen(req) as r:
env = json.load(r)
if env.get("error"):
raise RuntimeError(env["error"])
return env["data"]
// agent-door.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "YOUR_TOKEN"; // from https://agent-door.skillsafe.ai/tokens.html
async function api(method, path, body, headers = {}) {
const r = await fetch(BASE + path, { method, headers: { "Content-Type": "application/json",
...(TOKEN ? { Authorization: "Bearer " + TOKEN } : {}), ...headers },
body: body === undefined ? undefined : JSON.stringify(body) });
const env = await r.json();
if (env.error) throw new Error(env.error.code + ": " + env.error.message);
return env.data;
}
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "YOUR_TOKEN" // from https://agent-door.skillsafe.ai/tokens.html
func api(method, path string, body any, headers map[string]string) (map[string]any, error) {
var buf bytes.Buffer
if body != nil { json.NewEncoder(&buf).Encode(body) }
req, _ := http.NewRequest(method, base+path, &buf)
req.Header.Set("Content-Type", "application/json")
if token != "" { req.Header.Set("Authorization", "Bearer "+token) }
for k, v := range headers { req.Header.Set(k, v) }
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
var env struct { Data map[string]any; Error map[string]any }
json.NewDecoder(res.Body).Decode(&env)
if env.Error != nil { return nil, fmt.Errorf("%v", env.Error) }
return env.Data, nil
}
// Agent Door.java -- Java 17+, with com.fasterxml.jackson.core:jackson-databind
import com.fasterxml.jackson.databind.*;
import java.net.URI; import java.net.http.*; import java.util.*;
public class Agent Door {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "YOUR_TOKEN"; // from https://agent-door.skillsafe.ai/tokens.html
static final ObjectMapper M = new ObjectMapper();
static final HttpClient C = HttpClient.newHttpClient();
static JsonNode api(String method, String path, Object body, Map<String, String> headers) throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + path)).header("Content-Type", "application/json");
if (!token.isEmpty()) b.header("Authorization", "Bearer " + token);
if (headers != null) headers.forEach(b::header);
b.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(M.writeValueAsString(body)));
JsonNode env = M.readTree(C.send(b.build(), HttpResponse.BodyHandlers.ofString()).body());
if (env.hasNonNull("error")) throw new RuntimeException(env.get("error").toString());
return env.get("data");
}
}
require "json"
require "net/http"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # from https://agent-door.skillsafe.ai/tokens.html
def api(method, path, body = nil, headers = {})
uri = URI(BASE + path)
req = Net::HTTPGenericRequest.new(method, !body.nil?, true, uri.request_uri,
{ "Content-Type" => "application/json" }.merge(TOKEN.empty? ? {} : { "Authorization" => "Bearer #{TOKEN}" }).merge(headers))
req.body = JSON.generate(body) unless body.nil?
env = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)
raise env["error"].to_s if env["error"]
env["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = "YOUR_TOKEN"; // from https://agent-door.skillsafe.ai/tokens.html
function api(string $method, string $path, $body = null, array $headers = []) {
global $TOKEN;
$h = ["Content-Type: application/json"];
if ($TOKEN) $h[] = "Authorization: Bearer $TOKEN";
foreach ($headers as $k => $v) $h[] = "$k: $v";
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h, CURLOPT_RETURNTRANSFER => true]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$env = json_decode(curl_exec($ch), true);
if (!empty($env["error"])) throw new Exception(json_encode($env["error"]));
return $env["data"];
}
// Program.cs -- .NET 6+ console app
using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes;
static class Api {
public const string Base = "https://api.skillsafe.ai/v1/app-api";
public static string Token = "YOUR_TOKEN"; // from https://agent-door.skillsafe.ai/tokens.html
static readonly HttpClient C = new();
public static async Task<JsonNode?> Call(string method, string path, object? body = null, Dictionary<string, string>? headers = null) {
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (Token != "") req.Headers.Add("Authorization", "Bearer " + Token);
if (headers != null) foreach (var h in headers) req.Headers.Add(h.Key, h.Value);
if (body != null) req.Content = JsonContent.Create(body);
var env = JsonNode.Parse(await (await C.SendAsync(req)).Content.ReadAsStringAsync())!;
if (env["error"] is JsonNode e && e.GetValueKind() != JsonValueKind.Null) throw new Exception(e.ToJsonString());
return env["data"];
}
}
Step 2 — a token
A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"agent-door"}' | jq -r '.data.token')
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = ""
TOKEN = api("POST", "/guest", {"slug": "agent-door"})["token"]
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = "";
TOKEN = (await api("POST", "/guest", { slug: "agent-door" })).token;
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = ""
g, err := api("POST", "/guest", map[string]string{"slug": "agent-door"}, nil)
if err != nil { panic(err) }
token = g["token"].(string)
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = "";
token = api("POST", "/guest", Map.of("slug", "agent-door"), null).get("token").asText();
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN.replace("")
TOKEN.replace(api("POST", "/guest", { slug: "agent-door" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "agent-door"])["token"];
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
Api.Token = "";
Api.Token = (await Api.Call("POST", "/guest", new { slug = "agent-door" }))!["token"]!.GetValue<string>();
Step 3 — GET /me
Who the token is (subject_type: user or guest) and the credit balance.
api GET /me | jq '.data | {subject_type, credits}'
me = api("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
me, _ := api("GET", "/me", nil, nil)
fmt.Println(me["subject_type"], me["credits"])
JsonNode me = api("GET", "/me", null, null);
System.out.println(me.get("subject_type") + " " + me.get("credits"));
me = api("GET", "/me")
puts [me["subject_type"], me["credits"]].inspect
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"] ?? "", "\n";
var me = await Api.Call("GET", "/me");
Console.WriteLine($"{me!["subject_type"]} {me["credits"]}");
Step 4 — POST /estimate
Free. The body is the run input itself, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra.
# input.json: the run input object - {"task": "plan", "product": "Product: Vanecroft
What it does: ...", "agents": "Agent classes in scope: crawler, coding, chat ...", "carry": "", "facts": "The page's checks (...); verdict: agents meant to get in are blocked - fix the gate first:
- Reachability gate | reach | fail | ..."}
# - see the input table
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}'
inp = json.load(open("input.json")) # {"task": "plan", "product": "Product: Vanecroft
What it does: ...", "agents": "Agent classes in scope: crawler, coding, chat ...", "carry": "", "facts": "The page's checks (...); verdict: agents meant to get in are blocked - fix the gate first:
- Reachability gate | reach | fail | ..."}
# - see the input table
est = api("POST", "/estimate", inp)
print(est["hold_credits"], est["min_credits"], est["model"], est.get("warnings"))
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync("input.json", "utf8")); // {task: "plan" or "rollout", product, agents, carry, facts} (see the input table)
const est = await api("POST", "/estimate", input);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
raw, _ := os.ReadFile("input.json") // add "os" to the imports
var input map[string]any
json.Unmarshal(raw, &input)
est, _ := api("POST", "/estimate", input, nil)
fmt.Println(est["hold_credits"], est["min_credits"], est["model"])
Map<String, Object> input = M.readValue(new java.io.File("input.json"), Map.class);
JsonNode est = api("POST", "/estimate", input, null);
System.out.println(est.get("hold_credits") + " " + est.get("min_credits") + " " + est.get("model"));
input = JSON.parse(File.read("input.json"))
est = api("POST", "/estimate", input)
puts [est["hold_credits"], est["min_credits"], est["model"]].inspect
$input = json_decode(file_get_contents("input.json"), true);
$est = api("POST", "/estimate", $input);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], "\n";
var input = JsonNode.Parse(File.ReadAllText("input.json"))!;
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"{est!["hold_credits"]} {est["min_credits"]} {est["model"]}");
Step 5 — POST /run, then poll the job
Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses agent-door:<task>:<inputHash>:a<attempt> - the lane is part of the key, retry_note is left out of the hash - and its one automatic reformat retry is a new attempt number, named in the page as one extra run. A plan reply carries summary, identity, paths, signup, rungs and caveats; a rollout reply carries summary, gate_fixes, measurement, steps, exit and caveats. The code below prints the gist of either: each rung and its decision, or each step and when it is done.
KEY="agent-door:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}'
jq -r 'if .task == "rollout" then (.steps[] | "\(.rung): \(.done_when)")
else (.rungs[] | "\(.rung): \(.decision)") end' reply.json
import hashlib, time
key = "agent-door:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the reply as one JSON object
print(j["charged_credits"], j.get("truncated"), reply["task"])
if reply["task"] == "rollout":
for x in reply["steps"]:
print(x["rung"], x["done_when"])
else:
for x in reply["rungs"]:
print(x["rung"], x["decision"])
import { createHash } from "node:crypto";
const key = `agent-door:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, reply.task);
if (reply.task === "rollout") { for (const x of reply.steps) console.log(x.rung, x.done_when); }
else { for (const x of reply.rungs) console.log(x.rung, x.decision); }
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "agent-door:" + input["task"].(string) + ":" + hex.EncodeToString(sum[:8]) + ":a1"
job, _ := api("POST", "/run", input, map[string]string{"Idempotency-Key": key})
var j map[string]any
for {
time.Sleep(2 * time.Second)
j, _ = api("GET", "/jobs/"+job["job_id"].(string), nil, nil)
if s := j["status"]; s == "succeeded" || s == "failed" || s == "cancelled" { break }
}
var reply map[string]any
json.Unmarshal([]byte(j["output"].(map[string]any)["output"].(string)), &reply)
fmt.Println(j["charged_credits"], j["truncated"], reply["task"])
key, a, b := "rungs", "rung", "decision"
if reply["task"] == "rollout" {
key, a, b = "steps", "rung", "done_when"
}
xs, _ := reply[key].([]any)
for _, x := range xs {
xx := x.(map[string]any)
fmt.Println(xx[a], xx[b])
}
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "agent-door:" + input.get("task") + ":" + HexFormat.of().formatHex(digest, 0, 8) + ":a1";
JsonNode job = api("POST", "/run", input, Map.of("Idempotency-Key", key)), j;
do { Thread.sleep(2000); j = api("GET", "/jobs/" + job.get("job_id").asText(), null, null); }
while (!Set.of("succeeded", "failed", "cancelled").contains(j.get("status").asText()));
JsonNode reply = M.readTree(j.get("output").get("output").asText());
System.out.println(j.get("charged_credits") + " " + reply.path("task").asText());
if (reply.path("task").asText().equals("rollout")) {
for (JsonNode x : reply.path("steps")) System.out.println(x.path("rung").asText() + " " + x.path("done_when").asText());
} else {
for (JsonNode x : reply.path("rungs")) System.out.println(x.path("rung").asText() + " " + x.path("decision").asText());
}
require "digest"
key = "agent-door:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["task"]].inspect
if reply["task"] == "rollout"
reply["steps"].each { |x| puts "#{x["rung"]} #{x["done_when"]}" }
else
reply["rungs"].each { |x| puts "#{x["rung"]} #{x["decision"]}" }
end
$key = "agent-door:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", $reply["task"], "\n";
if ($reply["task"] === "rollout") { foreach ($reply["steps"] as $x) echo $x["rung"], " ", $x["done_when"], "\n"; }
else { foreach ($reply["rungs"] as $x) echo $x["rung"], " ", $x["decision"], "\n"; }
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"agent-door:{input["task"]}:{Convert.ToHexString(digest, 0, 8).ToLowerInvariant()}:a1";
var job = await Api.Call("POST", "/run", input, new() { ["Idempotency-Key"] = key });
JsonNode j;
do { await Task.Delay(2000); j = (await Api.Call("GET", "/jobs/" + job!["job_id"]))!; }
while (!new[] { "succeeded", "failed", "cancelled" }.Contains(j["status"]!.GetValue<string>()));
var reply = JsonNode.Parse(j["output"]!["output"]!.GetValue<string>())!;
var task = (string?)reply["task"];
Console.WriteLine($"{j["charged_credits"]} {task}");
if (task == "rollout") { foreach (var x in reply["steps"]!.AsArray()) Console.WriteLine($"{x!["rung"]} {x["done_when"]}"); }
else { foreach (var x in reply["rungs"]!.AsArray()) Console.WriteLine($"{x!["rung"]} {x["decision"]}"); }
Step 6 — POST /run-stream
The same run as Server-Sent Events, with the same input.json. Browsers receive progress ticks rather than text deltas; the final done event carries the reply.
# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: agent-door:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json
rev = json.load(open("input.json")) # the same run input as input.json: {"task": "plan", "product": "Product: Vanecroft
What it does: ...", "agents": "Agent classes in scope: crawler, coding, chat ...", "carry": "", "facts": "The page's checks (...); verdict: agents meant to get in are blocked - fix the gate first:
- Reachability gate | reach | fail | ..."}
# - see the input table
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "agent-door:" + rev["task"] + ":" + hashlib.sha256(json.dumps(rev).encode()).hexdigest()[:16] + ":a1"})
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().strip()
if line.startswith("data:"):
print(line[5:].strip()[:200])
const rev = JSON.parse(readFileSync("input.json", "utf8"));
const r = await fetch(BASE + "/run-stream", { method: "POST", body: JSON.stringify(rev), headers: {
"Content-Type": "application/json", Authorization: "Bearer " + TOKEN,
"Idempotency-Key": `agent-door:${rev.task}:${createHash("sha256").update(JSON.stringify(rev)).digest("hex").slice(0, 16)}:a1` } });
const dec = new TextDecoder();
for await (const chunk of r.body) for (const line of dec.decode(chunk).split("\n"))
if (line.startsWith("data:")) console.log(line.slice(5).trim().slice(0, 200));
rraw, _ := os.ReadFile("input.json") // add "bufio", "strings" to the imports
rs := sha256.Sum256(rraw)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(rraw))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "agent-door:"+input["task"].(string)+":"+hex.EncodeToString(rs[:8])+":a1")
res, _ := http.DefaultClient.Do(req)
sc := bufio.NewScanner(res.Body)
for sc.Scan() { if l := sc.Text(); strings.HasPrefix(l, "data:") { fmt.Println(l[5:]) } }
String rev = java.nio.file.Files.readString(java.nio.file.Path.of("input.json"));
var rd = java.security.MessageDigest.getInstance("SHA-256").digest(rev.getBytes());
var sreq = HttpRequest.newBuilder(URI.create(BASE + "/run-stream")).header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token).header("Idempotency-Key", "agent-door:" + input.get("task") + ":" + HexFormat.of().formatHex(rd, 0, 8) + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(rev)).build();
C.send(sreq, HttpResponse.BodyHandlers.ofLines()).body().filter(l -> l.startsWith("data:")).forEach(System.out::println);
rev = File.read("input.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "agent-door:#{JSON.parse(rev)["task"]}:#{Digest::SHA256.hexdigest(rev)[0, 16]}:a1")
req.body = rev
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |h|
h.request(req) { |res| res.read_body { |chunk| chunk.each_line { |l| puts l if l.start_with?("data:") } } }
end
$rev = file_get_contents("input.json");
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $rev, CURLOPT_HTTPHEADER => [
"Content-Type: application/json", "Authorization: Bearer $TOKEN",
"Idempotency-Key: agent-door:" . json_decode($rev, true)["task"] . ":" . substr(hash("sha256", $rev), 0, 16) . ":a1"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) { foreach (explode("\n", $chunk) as $l) if (str_starts_with($l, "data:")) echo $l, "\n"; return strlen($chunk); }]);
curl_exec($ch);
var rev = File.ReadAllText("input.json");
var rd = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(rev));
var sreq = new HttpRequestMessage(HttpMethod.Post, Api.Base + "/run-stream") { Content = new StringContent(rev, System.Text.Encoding.UTF8, "application/json") };
sreq.Headers.Add("Authorization", "Bearer " + Api.Token);
sreq.Headers.Add("Idempotency-Key", $"agent-door:{input["task"]}:{Convert.ToHexString(rd, 0, 8).ToLowerInvariant()}:a1");
using var http = new HttpClient();
using var res = await http.SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
using var rdr = new StreamReader(await res.Content.ReadAsStreamAsync());
for (string? l; (l = await rdr.ReadLineAsync()) != null;) if (l.StartsWith("data:")) Console.WriteLine(l);