Bot agents

A bot agent makes a coding agent available to Terminay Bots. The extension describes the agent: how to install it in the bot image, how it signs in, and which models it offers. Terminay owns everything else, including the bot, its container, sessions, and credentials. Added in API 3.5; API 3.6 moved bots to the native CLI runtime this page describes.

Manifest contribution

JSON
"api": "^3.6.0",
"permissions": ["agent-bots"],
"contributes": {
  "botAgents": [{
    "id": "com.example.bot/example",
    "displayName": "Example agent",
    "imageInstall": { "npm": ["@openai/codex@0.165.0"] },
    "runtimeProtocol": "native-cli-v1",
    "nativeDriver": "codex-cli",
    "runtimeVersion": "0.165.0",
    "autonomousOnly": true,
    "deviceSignIn": {
      "command": { "executable": "codex", "args": ["login", "--device-auth"] },
      "signOutCommand": { "executable": "codex", "args": ["logout"] },
      "label": "Sign in with ChatGPT",
      "help": "Approve the code in your browser.",
      "configFiles": [{
        "path": ".codex/config.toml",
        "contents": "cli_auth_credentials_store = \"file\"\n"
      }]
    },
    "credentialSecrets": [{
      "environmentVariable": "CODEX_API_KEY",
      "kind": "api-key",
      "label": "Example API key",
      "help": "Create an API key in your Example account."
    }],
    "botConfigFiles": [{
      "path": ".codex/config.toml",
      "contents": "cli_auth_credentials_store = \"ephemeral\"\n"
    }],
    "launchEnvironment": { "EXAMPLE_MAX_RETRIES": "0" }
  }]
}

Requires the agent-bots permission. A manifest may declare up to 8.

FieldMeaning
id, displayNameRequired. The id is namespaced by the extension ID.
imageInstallRequired. npm packages at exact versions, and optional debian packages, installed when Terminay builds the bot image. At most 32.
runtimeProtocolRequired. native-cli-v1.
nativeDriverRequired. claude-code-cli or codex-cli: the host driver that speaks to the CLI.
runtimeVersionRequired. The exact CLI version the driver is pinned to.
autonomousOnlyRequired. true when the agent cannot ask permission inside a container; the bot's approval mode is then fixed to Autonomous.
deviceSignInOptional. The agent's own login: command and signOutCommand (each { executable, args }), label, help, and optional configFiles written before sign-in and before each session while the bot signs in that way.
credentialSecretsOptional, up to 8. Each names an environmentVariable, a kind (sign-in-token or api-key), a label, and help. Values are never part of the manifest or runtime.
botConfigFilesOptional, up to 8 files of at most 16 KiB, written into the bot's home before each session.
launchEnvironmentOptional fixed, non-secret environment for the agent.

File paths are relative to /home/bot, cannot contain .., and cannot use memories/ or the host-reserved .terminay/. Neither fixed nor runtime environment may set a declared credential variable; the host supplies those.

Runtime contract

TypeScript
import { defineExtension } from '@terminay/extension-api';

export default defineExtension({
  activate(context) {
    context.subscriptions.add(
      context.agents.registerBotAgent('com.example.bot/example', {
        async models(signal) {
          signal.throwIfAborted();
          return [{ id: 'example-large', displayName: 'Example Large' }];
        },
        async status(signal) {
          signal.throwIfAborted();
          return { state: 'ready' };
        },
        async launch(_request, signal) {
          signal.throwIfAborted();
          // The host builds the native CLI arguments, including the model.
          return { args: [], env: {} };
        },
      }),
    );
  },
});
MethodContract
models(signal)Up to 64 { id, displayName } entries, cached per activation. The bot's model picker offers Default plus these.
status(signal){ state: 'ready' } or { state: 'not_signed_in', detail? }.
launch({ modelId }, signal)modelId is null for Default. Return { args: [], env }: the host owns native CLI arguments and passes the model through its driver.

Every result is closed and bounded. A failed or timed-out call makes the registration unavailable for that activation. Disposal or deactivation aborts outstanding calls and clears the model cache.

What the extension never gets

The permission grants description and registration only. The extension receives no bot or session input and no bot credential, even when it holds other secret permissions. Bots, containers, images, native protocols, sessions, transcripts, credential injection, and UI belong to the host, which passes a credential's name, never its value, in arguments.

Testing

createAgentExtensionHarness rejects an invalid bot declaration before activating the extension, and drives the runtime with registeredBotAgentIds(), botAgentModels(id), botAgentStatus(id), and botAgentLaunch(id, request). Pass botAgentDeadlineMs to test deadlines. The complete author example is examples/bot-agent.