Some tools read; some act — send a message, file a ticket, move money. Mark an acting tool protected and the framework refuses every call to it unless the session holds a grant for it. The model can request the action; only your application, on the person's behalf, can allow it. The credential behind the consent never enters the model's context.
When to use it
| The tool | Make it |
|---|---|
| Reads or gathers — search, fetch, read a file | Open (the default). Agents discover what a source covers by trying it. |
| Changes something outside the run, or leaks data somewhere | protected |
| Is safe in some sessions and not others | protected, and grant it in the sessions where it is safe |
Mark a tool protected
export class FileTicketTool extends Tool<{ title: string; body: string }> {
readonly name = "file_ticket";
readonly description = "Open a ticket in the team's tracker.";
readonly protected = true;
// …
}Called without a grant, the call is refused before it runs, and the agent reads in its place:
This action is protected and requires authorization that has not been granted for this session.The refusal is traced as tool:authReject, with the agent's full lineage. It is the first check every call meets, and no hook or harness.yml override can switch it off.
Hold the session's grants
Grants live in a GrantStore set on GrantStoreCtx. The scaffolded harness sets none, so every protected tool is refused until you provide one — failing closed is the default. In app.ts, set it after initializeHarness and before useExecution(), so every run inherits it:
import { GrantStoreCtx } from "@lloyal-labs/lloyal-agents";
import { createGrantStore } from "@lloyal-labs/rig";
const grants = createGrantStore(); // or createGrantStore(["file_ticket"]) to pre-grant
yield* GrantStoreCtx.set(grants);
const run = yield* useExecution();createGrantStore keeps grants in memory for the life of the session. A harness that needs durable or audited grants implements the four-method interface itself — has, grant, revoke, granted — against its own store.
Ask, then grant
Consent is your app's to ask for: a command from the surface, and a handler that records it. Hand grants to article.ts the way app.ts already hands it session.
export type Command =
// …
| { type: "grant"; tool: string }
| { type: "revoke"; tool: string };handlers: {
// …
*grant({ tool }) { yield* grants.grant(tool); },
*revoke({ tool }) { yield* grants.revoke(tool); },
},Permission is not acceptance
A grant means the session may call the tool. It does not mean every proposed call should run, or that the evidence for it is sufficient. Keep the other decisions where they belong:
- Refuse a particular call — a guard on the tool: Tool hooks and guards.
- Refuse to finish without evidence — an
onReturnhook. - Decide what becomes lasting state — the one place your harness commits, as in
article.ts.
Related
- Tools — the
protectedflag besidefanoutandhooks. - Abilities: the trust boundary — why protection is structural when tools run in-process.