Building
Host API reference
Every member of the host object, its arguments, its result, and when it is present.
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>
getreturns a non-secret setting's value (itsdefaultuntil the user saves one). Ahostsfield returns a JSON array encoded as a string —JSON.parseit. A key with no saved value and nodefault, or one you didn't declare, returnsnull.getSecretreturns asecretfield's value. It is fetched only when you call it. It throwsMissingCredential: …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.egressor ahostssetting (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; withoauththey are not followed and a 3xx comes back to you.The call rejects with an error whose
namesays why, so you can branch on it:error.nameWhen error.messageEgressDeniedthe host is refused, or is a private, loopback, link-local or metadata address host not allowlisted: <url>orblocked address for <host>RateLimitedover the rate limit fetch rate limit exceededFetchTimeoutit timed out fetch timed outResponseTooLargethe response is too big response exceeded <n> bytesTooManyRedirectsmore than 5 redirects more than 5 redirectsInternalanything else went wrong with the request fetch failed: <reason>With
oauth, the URL host must be in that key'sapiHosts, anyAuthorizationheader you set is replaced, and a 401 triggers one token refresh and retry. You never see the token. Errors haveerr.nameofOAuthNotConnected,OAuthAccountRequired,OAuthReconnectRequiredorOAuthUnavailable.
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 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 otherhost.sessionmember. 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.