Building

Host API reference

Every member of the host object, its arguments, its result, and when it is present.

View as Markdown

host is the only way plugin code reaches anything outside the sandbox. It's a global — never import it, never alias it (const a = host.agents hides the call from the permission scan).

Presence and permission. host.workspace, host.media, host.ssh, host.connection, host.llm and host.session.llm are undefined when you weren't granted them or the invocation has no context for them. The platform-API members (host.session, host.agents, host.models, host.cronJobs, host.plugins, host.license) are present in tool, middleware, lifecycle, compaction and web-provider runs, and each call needs its permission or is rejected with a 403. Use host.x!.y() when you know it's there, if (host.x) when it may not be.

Ids are strings. Every id (session, agent, todo, message, notify, model, installation…) is a string, because the underlying 64-bit numbers don't fit a JavaScript number. Compare and store them as strings.

Errors. Host calls reject with an Error whose message starts with a code, e.g. "rate_limited: …", "bad_arguments: …", "agent op agents.update failed: 404". Failures of host.session, host.agents, host.models, host.cronJobs, host.plugins and host.license also carry the HTTP status as a numeric error.status.

Member Needs Section
host.log — log
host.settings — settings
host.fetch manifest.egress fetch
host.oauth — oauth
host.crypto — crypto
host.license — license
host.session an agentApi:* permission per operation session
host.agents agentApi:agents.* agents
host.cronJobs agentApi:cronJobs.* cronJobs
host.plugins agentApi:plugins.* plugins
host.models agentApi:models.read models
host.workspace host:workspace:* + an agent in context workspace
host.media host:media:transcode media
host.session.llm, host.llm, host.decide context models from plugin code
host.ssh host:ssh:command, SSH hooks only ssh
host.connection host:connection:send, a live connection you own connection

host.log

host.log(level: "debug" | "info" | "warn" | "error", message: string): void

Fire-and-forget. Lines appear in the logs panel of the installed plugin's page for 24 hours.

host.settings

host.settings.get(key: string): string | number | boolean | null
host.settings.getSecret(key: string): Promise<string>
  • get returns a non-secret setting's value (its default until the user saves one). A hosts field returns a JSON array encoded as a string — JSON.parse it. A key with no saved value and no default, or one you didn't declare, returns null.
  • getSecret returns a secret field's value. It is fetched only when you call it. It throws MissingCredential: … for a key you didn't declare or the user hasn't set.
  • In a model provider handler these read the provider the user added; in a channel handler, scope: "channel" fields read that channel's values.

host.fetch

host.fetch(request: {
  method?: string;                          // default "GET"
  url: string;                              // http or https, host must be allowed
  headers?: Record<string, string>;
  body?: string;
  bodyEncoding?: "utf8" | "base64";         // "base64" to send binary
  responseEncoding?: "utf8" | "base64";     // "base64" to receive binary (audio, images)
  oauth?: string;                           // a manifest.oauth key: the platform adds the Bearer token
  account?: string;                         // host.oauth.accounts() id; required when several are connected
}): Promise<{ status: number; headers: Record<string, string>; body: string }>
  • Only hosts in manifest.egress or a hosts setting (rules).

  • 60 requests per minute per installation, 10 s timeout, 5 MB response.

  • Any HTTP status resolves — check status. Redirects are followed (at most 5), and every hop must also be an allowed host; with oauth they are not followed and a 3xx comes back to you.

  • The call rejects with an error whose name says why, so you can branch on it:

    error.name When error.message
    EgressDenied the host is refused, or is a private, loopback, link-local or metadata address host not allowlisted: <url> or blocked address for <host>
    RateLimited over the rate limit fetch rate limit exceeded
    FetchTimeout it timed out fetch timed out
    ResponseTooLarge the response is too big response exceeded <n> bytes
    TooManyRedirects more than 5 redirects more than 5 redirects
    Internal anything else went wrong with the request fetch failed: <reason>
  • With oauth, the URL host must be in that key's apiHosts, any Authorization header you set is replaced, and a 401 triggers one token refresh and retry. You never see the token. Errors have err.name of OAuthNotConnected, OAuthAccountRequired, OAuthReconnectRequired or OAuthUnavailable.

host.oauth

host.oauth.accounts(key?: string)
  : Promise<{ id: string; key: string; label: string | null; scopes: string[]; status: "connected" | "needsReconnect" }[]>

Always present. The sign-ins the customer connected for this installation, for one manifest.oauth key or all of them; [] when none. Pass an id as account to host.fetch when more than one is connected for a key.

host.crypto

All synchronous. Encodings: "utf8" | "hex" | "base64" | "base64url" (inputs default to utf8; digests are hex | base64 | base64url). Failures throw BAD_REQUEST: host.crypto.<op>: <reason>; the verify operations return false / { valid: false } instead of throwing. Budget: 200 operations and 8 MB of input per invocation.

host.crypto.hmacSha256({ key, keyEncoding?, data, dataEncoding?, outputEncoding? }): string
host.crypto.hmacSha1({ key, keyEncoding?, data, dataEncoding?, outputEncoding? }): string
host.crypto.sha256({ data, dataEncoding?, outputEncoding? }): string
host.crypto.sha1({ data, dataEncoding?, outputEncoding? }): string
host.crypto.aesCbcDecrypt({ key, keyEncoding?, iv, ivEncoding?, data, dataEncoding?,
                            padding?: "pkcs7" | "none", outputEncoding? }): string   // 16/24/32-byte key, 16-byte iv
host.crypto.ed25519Verify({ publicKey, publicKeyEncoding?, signature, signatureEncoding?, data, dataEncoding? }): boolean
host.crypto.ed25519Sign({ seed, seedEncoding?, data, dataEncoding?, outputEncoding? }): string
host.crypto.rsaSha256Sign({ privateKey, data, dataEncoding?, outputEncoding? }): string
  // RSASSA-PKCS1-v1_5 + SHA-256 (JWT "RS256"). privateKey: PEM, PKCS#1 or PKCS#8, line breaks optional. At most 10 per run.
host.crypto.verifyJwtRs256({ token, jwks: { keys: object[] }, issuer: string | string[], audience: string | string[] })
  : { valid: true; header: object; payload: object } | { valid: false; reason: string }  // 60 s clock skew
host.crypto.timingSafeEqual(a: string, b: string): boolean
host.crypto.base64Encode(text: string): string
host.crypto.base64Decode(base64: string): string
host.crypto.encode({ data: string; from: Encoding; to: Encoding }): string

Verify a webhook signature over the exact bytes the vendor sent:

// infrastructure/plugins/hello-channel/index.ts
const expected = host.crypto.hmacSha256({ key: secret, data: request.rawBodyBase64, dataEncoding: "base64", outputEncoding: "hex" });
if (!host.crypto.timingSafeEqual(signature, expected)) {
  return tools.reject("bad signature");
}

Sign a GitHub App JWT (RS256) with the app's private key:

const b64 = (value: object) => host.crypto.encode({ data: JSON.stringify(value), from: "utf8", to: "base64url" });
const now = Math.floor(Date.now() / 1000);
const unsigned = `${b64({ alg: "RS256", typ: "JWT" })}.${b64({ iss: appId, iat: now - 60, exp: now + 540 })}`;
const signature = host.crypto.rsaSha256Sign({ privateKey: await host.settings.getSecret("privateKey"), data: unsigned, outputEncoding: "base64url" });
const jwt = `${unsigned}.${signature}`;

Derive a short idempotency key:

// plugins/discord/index.ts
host.crypto.sha256({ data: `${deliveryId}:${index}`, outputEncoding: "hex" }).slice(0, 25);

host.session

Acts on the chat the invocation belongs to. Present in tool, middleware, lifecycle, compaction and web-provider runs; each operation requires its own permission (a call to one that wasn't derived rejects with a 403). sendError needs host:session:sendError and is present only with that. Operations on "your" session fail in invocations that have none (for example pluginInstalled).

host.session.get(): Promise<{ id: string; agentId: string; state: string }>

agentApi:session.read — your session, with its effective state.

host.session.channelChat(): Promise<{ chat: { channelId: string; chatId: string; targetAgentId: string | null } | null }>

agentApi:session.read — the channel chat your session belongs to (any channel kind), or chat: null for a console session or one with no chat. targetAgentId is the agent the channel points at, null if it points at none. The ids are the ones list_channels and host.cronJobs deliverTo use.

host.session.conversation.read(args?: { sessionId?: string; limit?: number; cursor?: string })
  : Promise<{ messages: ConversationMessage[]; nextCursor: string | null }>

interface ConversationMessage {
  id: string; role: string; messageType: string; content: string;
  senderSessionId: string | null;        // set when another agent's session sent it
  senderAgentName: string | null;
  channelSender: { displayName: string; handle: string | null; isVerified: boolean; isSharedConversation: boolean } | null;
  senderUserName: string | null;         // which console user typed it
  createdAt: string;
}

agentApi:conversation.read. Newest limit messages first page; pass nextCursor back as cursor for older pages (null = no more). Text lines only. Passing another sessionId also needs agentApi:session.read.cross; an unknown or foreign id returns an empty page.

host.session.message.post(args: { text: string }): Promise<{ messageId: string }>

agentApi:message.post — post into your session.

host.session.todo.create(args: { texts: string[] }): Promise<{ todos: { id: string; text: string }[] }>   // todo.write
host.session.todo.list(): Promise<{ todos: { id: string; text: string }[] }>                             // todo.read
host.session.todo.update(args: { todoId: string; text: string }): Promise<{ id: string; text: string }>  // todo.write
host.session.todo.complete(args: { todoId: string }): Promise<void>                                      // todo.write — marks done
host.session.todo.delete(args: { todoId: string }): Promise<void>                                        // todo.write — removes

Open todos keep the agent working turn after turn. A missing or foreign todo rejects (404).

host.session.notify.schedule(args: { message: string; delaySeconds: number })
  : Promise<{ notifyId: string | null; fireAt: string | null; isCanceled: boolean; cancelReason: string | null }>

agentApi:notify.write — wake this session later with a note. Another plugin's middleware may rewrite, delay or cancel it; a blanked note comes back { notifyId: null, isCanceled: false }.

host.session.subagent.spawn(args: { targetAgentId?: string; task: string; isFireAndForget?: boolean })
  : Promise<{ outcome: "created" | "forbidden" | "depthExceeded"; sessionId: string | null }>

agentApi:subagent.spawn — start a subagent; never blocks. targetAgentId omitted = a copy of the current agent. isFireAndForget: true = no report expected. Same reach and depth rules as the start_subagent tool.

host.session.peer.send(args: { sessionId: string; text: string })
  : Promise<{ isDelivered: boolean; isCanceled: boolean; cancelReason: string | null }>
host.session.peer.get(args: { sessionId: string }): Promise<{ id: string; agentId: string; state: string }>

agentApi:peer.send / agentApi:session.read.cross. send delivers into another visible session in the account and wakes it (fires interAgentMessage); not delivering is data, not an error. get works without a session of your own; a closed session is readable but not messageable; a foreign id is "not found".

host.session.sendError?(message: string): Promise<void>

host:session:sendError — report a failure into the invoking session. At most 5 per invocation. Stays available even when the hop-depth fuse withholds the rest.

Hop-depth fuse

When plugin code sends a message that triggers plugin code that sends a message… each generation is a hop. Past 4 hops from one original event, the platform-API members (host.session operations, host.agents, host.models, host.cronJobs, host.license) are absent for that generation; host.session.sendError and host.session.llm stay. Normal use — reacting once to one event — is one hop.


host.agents

Your installation's own agents. Each operation needs agentApi:agents.read or agentApi:agents.write; works without a session. Never reaches an agent your installation didn't create (those are "not found").

host.agents.create(args: {
  key: string; name: string; description?: string; modelId?: string; soul?: string; instructions?: string;
  isSubagent?: boolean; parentAgentId?: string | null; isGroupOnlyCallable?: boolean;
  replyMode?: "implicit" | "auto" | "all"; thinkingLevel?: "off" | "low" | "medium" | "high";
  tools?: string[];
}): Promise<PluginAgent & { wasCreated: boolean }>                         // agents.write — get-or-create by key (key 1..200 chars)

host.agents.list(): Promise<{ agents: PluginAgentListItem[] }>             // agents.read — match on `key`

host.agents.update(args: {
  agentId: string; name?; description?; modelId?; soul?; instructions?; enabled?: boolean;
  isSubagent?: boolean; parent?: { parentAgentId: string | null };
  isGroupOnlyCallable?; replyMode?; thinkingLevel?; tools?: string[];
}): Promise<PluginAgent>                                                   // agents.write — omitted = unchanged

host.agents.delete(args: { agentId: string }): Promise<void>               // agents.write — 409 if it's the account default

interface PluginAgent {
  id: string; slug: string; name: string; description: string; modelId: string | null;
  soul: string; instructions: string; enabled: boolean;
  isSubagent: boolean; parentAgentId: string | null; isGroupOnlyCallable: boolean;
  replyMode: "implicit" | "auto" | "all"; thinkingLevel: "off" | "low" | "medium" | "high";
  unresolvedTools: string[];             // names in `tools` that matched no installed tool
}
interface PluginAgentListItem {
  id: string; slug: string; name: string; description: string; modelId: string | null; enabled: boolean;
  key: string | null; isSubagent: boolean; parentAgentId: string | null; isGroupOnlyCallable: boolean;
}

tools limits the agent to exactly those tools: a built-in by bare name (web_search), your own tool by bare name, another plugin's tool by its full slug__tool name. Passing it makes the agent's tool list managed by your plugin: the console shows it read-only, the user can no longer change it, and every update({ tools }) replaces the whole list. tools: [] means no tools. Names that match nothing come back in unresolvedTools and are not granted; re-apply tools in your setup on sessionStarted and pluginUpdated. On a create whose key already exists, tools is ignored like every other field. Omit tools on update to leave the list alone.

Guide: Agent crews.

host.cronJobs

Schedules your installation owns, addressed by your own key (^[a-z0-9][a-z0-9_-]{0,63}$).

host.cronJobs.create(args: {
  key: string; agentId: string; name: string; prompt: string;
  cron?: string; eventSourceId?: string;                  // exactly one
  deliverTo?: { channelId: string; personId?: string; chatId?: string };
  enabled?: boolean;
}): Promise<PluginCronJob & { wasCreated: boolean }>                       // cronJobs.write — get-or-create by key

host.cronJobs.list(): Promise<{ cronJobs: PluginCronJob[] }>               // cronJobs.read
host.cronJobs.update(args: { key: string; name?: string; prompt?: string; cron?: string;
  deliverTo?: { channelId?: string; personId?: string; chatId?: string }; enabled?: boolean }): Promise<PluginCronJob>  // cronJobs.write
host.cronJobs.delete(args: { key: string }): Promise<void>                 // cronJobs.write — an unknown key succeeds
host.cronJobs.listEventSources(): Promise<{ eventSources: { id: string; pluginSlug: string; pluginName: string; enabled: boolean }[] }>  // cronJobs.read

interface PluginCronJob {
  id: string; key: string; agentId: string; name: string; prompt: string;
  cron: string | null;                 // 5 fields, in the account's timezone; null on an On event schedule
  eventSourceId: string | null;        // the live connection that triggers an On event schedule
  enabled: boolean;
  deliverTo: { channelId: string; personId: string | null; chatId: string | null } | null;
  nextRunAt: string | null;
}

An existing key comes back untouched with wasCreated: false. agentId must be an agent your installation created. deliverTo must be a person or chat that agent can already reach (the ids list_channels shows), otherwise the call rejects with 400. It rejects with 402 when the plan has no room or the cron is denser than the plan allows. update changes only what you pass; deliverTo: {} stops delivering, and cron is refused on an On event schedule. A schedule is deleted with its agent. The console shows it as "From " and the user may pause, edit or delete it, so don't re-create a deleted one on every sessionStarted.

host.plugins

Build and release the plugins your account owns, from files in the calling agent's workspace. It is meant for a plugin like Plugin Studio. Every call works on a plugin the account created, by id; an unknown, foreign or official id answers plugin_not_found. It is present in tool runs. Middleware and lifecycle runs may read, but a write answers needs_tool_call, because only a tool run knows who started the turn.

host.plugins.list(): Promise<{ plugins: AuthoredPluginSummary[] }>                          // plugins.read
host.plugins.get(args: { pluginId: string }): Promise<AuthoredPluginInfo | PluginActionError>   // plugins.read — no file contents
host.plugins.logs(args: { pluginId: string; levels?: string[]; since?: string; search?: string;
  sessionId?: string; beforeCursor?: string; limit?: number }): Promise<{ logs: PluginLogLine[]; nextCursor: string | null } | PluginActionError>  // plugins.read

host.plugins.create(args: { slug: string; name: string; description?: string }): Promise<{ pluginId: string; slug: string; url: string; alreadyExisted: boolean } | PluginActionError>  // plugins.write
host.plugins.checkout(args: { pluginId: string; versionId?: string; path: string; overwrite?: boolean }): Promise<PluginCheckoutResult | PluginActionError>  // plugins.write
host.plugins.saveDraft(args: { pluginId: string; workspacePath: string }): Promise<PluginDraftResult | PluginActionError>   // plugins.write
host.plugins.install(args: { pluginId: string; follow: "draft" | "latest"; agentIds?: string[] }): Promise<PluginInstallResult | AwaitingConfirmation | PluginActionError>  // plugins.write
host.plugins.acceptUpdate(args: { pluginId: string; versionId: string }): Promise<AwaitingConfirmation | PluginActionError>  // plugins.write
host.plugins.configure(args: { pluginId: string; settings: Record<string, unknown> }): Promise<{ url: string } | AwaitingConfirmation | PluginActionError>  // plugins.write
host.plugins.uninstall(args: { pluginId: string }): Promise<AwaitingConfirmation | PluginActionError>  // plugins.write

host.plugins.publish(args: { pluginId: string; draftVersionId: string }): Promise<AwaitingConfirmation | PluginActionError>  // plugins.release
host.plugins.yank(args: { pluginId: string; versionId: string }): Promise<AwaitingConfirmation | PluginActionError>         // plugins.release
host.plugins.addToBrowse(args: { pluginId: string }): Promise<AwaitingConfirmation | PluginActionError>                     // plugins.release
host.plugins.removeFromBrowse(args: { pluginId: string; deprecationMessage?: string }): Promise<AwaitingConfirmation | PluginActionError>  // plugins.release
host.plugins.setPrice(args: { pluginId: string; price: { model: "OneTime" | "Monthly"; amountUsd: number } | null; payoutCountry?: string }): Promise<AwaitingConfirmation | PluginActionError>  // plugins.release
host.plugins.delete(args: { pluginId: string }): Promise<AwaitingConfirmation | PluginActionError>                           // plugins.release

type PluginActionError = { error: string; message: string };
type AwaitingConfirmation = { status: "awaiting_confirmation"; summary: string; note: string };

Paths are workspace paths: a folder or a .zip, with the shared/ prefix for the shared workspace. saveDraft reads the package itself in chunks, so bytes never pass through the model or the sandbox; a build error comes back as { error, message }. Saving the same content again returns the existing draft with unchanged: true. publish takes a draft id: the person approves that exact draft, and publishing promotes it without building again.

Results, not rejections. A refusal or build error answers HTTP 200 with { error, message }, so the message always reaches you (a bridge error would drop the body). A missing permission is still a 403. Codes include plugin_not_found, package_not_found, draft_not_found, package_invalid, build_failed, setup_off, needs_trusted_turn, setup_needs_account_user and setup_needs_direct_chat.

Asking for a person's OK. Each plugins.release call, and the plugins.write calls that widen access, answer awaiting_confirmation and change nothing yet. See Asking for a person's OK.

A tool that holds agentApi:plugins.write may run for up to 120 seconds instead of 30; middleware and lifecycle runs keep their own limits. One draft save per account runs at a time; a second answers busy_try_again. The route is rate limited per web pod (4 at once, 120 a minute), so the account-wide ceiling is that times the number of web pods.

Who can change things. A change works only in a direct chat with a user of the account, or in a scheduled run in its own chat with nothing from outside in the turn. A reminder from notify_session_in counts as outside input, and a schedule pointed at a shared chat is refused. A subagent, a peer-started turn and a platform-started turn answer setup_needs_account_user: report the ids to the agent that started you. A turn that includes a message from someone who is not a user of the account answers needs_trusted_turn, and a group chat setup_needs_direct_chat. host.plugins.logs follows the same rule, because log lines can hold other sessions' data.

host.models

host.models.list(): Promise<{ models: {
  id: string; modelIdentifier: string; displayName: string;
  inputUsdPer1M: number | null; outputUsdPer1M: number | null; cachedUsdPer1M: number | null;
}[] }>

agentApi:models.read — the account's enabled models and the prices the user set. Pass id as modelId to host.agents.create/update.

host.license

host.license(): Promise<{ model: "OneTime" | "Monthly"; status: "Active" | "Cancelling" | "PastDue" | "Ended";
                          paidThrough: string | null } | null>

This installation's own licence, read-only, no permission needed. null means nothing to pay: the plugin is free, the account owns it, or it was installed while the plugin was free. paidThrough is an ISO timestamp (monthly only).


host.workspace

The files of the agent the invocation runs for — the same files and rules as the agent's own workspace_* tools (including the account's shared area). Present with host:workspace:read and/or host:workspace:write and an agent in context: tools, every middleware hook (for interAgentMessage it is the sender) and the session lifecycle events.

host.workspace.list(args?: { dir?: string })
  : Promise<{ path: string; entries: { name: string; dir: boolean; sizeBytes: number; modified: string;
                                        contentHash: string | null }[]; truncated: boolean }>        // read

host.workspace.read(args: { path: string })
  : Promise<{ path: string; binary: boolean; tooLarge: boolean; sizeBytes: number;
              contentHash: string; text: string | null; totalLines: number }>                        // read

host.workspace.read(args: { path: string; encoding: "base64"; offset?: number; length?: number })
  : Promise<{ path: string; sizeBytes: number; contentHash: string; offset: number; base64: string }> // read

host.workspace.write(args: { path: string; content: string; encoding?: "utf8" | "base64" })
  : Promise<{ path: string; sizeBytes: number; contentHash: string }>                                // write

host.workspace.edit(args: {
  path: string;
  operation: "replace_lines" | "insert_at_line" | "str_replace";
  expectedHash?: string;                    // refuse if the file changed since you read it
  fromLine?: number; toLine?: number;       // replace_lines
  line?: number;                            // insert_at_line
  content?: string;                         // replace_lines / insert_at_line
  oldText?: string; newText?: string;       // str_replace
}): Promise<{ path: string; sizeBytes: number; contentHash: string }>                                // write

host.workspace.delete(args: { path: string }): Promise<{ deleted: true }>                           // write

write stores UTF-8 text, or the decoded bytes when encoding is "base64" (invalid base64 is bad_arguments). A base64 read returns the byte range starting at offset (default 0), at most 2 MiB per call (length is clamped to that; read a larger file in chunks, using sizeBytes and contentHash to know when you are done and that the file did not change). An offset past the end gives an empty base64. Errors carry codes such as invalid_path, file_not_found, bad_arguments and workspace_storage_exceeded.

host.media

Upload audio and have the platform transcode it. Requires host:media:transcode, which also raises the invocation's limits (wall clock at least 60 s, more memory).

host.media.uploadAudio(args: { contentType: string; dataBase64: string }): Promise<{ uploadToken: string }>
host.media.transcodeAudio(args: { uploadToken: string }): Promise<{ downloadUrl?: string; /* … */ }>

One upload at a time: call transcodeAudio before the next uploadAudio. The returned downloadUrl can be fetched once with host.fetch. 10 media operations per invocation.


Models from plugin code

Three surfaces let tools, middleware, lifecycle events, compaction and web providers use a model without an API key. None needs a permission; usage is metered to the account. None takes a model argument — the platform decides which model runs. Model-provider plugins don't get them.

host.session.llm?.complete(args: LlmCompletionArgs): Promise<LlmCompletion>   // the chat's own agent model
host.llm?.complete(args: LlmCompletionArgs): Promise<LlmCompletion>           // the account's Utility model

interface LlmCompletionArgs {
  messages: { role: string; content: string }[];
  params?: { temperature?: number; maxTokens?: number; topP?: number; stop?: string[];
             thinking?: "off" | "low" | "medium" | "high" };
}
interface LlmCompletion {
  schemaVersion: number;
  assistant: { text: string | null; toolCalls: { id: string; name: string; argumentsJson: string }[];
               thinking?: { text: string | null; replayJson: string | null } };
  usage: { inputTokens: number; outputTokens: number; cachedTokens: number };
  stopReason: string;
}
  • host.session.llm — present whenever the invocation has a chat and agent, even without any other host.session member. 5 calls per invocation.
  • host.llm — present only when the account has a Utility model set (AI Providers page). No fallback to the agent's model. 5 calls per invocation, counted separately.
host.decide.noul({ state, questions: { instructions: string; trueMeans?: string; falseMeans?: string }[] })
host.decide.choose({ state, questions: { instructions: string; options: Record<string, string | null> }[] })
host.decide.score({ state, questions: { instructions: string; levels: string[] }[] })
  : Promise<{ answeredBy: "systemOne" | "llmFallback"; fallbackReason?: string; answers: … }>

Fast, typed decisions on the account's (or agent's) Decision model. state is a string, object or array read once for all questions; answers come back in the same order: { probability }, { choice, confidence?, probabilities? }, { score, confidence?, probabilities? }. With no decision model, a chat model answers instead (answeredBy: "llmFallback", slower, no calibrated confidence). If nothing can answer, the call throws not_configured: …. No per-invocation cap.


host.ssh

host.ssh.command(command: string, options?: { timeoutSeconds?: number })
  : Promise<{ exitCode: number; output: string; errorOutput: string; isTruncated: boolean; durationMs: number }>

Only inside sshConnectionOpening, sshCommandSending and sshCommandCompleted, only on the server that hook fired for. host:ssh:command. 60 s default, 90 s max, 10 calls per invocation, stateless. A non-zero exitCode is a normal result; an unreachable host rejects. See Middleware.

host.connection

host.connection.send(frames: ({ link?: string; text: string } | { link?: string; base64: string })[],
                     options?: { idempotencyKey?: string }): Promise<void>

Write frames onto a live connection your plugin owns, from a channel handler of that connection's channel. host:connection:send. At most 20 frames per call, all accepted or all refused; 20 calls per invocation. Reuse the same idempotencyKey on a retry. See Live connections.