An AI agent permission should specify **who is calling, which connection they use, which resource they can reach, what the action changes, and who makes the final commit**. A tool catalog tells an agent which commands exist. The server still has to decide whether this caller may run this command on this object now, and the response must say what actually happened.

That distinction matters when a single interface can discover thousands of operations. On September 28, Cloudflare introduced [`cf`, an agent-oriented CLI](https://blog.cloudflare.com/cloudflare-cf-cli-launch/) for its API. The announcement makes command discovery easier; it does not turn discovery into permission. The [Hacker News discussion](https://news.ycombinator.com/item?id=49879577) shows why the release caught operators' attention, but comments there are questions and opinions, not a security assessment of the CLI.

For a SaaS product, the useful response is a contract for each consequential action. Kit's Hiring, security response, and team tools make the difference concrete: their verbs sound similar, but their effects differ sharply. A candidate reply can be drafted without being sent. A bounty can be proposed without being awarded. Other authorized calls change state immediately. You can inspect and test each decision.

## What did Cloudflare's `cf` launch change?

Cloudflare says its open-beta `cf` CLI exposes commands derived from an API with **more than 3,000 operations**, compared with roughly 280 Wrangler command paths. It returns JSON by default and offers `cf cli search`, so an agent can find a command from a natural-language description without carrying the full catalog in its context. Those are changes to **discovery and interface design**, as described in [Cloudflare's launch post](https://blog.cloudflare.com/cloudflare-cf-cli-launch/) and [`cf` repository](https://github.com/cloudflare/cf).

Cloudflare also reports that agents accounted for 48% of Wrangler use in the week before the announcement, up from 25% in March 2026. That is Cloudflare's measure of Wrangler use, with no public denominator or method in the post. It is not a customer adoption rate.

An agent can search for a read, a deployment, a WAF change, or a domain purchase through one command surface. That does not give it credentials for all those actions. Cloudflare's [API token documentation](https://developers.cloudflare.com/fundamentals/api/how-to/create-via-api/) defines policies by resources and permission groups; its [permission reference](https://developers.cloudflare.com/fundamentals/api/reference/permissions/) separates read and write permissions across user, account, and zone resources. Those docs explain available API controls. The launch announcement does not establish the authentication or approval behavior of every `cf` invocation.

That gap is the point for anyone exposing a large tool catalog: **discoverable is not authorized**. A command description can tell the agent how to ask. Only the system behind the command can decide whether the current caller may act on a particular object and state. The more commands an interface makes easy to locate, the more valuable it becomes to document that decision for each one.

## What does an AI agent permission need to specify?

An effective AI agent permission is a **resource-and-action contract**, not a single “read” or “write” badge. Before exposing a tool, write down its connection grant, current actor, object lookup, state checks, resulting effect, final committer, outward recipient, response visibility, and denial behavior.

Imagine a request to “send a reply to this applicant.” The connection may carry `hiring_write`, yet the member may be assigned to only one job. The applicant might belong to another job, the reply window might be closed, and the tool might only create a draft. Each question changes the answer to “may the agent do this?” A broad scope is necessary in that path, but it is not sufficient.

Start with these six questions:

1. **Who is acting?** Identify the human or service principal linked to the connection. Check their current account membership and role, not only the role they had when the connection was created.
2. **What was delegated?** Name the connection scope and the account or module it covers. A token with one module's write scope should not quietly reach another module.
3. **Which object is targeted?** Resolve the ID through a scope of objects this actor can see. A tenant-wide query may still be too broad for a restricted job or private report.
4. **What state permits the action?** A member who can update a candidate record may still be unable to reply after a workflow closes. Check the lifecycle at execution time.
5. **What is committed?** Distinguish a suggestion, an internal draft, a direct database change, a message to another person, and a payment. Tool names alone cannot carry this distinction.
6. **What can the caller see afterward?** A response can expose private counts, names, or records even when the write itself was allowed. Apply the viewer's read boundary to the result.

These questions make “human approval required” testable: who approves, in which channel, and which endpoint enforces it?

## Why are connection scopes only the first check?

A connection scope limits the range of operations a client may request. It cannot answer whether a member can work on **this particular record**. The server has to intersect the connection grant with current membership, tenant, record visibility, policy, and lifecycle.

Kit's external MCP connection uses OAuth scopes such as `hiring_read`, `hiring_write`, `csirt_read`, `csirt_write`, and `team_write`. Its consent flow limits requested scopes to ones the current account member can grant. A base `mcp` scope alone grants no product module. On a tool call, Kit checks the token scope again, along with current module access; hiding a tool from the discovery list is only a usability measure. A client can still submit a tool name it never saw listed. See Kit's [guide to connecting AI assistants](/docs/connecting-ai-assistants) for the user-facing connection flow.

For an application, Kit then looks under job postings visible to that member and applies the application policy. An application in a restricted posting comes back as not found when the member cannot access that posting; a visible record may instead receive a policy denial for the requested action. Security reports have a comparable member-visible lookup, though individual actions have different role and policy checks. Tenant context narrows the account; object lookup narrows the target.

This gives you a useful negative test. Connect with module-wide `hiring_write` as a non-admin member. Ask the tool to update an application in a restricted posting that member cannot access. The expected result is not found, without revealing whether the application exists. Then lower that member's module access and retry over the existing connection. Kit's call-time access check makes the next call respect the current role. It does not erase information the client already received or undo an earlier committed action.

Cloudflare's [API token restrictions](https://developers.cloudflare.com/fundamentals/api/how-to/restrict-tokens/) illustrate a separate layer: tokens can have time and client-IP restrictions. Those reduce the period or location in which a credential can be used. They do not replace the resource-specific policy of a SaaS application sitting behind a tool call.

## What happens when an agent drafts a candidate reply?

Kit's `hiring_send_message` name sounds like an outward action. The implemented effect is narrower: an allowed call **stages a pending reply** for a teammate to review and send from the application's email thread. The tool response says so. The server's draft state, rather than an instruction asking the model to confirm its wording, enforces that boundary.

The call requires a linked account member with `hiring_write`, finds the application through that member's visible job postings, checks permission to update it, and checks whether a reply can be staged in the thread's current state. A successful result creates an internal draft and supplies a link to the thread. The web confirmation action is the point where the candidate-facing email is sent.

That draft still matters. It can carry sensitive or misleading language. It may influence the teammate who later presses send. But the immediate result is **“pending draft,” not “candidate notified.”** A tool response that says merely “Done” would hide the most important fact in the workflow.

Write the contract as two actions: *stage reply* and *send reply*. For the first, the agent can be the committer once the record checks pass; the recipient is still internal. For the second, the teammate is the committer in the web UI and the candidate becomes the recipient. If you choose a different product design, name that choice just as explicitly. The connection scope by itself cannot tell a reader which action occurred.

The same distinction applies to a CRM tool called `send_quote`: it might save a quote for review, enqueue an email, or transmit it immediately. Verify the persisted and delivered state before naming the effect.

## How does a bounty proposal differ from an award?

Kit's security response tools show why “write” is too coarse. `csirt_propose_bounty` creates an **internal proposed amount** on a report visible to the member. It does not create an award, ledger entry, researcher notification, or payment. A later proposal can replace the open proposal and make earlier votes stale. The proposal is a real write, but its audience and consequence are bounded.

Team members can use `csirt_vote_bounty_proposal` to record an advisory position. In blind-voting mode, the response must not leak the hidden tally through its prose while concealing it in structured data. This is a reminder to check **response visibility** alongside write authorization: a permitted vote does not automatically entitle the caller to everyone's votes.

`csirt_approve_bounty` requires CSiRT module admin access and a `csirt_write` connection. An authorized call directly records an award decision in Kit, subject to the report's award rules. Its description asks the assistant to confirm the amount and kind with the user, but that text is client guidance, not a second server-side gate. Approval does **not transfer funds**; payment takes place outside Kit.

“Human in the loop” needs a precise meaning. For a proposal, the decision is still ahead. For the approval tool, the authorized administrator's call is the final Kit commit. If your policy needs a second approval for an award, build a distinct server-side transition. A conversation transcript that says “yes” cannot substitute for one.

Compare the effect named in the tool description with the result. “Award approved in Kit” leaves payment status separate. A proposal response should never suggest the researcher has been promised money.

## When must team access leave the chat?

Changing a colleague's access has consequences beyond one workflow. Kit treats this differently depending on the channel. In its **in-app AI chat**, the adapter blocks team invitations, access changes, invitation edits and revocations, and member removal. It routes the user to authenticated settings instead. A model-visible “yes, I approve” in chat does not make the chat a trusted access-granting surface, especially when the model may have read candidate-controlled documents.

The **external OAuth MCP path has a separate contract**. An account admin can receive `team_write` and use `team_update_member_access` to change a member's role or module levels directly. The tool looks up the member in the account and protects the account owner from being re-roled or demoted. `team_invite_member` can send an invitation or add an existing user to a job, depending on its inputs, after its own admin and account checks. These are direct actions, not settings redirects.

That difference should be visible in any permissions review. “Kit chat cannot change team access” is accurate for the in-app adapter. “No agent can change team access” is not. External clients with an authorized grant can reach a different committing path. If your product has a web assistant, an API, and a remote tool server, write a separate row for each channel that can perform the same named action.

The endpoint matters more than a model's promise to ask first. A settings redirect enforces a channel boundary. An external admin tool enforces a role boundary through its scope and admin checks. Both need more precise names than “approval.”

## What should your resource-and-action checklist include?

Use one row per **actual effect**, even when a product presents several effects under one friendly verb. Fill the row from code and tests, then verify it with a permitted and denied call. Here is the compact version for six Kit actions:

| Action | Grant and actor | Resource and state fence | Effect and final committer | Boundary to state plainly |
| --- | --- | --- | --- | --- |
| Read application summary | `hiring_read`; linked member | Application under a visible posting | Returns candidate context and notes; no write | The result contains sensitive hiring data. |
| Draft candidate reply | `hiring_write`; linked member allowed to update the application | Application under a visible posting; reply lifecycle allows staging | Pending internal draft; teammate sends in the web UI | The candidate has not received an email. |
| Advance application | `hiring_write`; member allowed to advance | Visible application; valid next or chosen stage | Stage changes directly; notifications are handled by the workflow | No separate approval step in this tool. |
| Propose bounty | `csirt_write`; member with CSiRT access | Member-visible report; program and amount rules | Internal proposal recorded by the tool | No award, notification, or payment. |
| Approve bounty | `csirt_write`; CSiRT module admin | Report in the account program; award rules | Award recorded in Kit by the authorized tool call | No funds transfer; model confirmation text is not an extra server gate. |
| Change team access | `team_write`; external MCP account admin | Account member; owner protection | Member access changed directly by external MCP | In-app chat instead routes to settings. |

For each row in your own system, add three columns that may not fit in a product-facing summary: **outward recipient**, **result visibility**, and **evidence of the decision**. An email has a recipient. A private report summary has viewers. A failed call needs a denial reason that helps the authorized user without revealing an inaccessible record. These details often expose a boundary that a scope table misses.

Then run four checks against the implementation:

1. **Bypass discovery.** Call a tool name directly even when it was omitted from the catalog. The server must still enforce its scope and role checks.
2. **Cross the object fence.** Use a valid-looking ID from another tenant or a restricted object in the same tenant. Both paths should fail without leaking private existence or content.
3. **Change access mid-connection.** Reduce the member's role and repeat the call. The next call should use current authority. Treat previously downloaded data, cached results, and signed URLs as separate revocation questions.
4. **Read the response literally.** Check that “draft,” “proposal,” “award,” “sent,” and “paid” match persisted state and outward effects. A result summary is part of the security contract because people may act on it.

Evidence needs precise language too. Kit labels some tools as destructive or outward-facing for MCP clients, and emits structured events for **open-world tool calls**. Those annotations inform client presentation; they do not add a server-side approval step. The events do not establish an immutable audit log for every tool invocation. Kit also counts external MCP tool calls against an account budget, including calls inside a batch. That limits use but does not authorize an object. Define your audit requirement, then verify that the committing endpoint records enough evidence to meet it.

Revocation is similarly bounded. A current-role check can stop a future call on an existing connection. It cannot unsend an email, reverse a committed access change, recall data already copied into an agent's context, or make an issued signed URL vanish. List each of those artifacts if your workflow creates them, and define its own lifetime or revocation path.

## How does Kit put this contract to work?

Kit's useful pattern is to make the **object and final effect** explicit. A delegated MCP scope opens a module, current member and tenant checks narrow the caller, and the tool's record lookup narrows the target. After that, the action can stage, propose, redirect, or commit directly. You can inspect the connection flow in [Kit's AI assistant guide](/docs/connecting-ai-assistants) and the broader recruiting use case in [MCP for Hiring](/blog/mcp-for-hiring).

Choose one consequential tool, fill out the checklist, and test its denied object, changed role, and final response. If you are connecting an assistant to Kit, review the specific workflows your team needs before granting write scopes. The goal is an agent that can tell you what it changed, on whose authority, and what still needs a person to act.