agents
What an agent may do here.
Fwozen is built to be driven by a program. An agent can take an organisation from nothing to an enforced freeze without a human driving a browser for anything except consent. This page is the contract: which credentials exist, which four moments still need a person, and which decisions an agent must never make on its own.
what agents may do
The whole journey, minus the consent.
Everything below already exists as a documented REST route. There is no separate agent API, no partner tier, and no application to fill in. A program uses the same endpoints, the same scopes, and the same audit log as the dashboard does.
an agent can
- Create the organisation and read its onboarding state.
- Mint the signed GitHub App install URL and poll the install to completion.
- Enable the repositories a person named, and watch their branches.
- Fetch the enforcement snippets a person then saves in GitHub.
- Read freezes, repositories, schedules, entitlements, and the audit log.
- Start and lift freezes and per-PR exceptions, when told to, with a reason.
- Start the no-card trial, and hand a person a checkout URL.
nothing here can
- Charge a card. Checkout and the billing portal return a URL for a person.
- Mint a credential from a credential. A key can never create another key.
- Enable a repository on its own initiative.
- Set up or repair an enforcement rule. That is a signed-in action — an fwz_ key is refused with 403 credential_type_not_allowed, and no scope in the list below grants it. No endpoint deletes a ruleset at all.
- Convert a trial into a subscription. Expiry drops the organisation to Free.
- Read a byte of your source code. No content endpoint is called, by anyone.
credentials
Two credentials, and only one of them mints.
A session is a person’s authorisation, obtained once through a device flow. A key is unattended automation. The rule that keeps the two apart is that a key can never mint a key, so a leaked key cannot widen itself into a longer-lived or broader-scoped one.
sessions: device authorization (RFC 8628)
curl -s -X POST https://fwozen.com/api/auth/device/code \
-H 'content-type: application/json' \
-d '{"client_id":"fwozen-agent"}'
# show the human verification_uri_complete and user_code, then poll:
curl -s -X POST https://fwozen.com/api/auth/device/token \
-H 'content-type: application/json' \
-d '{"grant_type":"urn:ietf:params:oauth:grant-type:device_code",
"client_id":"fwozen-agent",
"device_code":"<device_code>"}'
# authorization_pending -> keep waiting. slow_down -> add 5s and keep waiting.
# access_denied / expired_token -> terminal, start again.
# success -> {"access_token":"<session token>","token_type":"Bearer","expires_in":2592000}Both bodies are JSON, and that is worth reading twice, because RFC 8628 says form-encoded. The device endpoint accepts either. The token endpoint accepts JSON only and answers 415 to a form body, so a client that follows the RFC to the letter starts a login it can never finish. Send JSON to both.
That client id is public by design. This is a public client, it has no secret, and it is published in /.well-known/fwozen-app so an agent never has to go through a human to find it. The authorization server validates it against a fixed allowlist, so it is a name, never a capability. Never look for a client secret and never send one.
the approval page
What the human sees, and what you must not do about it.
The device flow’s whole security property is that a person, in their own browser, looks at who is asking and says yes. Everything on https://fwozen.com/device exists to make that a real decision rather than a reflex. Some of it is friction you are not allowed to remove.
the page is two steps, by protocol
First the person enters the code, which claims it for their session. Only then does the page show them the application name, the scopes it asked for and the RFC 8628 §5.4 warning, with explicit approve and deny buttons. The server refuses an approval for a code the approving session has not looked up, so there is no blind-POST path, not for them and not for anyone who talks them into pasting something.
the link prefills; it never approves
verification_uri_complete carries the code in the query string so nobody types it twice. It does not submit it, and it does not approve it. Show the person both the URL and the code, say what you are, and let them decide. The page sends a signed-out visitor to sign in, then back with the code still filled in.
One more thing worth saying plainly. A first-party device session signs you in as that person. The page shows them the scopes you request, and the approval is recorded, but the JWT you receive carries their role and their permissions. The scope list is not a ceiling the API enforces on a session. If you want a credential that cannot do more than a stated list, that is an fwz_ key, and that is what the scopes on a key are for.
human consent
Four moments that are not yours to simulate.
These are not friction we intend to remove. Each one is a person granting something a program cannot grant itself, and an agent that tries to route around one is doing something worse than failing.
Approving the device code
An agent starts a device authorization and shows the person a URL and a short code. They open fwozen.com/device, sign in, enter the code, and see who is asking before anything is granted. The link may prefill the code, but it never approves it. Until they press approve in their own browser, the agent has no session and no organisation. There is no headless path around this, and the client is a public client with no secret to compromise.
Installing the GitHub App
A GitHub organisation owner installs the App from a URL Fwozen minted with a signed nonce, and grants it on GitHub’s own consent screen. If the person is not an owner, GitHub files an approval request instead and the install sits pending. GitHub sends nothing in between, so an agent that reads a thirty-second silence as failure is wrong.
Making the fwozen check required
Making the fwozen check required is a signed-in action, never an agent’s: POST /v1/enforcement/setup refuses an fwz_ key with 403 credential_type_not_allowed, and no key scope grants it. That holds both ways it can happen. On an installation that has granted repository administration, a signed-in person presses one button and Fwozen writes the repository ruleset. Everywhere else, a person with that authority saves one native rule in GitHub’s own settings under their own account, so GitHub’s audit log shows a human because it was one. Fwozen asks for no organisation administration in either direction, so a rule at organisation scope is always a rule a person saves. Fwozen shows the exact values and verifies afterwards.
Entering a card
Nothing in the API, the MCP server, or the CLI charges anything. The checkout route returns a URL. An agent hands that URL to a person and stops. The 14-day trial takes no card at all and does not auto-convert. At expiry the organisation drops to Free.
surfaces
Every surface a program can reach.
The same table appears on the security page, rendered from the same source, so a reviewer and an agent read identical facts. The not allowed column is the one that matters.
/llms.txt · /llms-full.txt
no credentialThe map and the corpus: what Fwozen is, the whole acquisition journey with the four human-consent moments marked, and every documentation page. Both origins serve the same bytes from one builder.
not allowedNothing tenant-specific, ever. The build writes these documents from the marketing content and the public route table, and they contain no customer data of any kind.
/.well-known/fwozen-app
no credentialApp and auth discovery as JSON: the App slug, the install-URL template, the OpenAPI and skills links, and both credential schemes. One is Fwozen’s own device flow, with its two endpoints, its public client id, and its human approval page. The other is the `fwz_` bearer scheme with its scope list.
not allowedThis document publishes no secret. The device-flow client id is a PUBLIC client and has no secret to leak; there is no client secret, no App private key, and no installation id in this document.
/.well-known/skills/index.json · /skills/fwozen/SKILL.md
no credentialThe agent skill, published under the agentskills.io index schema with a SHA-256 digest over the exact bytes served. The skill is the CLI’s own file, copied and hashed at build.
not allowedThe digest is generated, not transcribed, so the index cannot advertise a hash for a file that has moved on. Nothing here executes; it is a document.
GET /v1/plans
no credentialThe plan catalog, from the same constant the pricing page renders. Tiers, prices, what each includes, and the trial terms. Cacheable for five minutes.
not allowedNo organisation, no usage, no entitlement lookup. It answers identically for every caller, which is why it needs no credential.
GET /.well-known/oauth-protected-resource
no credentialRFC 9728 protected resource metadata for the MCP endpoint: the resource URL, the authorization server, the bearer methods, and the supported scopes. The MCP endpoint answers an unauthenticated request with a 401 whose `WWW-Authenticate` header points at this document.
not allowedMetadata only. It grants nothing, and discovering it is not a step toward a credential. The credential still comes from a human approving a device code, or from a key an authorised person minted.
POST /v1/api-keys
session onlyMints an `fwz_` key. A signed-in person creates it, or an agent holding the session that person approved in the device flow.
not allowedA key can never mint a key. This route takes a session credential only, so a leaked `fwz_` key cannot widen itself into a second credential with different scopes or a longer life.
Agent-designated API keys
session onlyA key is created as `human` or `agent`. An `agent` key must carry an expiry, capped at 90 days, and every audit row written under a key names the key’s id, label, and kind, so an agent principal is distinguishable from the person who authorised it.
not allowedScopes cannot be widened after creation, and keys do not get billing scopes by default. The scope picker shows those unchecked. An agent key with no billing scope cannot open a checkout session or start a trial.
GET /v1/installations/status?state=…
session or keyResolves the signed `state` nonce that `GET /v1/github/install-url` minted, so an agent can poll an install to `pending`, `installed_unclaimed`, or `installed_claimed` instead of asking a human whether they finished.
not allowedIt never accepts a caller-supplied installation id, and it reveals nothing unless the nonce is valid and was minted for that caller. Any mismatch is a 404, the same answer as a nonce that never existed, so nobody can probe for installations through this route.
POST /mcp
session or keyA streamable-HTTP MCP server. It accepts exactly what the REST API accepts: a session JWT or an `fwz_` key. It builds the same actor and stays scoped to that actor’s organisation. Its tools are thin adapters over routes that already exist, each marked read-only or destructive, and no tool mixes a read with a write.
not allowedNo tool completes a payment. The checkout tool returns a URL for a human and stops. No tool enables a repository the caller did not name. Ending a freeze is the one destructive verb. It is marked destructive, and it refuses without an explicit confirmation argument, because a thaw unblocks a merge someone deliberately stopped.
Per-principal write limits
session or keyFreezes, repository enrolment, and branch changes push state into GitHub. The routes behind them carry a per-principal bucket (by key id or user id) in front of the per-organisation one, and organisation creation is limited per signed-in user. An MCP tool call charges the bucket exactly as a REST request does, so driving a write through `POST /mcp` costs what the route costs. Agents retry harder than people do, and the write path into GitHub has a real ceiling behind it.
not allowedA tripped limit is a 429 with a `Retry-After`, never a silently dropped write and never a partial freeze reported as applied. Lifting a freeze is exempt from the per-principal bucket on both surfaces, so one runaway client can never spend the way out. It is not exempt from the organisation’s own ceiling, which is charged first, so a lift can still answer 429. Wait out the `Retry-After` rather than reading it as a freeze that cannot be lifted. The limits are modest and tunable, and we do not quote a number here, because the number is an operational setting rather than a promise. Read `RateLimit-Remaining` off a credentialled `/v1` response instead. The two credential-free routes carry no rate headers.
Connect an MCP client.
The server is https://api.fwozen.com/mcp, streamable HTTP, and it takes exactly the credentials the REST API takes. Two ways in. Pick the second one if you are unattended.
a. sign in as a person (OAuth)
claude mcp add --transport http fwozen https://api.fwozen.com/mcpAn unauthenticated call answers 401 with a WWW-Authenticate header naming the protected-resource document, which names the authorization server, and the client walks that chain itself.
b. send a key (unattended)
claude mcp add --transport http fwozen https://api.fwozen.com/mcp \
--header "Authorization: Bearer fwz_..."For a client that takes a config file rather than a command:
{
"mcpServers": {
"fwozen": {
"type": "http",
"url": "https://api.fwozen.com/mcp",
"headers": {
"Authorization": "Bearer fwz_..."
}
}
}
}start here
Where to point a cold agent.
The map. What Fwozen is, the whole journey from nothing to an enforced freeze, and every other document on this list. Start here.
llms.txt
The same document followed by every documentation page and the public route table. One fetch instead of twenty, if you can afford the tokens.
llms-full.txt
App and auth discovery as JSON: the App slug, the install-URL template, both credential schemes, and the scope list. Served by this origin and by the API.
.well-known/fwozen-app
The full request and response schemas for every publicly documented route, with each route’s auth mode and scopes on it.
OpenAPI 3.1
An agentskills.io 0.2.0 index over the Fwozen skill, with a SHA-256 digest of the exact bytes served.
.well-known/skills/index.json
The skill itself: the acquisition journey, the gate discipline, and the commands. It is the CLI’s own file, published verbatim.
skills/fwozen/SKILL.md
One standing qualifier, so nobody reads this list as more than it is. The CLI is not published to npm, so npx fwozen does not work today. Install it with curl -fsSL https://fwozen.com/install | sh, or use the REST API directly.
rate limits
Rate limits, and why we do not print the number here.
Reads and writes have separate per-minute budgets, and they scale with the plan. The deploy-gate poll has a third budget of its own, generous on purpose, because throttling a deploy check would break deploys. It must never share a bucket with dashboard reads. Writes are the tightest of the three, because behind them is a real ceiling on how fast anything can push state into GitHub. A per-principal bucket sits in front of the per-organisation one, so one runaway client cannot spend the whole organisation’s budget.
Every /v1 response to a credentialled request carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset; a 429 additionally carries Retry-After. Honour it. Back off for that long rather than retrying on a fixed timer, and never treat a 429 as a reason to re-issue a freeze that may already have been applied. Creating a freeze, lifting one, and creating an exception all accept an Idempotency-Key header for exactly that case, and a replay is served the original response verbatim.
Only two routes take no credential: GET /v1/plans and GET /v1/github/oauth/callback. They sit outside the organisation bucket, so they carry no rate headers at all. Do not size a retry budget off the price catalogue; read it from the first credentialled call you make. And lifting a freeze, though exempt from the per-principal bucket, still counts against the organisation’s ceiling. A thaw can answer 429 like any other write, and the answer is to wait out Retry-After, not to conclude that Fwozen is broken.
We do not print the numbers here, because they are per-plan operational settings rather than a promise. Read them from those headers, or from GET /v1/entitlements, which reports the current organisation’s limits. The shape of the whole API is in the API documentation.
identify yourself
Say what you are in the User-Agent.
Not enforced, and not a gate. No request is refused for the shape of its User-Agent. We ask because when something goes wrong at three in the morning, the difference between a useful log line and a useless one is usually this string.
User-Agent: acme-release-bot/2.1 (+https://acme.example/bots; [email protected])
User-Agent: my-agent/0.4 (llm-driven; [email protected])A product name, a version, and one way to reach whoever runs it. If the client is an LLM agent acting for a person, say so. It is the fastest way for us to tell an unusual traffic pattern from an incident, and it means the first email about it goes to you rather than to your organisation’s owner. Do not impersonate a browser.
never decide
Four decisions that are not an agent’s to make.
These are the same four lines that appear in /llms.txt and in the published skill, because an agent should meet them wherever it enters. Having the scope to do something is not the same as having the standing to decide it.
Never start or lift a freeze on your own judgement.
A freeze blocks every engineer in the organisation. A thaw unblocks a merge somebody deliberately stopped. Both need an explicit human instruction, and a reason is required on the record either way.
Never enable a repository a human did not name.
Private repositories are the billable unit. Enabling one silently is a charge nobody agreed to, so no route, tool, or command auto-enables. Ask, then enable exactly what was named.
Never read "the API was unreachable" as "not frozen".
The deploy gate fails closed on purpose. An error is an error; report it as one rather than resolving the ambiguity in the direction that lets a deploy through.
Never report a branch as protected because enrolment succeeded.
Enrolment and enforcement are two different facts. Read the verification fields and repeat what GitHub actually said, including when it said it could not tell you.
talk to us
Building something on this?
Tell us what you are building, especially if a limit here is in your way. An agent-shaped integration is the kind of thing we want to hear about before it breaks on a limit we could have raised. [email protected], or [email protected] if you found a way around one of the rules on this page.