Building

Manifest reference

Every key of globalThis.manifest, what it does, and how it is validated.

globalThis.manifest is one optional, plugin-wide object. It carries what the platform can't learn by reading your handlers: which hosts you call, what settings the user fills in, which agents you'll create, and which provider you register as.

globalThis.manifest = {
  egress: ["api.example.com", "api.example.com:8443", "*.discord.gg"],
  settings: [{ key: "apiKey", title: "API key", type: "secret", required: true }],
  agents: [{ key: "oracle", name: "Oracle" }],
  modelProvider: { kindId: "my-vendor", displayName: "My Vendor" },
  installOwnedConnection: true,
  permissions: ["agentApi:agents.write"],
};

Assign it at the top level as a plain JSON value. It is validated strictly: an unknown key — a typo such as egres — fails the publish with "unknown globalThis.manifest key 'egres'". Many plugins need no manifest at all.

Key Needed when Section
egress your code calls host.fetch, or opens a live connection egress
settings users must configure something (an API key, a toggle) settings
agents your plugin creates agents with host.agents.create agents
modelProvider you register any modelProvider.* handler modelProvider
installOwnedConnection your live connection belongs to the install, not to a channel installOwnedConnection
permissions rarely — a host.* call the scanner can't see permissions

egress

string[] — the hosts host.fetch may reach. Outbound HTTP is deny-by-default: a request to any host not on this list (or on an installer-provided hosts setting) is refused. Publishing code that calls host.fetch with an empty egress fails: "the code calls host.fetch but declares no egress hosts".

Entry Matches
api.example.com https://api.example.com/… and http://api.example.com/… on the default port (443 / 80) only
api.example.com:8443 that host on port 8443 only
*.discord.gg any subdomain (gateway.discord.gg, a.b.discord.gg) — never the bare discord.gg
*.example.com:9000 any subdomain on port 9000

No scheme, no path, no trailing slash. A wildcard needs at least two labels after *.. Matching is case-insensitive.

The installer sees these hosts under What it can reach before installing, and a later version that adds a host waits for their OK. Examples: Brave Search declares ["api.search.brave.com"]; Discord declares ["discord.com", "gateway.discord.gg", "*.discord.gg", "cdn.discordapp.com", "media.discordapp.net"].

settings

An array of fields that become the plugin's settings form in the console. Values reach your code through host.settings.get(key); secrets through await host.settings.getSecret(key).

// plugins/discord/index.ts
settings: [
  { key: "botToken", title: "Bot token", type: "secret", required: true, scope: "channel" },
  {
    key: "readMessageContent",
    title: "Use the Message Content intent",
    type: "bool",
    default: true,
    scope: "channel",
    description: 'Needs "Message Content Intent" on in the Developer Portal. DMs and @mentions work without it.',
  },
  { key: "respondToBots", title: "Answer other bots", type: "bool", default: false, scope: "channel" },
],
Field Type Rules
key string required; ^[a-zA-Z][a-zA-Z0-9_]{0,63}$; unique
title string the label in the form
description string help text under the field
type "text" (default), "secret", "bool", "int", "enum", "hosts" see below
required boolean the user must fill it in
default string / number / boolean must match the type; not allowed on secret
options string[] required (non-empty) for enum, not allowed otherwise; default must be one of them
scope "install" (default) or "channel" channel = filled in per connected channel, never inherited from the install

Any other field key refuses the publish.

Types:

  • text → a string. int → a number. bool → a boolean. enum → one of your options.
  • secret → write-only in the console, stored encrypted, never returned to the browser. Read it with await host.settings.getSecret(key); host.settings.get never returns secrets.
  • hosts → up to 10 host[:port] values (no wildcards) that are added to this installation's egress allowlist. Use it when the user picks the server — a self-hosted instance, a home gateway. host.settings.get returns it as a JSON array encoded in a string: JSON.parse it.
// infrastructure/plugins/hello-live/index.ts
function gatewayHost(): string {
  const raw = host.settings.get("gatewayHost");
  const hosts = typeof raw === "string" ? (JSON.parse(raw) as string[]) : [];
  if (!hosts[0]) {
    throw new Error("hello-live: no gatewayHost configured");
  }
  return hosts[0];
}

host.settings.get on a key you didn't declare throws — it's a bug in your plugin, not a missing value. Defaults apply until the user saves a value.

Scope channel is for per-bot credentials: the official Discord plugin needs one bot token per connected channel, so every Discord field is scope: "channel" and is entered on Channels → Connect a channel.

agents

{ key, name }[] — the agents your plugin will create with host.agents.create. Declaring them up front lets the console say "Adds 7 agents" and check the account has room on its plan before install, instead of your setup hook discovering it half-way.

// infrastructure/plugins/oh-my-openagents/index.ts
globalThis.manifest = {
  agents: CREW.map((member) => ({ key: member.key, name: member.name })),
};
  • At most 20 entries. key matches ^[a-z0-9][a-z0-9_-]{0,63}$ and is unique; name is 1–100 characters.
  • Use the same key you pass to host.agents.create. See Agent crews.
  • A new version that declares an extra agent counts as new access and waits for the owner's OK.

modelProvider

Required when you register any modelProvider.* handler, and forbidden when you don't. It makes your plugin a selectable provider kind under AI Providers → Add provider, shown as "displayName from plugin".

// infrastructure/plugins/openai-voice/index.ts
modelProvider: { kindId: "openai-voice", displayName: "OpenAI Voice" },
Field Rules
kindId required; ^[a-z][a-z0-9-]{1,63}$ (2–64 characters); built-in kind ids such as openai-compatible, anthropic, fish-audio, elevenlabs, deepgram are reserved
displayName optional, ≤ 100 characters
description optional, ≤ 500 characters
mediaSupport optional; any of "image", "audio" — the media your chat models accept. The platform strips media you don't support from the request before calling you.

See Model providers and Voice and speech-to-text.

installOwnedConnection

boolean. Set true when your connection.* handlers serve a connection that belongs to the installation rather than to a chat channel — for example a smart-home event stream that fires schedules. Declaring it without connection.* handlers fails the publish. See Live connections.

permissions

string[] — an escape hatch. Permissions are normally derived from your code. If you reach a host member in a way the scanner can't see (through an alias or a computed property), list the permission here. Values must come from the known set; anything else fails the publish:

agentApi:session.read, agentApi:conversation.read, agentApi:message.post, agentApi:todo.read, agentApi:todo.write, agentApi:subagent.spawn, agentApi:peer.send, agentApi:notify.write, agentApi:session.read.cross, agentApi:agents.read, agentApi:agents.write, agentApi:models.read, host:workspace:read, host:workspace:write, host:media:transcode, host:session:sendError, host:ssh:command, host:connection:send.

The better fix is almost always to call host.x.y(...) literally.