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.
Write the roster
Section titled “Write the roster”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.
Withhold a capability
Section titled “Withhold a capability”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(); }}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",);node --import tsx bookmarks/run-read-only.tsRefused: ShiitakeError: Extension is unavailablereadOnlySave 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.