Skip to content
Use Fungi

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.

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.

Use your owner key, not an App’s credentials. Set AGENT_ID to the Agent’s host-issued ID:

TERMINAL
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.

To approve write access, send the current revision as expectedRevision:

TERMINAL
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:

TERMINAL
npm install @fungi.computer/approvals @fungi.computer/watchdog

This disposable example stages a request without executing saveNote. Save it as approval.ts and run node approval.ts:

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.