Skip to content
Write an extension

Grant only what it needs

Separate what an extension asks for from what a host allows.

An extension requests capabilities by what it defines: prompt for prompt sections, state for collections, commands, run_tools for tools, projection, and so on. A host grants some or all of them. Until now the test host granted everything each extension asked for. A real host decides.

bookmarks/roster.ts
import type { ExtensionRosterV0 } from "@fungi.computer/shiitake/extensions";
import { bookmarks } from "./extension.js";
/** The host's decision: this roster entry, and exactly these capabilities. */
export const bookmarkRoster: ExtensionRosterV0 = {
roster: [bookmarks],
grants: [
{
id: bookmarks.manifest.id,
capabilities: ["state", "prompt", "commands", "run_tools", "projection"],
},
],
};

A roster lists the extensions a host loads and the capabilities it grants to each. Pass it as the extensions option of Shiitake.make, or of memoryHost when you want a test to use the real grants. Shiitake refuses a grant the extension never requested, and a grant for an extension that is not on the roster.

bookmarks/grants.ts
import type { ExtensionRosterV0 } from "@fungi.computer/shiitake/extensions";
import { memoryHost } from "@fungi.computer/shiitake/testing";
import { bookmarks } from "./extension.js";
/** A host that lets clients read bookmarks but nobody save them. */
export const readOnlyRoster: ExtensionRosterV0 = {
roster: [bookmarks],
grants: [
{
id: bookmarks.manifest.id,
capabilities: ["state", "prompt", "projection"],
},
],
};
/** A person tries to save a link on the read-only host. */
export async function readOnlySave() {
const host = await memoryHost({ extensions: readOnlyRoster });
try {
const session = host.agent.session("reading");
await session.send("Start.", { requestId: "start" });
await host.settle("reading");
return await session.extension(bookmarks.manifest.id).call("add", {
url: "https://fungi.computer/",
title: "Fungi",
});
} finally {
await host.close();
}
}
bookmarks/run-read-only.ts
import { readOnlySave } from "./grants.js";
const [outcome] = await Promise.allSettled([readOnlySave()]);
console.log(
outcome.status === "rejected"
? `Refused: ${String(outcome.reason)}`
: "Saved, which this host should have refused",
);
TERMINAL
node --import tsx bookmarks/run-read-only.ts
TEXT
Refused: ShiitakeError: Extension is unavailable

readOnlySave asks the read-only host to save a link through the command. Without commands, that call is refused. Without run_tools, the model is never offered bookmarks.add; a model that calls it anyway gets an error and nothing is saved. The extension’s code did not change. Removing a grant removes the authority; do not re-check grants inside the extension.

A host can also approve each tool call: the roster’s gateRunTools is asked before every run_tools call in a Session, and a refusal returns the host’s value without running the operation.

Next: Test it.