Approve an AI agent's actions before they run
Approve an Oyster Agent's App access, or use ApprovalLedger to stage a write, collect a human decision and recheck authority before dispatch.
A proposed write should not become an executed write merely because an agent asked for it. Record what it wants to do, collect an eligible person’s decision, and check the requester’s current authority again before dispatching.
There are two different boundaries here. On Oyster, the owner approves an Agent’s App-tool access and the installed Apps’ permissions. For a host you are building, the Approvals library supplies the durable record for an individual proposed action. A blanket tool grant is not a prompt before every call.
Approve App access on Oyster
Section titled “Approve App access on Oyster”An App with agent.manage can create Agents and request tool-policy changes,
but those requests remain pending until the owner directly approves them.
Creating an Agent does not grant it write abilities.
1. Read the Agent’s current policy
Section titled “1. Read the Agent’s current policy”Use your owner key, not an App’s credentials. Set AGENT_ID to the Agent’s
host-issued ID:
curl -H "Authorization: Bearer $OYSTER_OWNER_KEY" \ "$OYSTER_URL/api/agent-policies/$AGENT_ID"Read the current revision before changing the policy. The Agent must belong to this host and tenant; an unknown or unpinned Agent cannot acquire access by supplying an ID.
2. Approve the tool level deliberately
Section titled “2. Approve the tool level deliberately”To approve write access, send the current revision as expectedRevision:
curl -X PUT -H "Authorization: Bearer $OYSTER_OWNER_KEY" \ -H 'content-type: application/json' \ -d '{"level":"write","expectedRevision":"<the current revision>"}' \ "$OYSTER_URL/api/agent-policies/$AGENT_ID"Use JSON null for expectedRevision only when no policy exists. A stale
revision returns 409; reread and review the current state rather than
overwriting it blindly. These are host-owned routes, and they reject an App
installation header.
The host records who approved the policy and that member’s current membership version. Demotion, removal or suspension and their later reversal require new approval; restoring a role does not revive the old approval.
3. Review the installed App’s permissions too
Section titled “3. Review the installed App’s permissions too”The Agent’s policy does not replace installation consent. An App receives only the operations and resources approved for its installed Release. Each method receives only the bindings declared in its requirements.
Consent is for capabilities, not every individual call. An upgrade may add a method using an already approved capability without a new prompt, but a new capability needs approval. Release selection intersects existing approvals; it does not silently add grants.
See Use Apps for reviewing App permissions in Settings. For the underlying policy and MCP contract, see Oyster.
Do not mistake a named key for per-call approval
Section titled “Do not mistake a named key for per-call approval”A named apps.use key carries the owner’s stored approval for its admitted
Apps. An all-Apps key can read and write every App on that Oyster, including
Apps installed later, without another prompt. Revoke the key to stop that
access. Its grants are immutable; narrowing them means revoking and reissuing.
Other MCP writes, including the owner’s own calls, require explicit approval through a client supporting form elicitation. If you need to review each proposed action, do not rely on the broad key in the cloud sandbox guide to ask again before each write.
Stage an individual write in your TypeScript host
Section titled “Stage an individual write in your TypeScript host”The Approvals library separates staging, answering, applying and result delivery. Your host supplies its native SQLite owner, requester identity, current grants and actual operation dispatch.
Install the packages in an ESM project with Node 24:
npm install @fungi.computer/approvals @fungi.computer/watchdogThis disposable example stages a request without executing saveNote. Save it
as approval.ts and run node approval.ts:
import { DatabaseSync } from "node:sqlite";import { fromNodeSqlite } from "@fungi.computer/watchdog/node-sqlite";import { ApprovalLedger } from "@fungi.computer/approvals";
const database = new DatabaseSync(":memory:");try { const ledger = new ApprovalLedger({ owner: fromNodeSqlite(database), home: { kind: "team", id: "example-team" }, now: Date.now, }); const staged = await ledger.request({ requester: { principalId: "agent:example", agentId: "example-agent", sessionId: "example-session", displayName: "Example", }, module: "notes", operation: "saveNote", access: "write", reversibility: "reversible", authorityFence: "grant-generation:1", idempotencyKey: "turn:1/tool:1/operation:0", inputJson: '{"text":"hello"}', }); if (staged.kind !== "record") throw new Error(staged.reason); console.log(staged.record.state); // "pending"} finally { database.close();}Expect pending, not a saved note. This example uses a Team home, whose default
auto-approve policy is OFF. Local and solo Oyster homes default ON. request
stages a record and records the policy decision without dispatching; submit
can also apply a newly policy-approved request inline through the authority
recheck.
The identity, authority fence and idempotency key above are illustrative host-attested values. Do not accept them from an Agent or browser. Real requests need a persistent, host-owned database, not this in-memory one.
Collect a decision, recheck, then dispatch
Section titled “Collect a decision, recheck, then dispatch”Integrate the lifecycle through the host’s existing doors:
| Step | Host responsibility |
|---|---|
| Stage | Bind the request to the real requester and stable operation identity. A pending tool result contains a record ID and description; no effect runs. |
| Answer | Resolve an eligible human member freshly and pass answer the expected record revision. Do not trust an actor supplied in the request body. |
| Recheck | Give apply an ApprovalApplyHost whose recheck reads the requester’s current grants and exact authority fence. |
| Dispatch | Call the existing operation door from dispatch only after recheck authorizes it. |
| Handoff | Send a deferred result through the ordinary Agent new-message path, deduplicating with the stable deliveryId. |
An answer does not widen the requester’s authority. A revoked grant or changed fence can still refuse an approved request. Likewise, auto-approve records a policy decision; it does not override revoked authority.
Keep the same idempotency key for a retry of the same intended operation. The
ledger records the claim and result rather than blindly redispatching an
uncertain effect. An unknown outcome remains outcome_unknown, not a denial or
permission to retry the write.
A pending approval returns immediately; it does not suspend the Agent turn.
Deferred completion is a later message, not a replay of an unsafe execute
call. Unanswered requests expire after 24 hours by default; expiry never
approves.
This is a library integration contract, not evidence that every Oyster or Team surface has a per-call approval queue. Your host must actually wire the decision, apply and result-delivery work. The Approvals documentation describes the current implementation boundary and its public consumer example.
For local commands instead of App tools, see Sandbox a coding agent on Linux and macOS.