Plugins

Implementações de modelo, ferramentas, skills de runtime, destinos de auditoria, conectores e serviços em segundo plano usam um único protocolo RuntimePlugin e um PluginHost por Harness. São plugins da arquitetura do código, não servidores MCP. Um plugin pode contribuir com várias capacidades; o sistema não atribui categorias de plugin mutuamente exclusivas.

O loop do Agent, a Session, as permissões e os registros ficam no engine. Os pacotes declarativos oharness.plugin.json existentes continuam suportados e nunca são importados automaticamente como módulos executáveis.

Início rápido

Rode a demonstração com quatro plugins, sem credenciais nem chamadas de rede:

npm run demo:plugins
npm run test:plugins

Os exemplos em script/fixtures/runtime-plugins.ts implementam um modelo roteirizado, uma ferramenta local, um observador de auditoria e um conector simulado. São demonstrações. O conector do Cloud hospedado é registrado como connector-cloud por padrão fora de instâncias efêmeras. Ele registra duas ferramentas sem acesso à rede na inicialização; a falta de login ou de configuração do servidor vira um erro de ferramenta, não uma falha de inicialização. Desative-o com { "id": "connector-cloud", "enabled": false } em runtimePlugins do plugins.json. Quem embute o engine pode definir cloudConnectors: false. O adaptador direto do Composio continua sendo uma alternativa opcional para desenvolvedores; não o habilite em clientes hospedados. O Pipedream não está implementado.

O plugin de memória embutido, ID memory, agora é dono da ferramenta de memória, do índice no prompt a cada turno, da página web e do ciclo de vida da sincronização em segundo plano. Ele fica ativo mesmo em instâncias efêmeras do Harness (igual ao comportamento anterior da memória local). Desative-o no plugins.json para remover todas essas contribuições. Isso é diferente de desligar a sincronização na nuvem mantendo a memória local.

Escrever um plugin

import type { RuntimePlugin } from "@oharness/engine";

export default {
  id: "hello-tools",
  version: "1.0.0",
  apiVersion: 1,
  parseConfig(raw) {
    if (raw !== undefined) throw new Error("This plugin takes no options");
    return {};
  },
  setup(ctx) {
    ctx.tools.register({
      name: "hello_greet",
      description: "Return a greeting",
      inputSchema: { type: "object", properties: {} },
      readOnly: true,
      async execute() {
        return { content: [{ type: "text", text: "Hello" }] };
      },
    });
  },
} satisfies RuntimePlugin;

setup roda uma vez por instância do Harness. Mantenha clientes, timers e estado mutável dentro dele, não no escopo do módulo. Os IDs de plugin usam letras minúsculas, números, pontos, sublinhados ou hífens. A versão 1 da API é verificada independentemente da versão do próprio plugin.

Carregar e configurar

Um app que embute o engine pode fornecer explicitamente definições de plugin confiáveis:

import { Harness } from "@oharness/engine";
import greeting from "./hello-plugin.js";

const harness = await Harness.create({
  runtimePlugins: [{ plugin: greeting, required: true }],
});
try {
  console.log(harness.runtimePluginStatus());
  // Create sessions / agents as usual.
} finally {
  await harness.close();
}

Outra opção é adicionar runtimePlugins ao arquivo de plugins do usuário (plugins.json em OHARNESS_HOME, normalmente ~/.oharness). É aceito um caminho absoluto de módulo ou um pacote instalado que possa ser resolvido a partir desse diretório; nada é baixado. Os módulos precisam exportar por padrão uma definição de plugin. Prefira JavaScript compilado para distribuição; TypeScript de código-fonte exige o subconjunto de remoção de tipos suportado pelo Node.

{
  "runtimePlugins": [
    {
      "id": "hello-tools",
      "module": "/absolute/path/to/hello-plugin.js",
      "enabled": true,
      "required": true
    }
  ]
}

config é passado para parseConfig. Uma entrada configurada com o mesmo ID pode configurar/desativar um plugin inline, mas não pode substituir a implementação dele por outro módulo. IDs duplicados em qualquer uma das listas são rejeitados. enabled: false pula totalmente a importação do módulo, o parsing da configuração e o setup. required tem padrão true; falhas opcionais aparecem no status sem abortar a criação do Harness. A lista da configuração substitui uma lista anterior em vez de mesclar profundamente as opções dos plugins. As alterações valem na próxima criação do Harness, não para sessões ativas.

A configuração do projeto não pode introduzir nem reconfigurar plugins de runtime. Fornecer HarnessOptions.config é uma decisão explícita de confiança do app que embute o engine. Não coloque segredos na configuração do projeto nem nos metadados de plugins. Carregue-os pela sua integração de credenciais/ambiente aprovada e nunca os registre em log.

Interfaces de capacidade

InterfaceContribuição
ctx.tools.register(tool)Definição e implementação de ferramenta; o pipeline de permissões existente continua valendo
ctx.skills.register(skill)Skill em memória disponível durante a vida deste plugin; nomes duplicados são rejeitados
ctx.models.register(provider, models)Implementação de modelo mais as definições dos modelos, validadas atomicamente
ctx.auth.register(provider)Implementação de autenticação usando o contrato AuthProvider existente
ctx.hooks.on(event, handler)Hook tipado de ciclo de vida existente, com cancelamento automático da inscrição
ctx.audit.registerSink(sink)Observador de eventos de permissão/execução no modo melhor esforço
ctx.prompts.register(source)Seção assíncrona do prompt de sistema, atualizada a cada turno do Agent
ctx.pages.register(page)Descritor de página renderizada pelo host e handlers de leitura/ação
ctx.onStart(fn)Trabalho em segundo plano, iniciado depois que todos os setups de plugins terminam
ctx.onDispose(fn)Recursos a fechar, na ordem inversa de aquisição

Todas as contribuições precisam ser registradas durante o setup. Elas são rastreadas pelo host e revertidas automaticamente. O registro padrão rejeita conflitos; os plugins não podem substituir em silêncio os recursos embutidos nem os registros uns dos outros. As APIs fluentes de registro existentes continuam compatíveis. Remover uma implementação de autenticação não apaga credenciais. Provedores de modelos devem contribuir com os próprios IDs, e não tentar sobrescrever entradas existentes do catálogo. Selecione um modelo de plugin pela configuração model normal ou pela API de seleção de modelo.

Uma skill de runtime fornece name, description e body, mais um directory absoluto opcional quando o corpo referencia scripts ou outros arquivos. Ela usa a mesma ferramenta skill de divulgação progressiva das skills persistidas, aparece com a origem runtime e é removida automaticamente quando o plugin dela para. Skills de runtime não podem substituir skills embutidas, do usuário, do projeto, configuradas ou outras skills de runtime com o mesmo nome.

ctx.signal anuncia o desligamento do plugin. Ferramentas e streams de modelo também recebem o sinal de cancelamento combinado do host e da chamada. Mantenha o trabalho em segundo plano cooperativo; registre a limpeza de um recurso logo depois de adquiri-lo, antes de awaits posteriores que possam falhar. onStart precisa resolver depois de iniciar o trabalho, e não aguardar um loop de worker infinito. Em vez disso, guarde a promise de conclusão e aguarde-a em onDispose.

Plugin de auditoria

O engine traz jsonlAuditPlugin (id: "audit-jsonl") usando esse mesmo contrato. Passe { path: "/absolute/path/to/events.jsonl" } como a configuração dele, seja pela API de embutir, seja por uma entrada de módulo apontando para packages/engine/dist/runtime-plugins/builtin/jsonl-audit.js. Ele cria os diretórios pai, abre em modo de acréscimo (0600 para um arquivo recém-criado) e esvazia a fila antes de fechar. Ele não rotaciona arquivos nem muda permissões de arquivos já existentes.

Um destino de auditoria recebe RuntimeAuditEvent:

  • permission: registros de decisão/resposta existentes do PermissionEngine, incluindo decisões de subagentes encaminhadas pelo host.
  • tool_execution: chamadas concluídas, incluindo erros lançados por ferramentas convertidos em resultados de erro; inclui tempos, identidade da ferramenta e qual plugin de runtime é o dono dela.

Os eventos de execução omitem os corpos de entrada/saída e as credenciais. Os registros de permissão mantêm os campos existentes de regra/principal, que podem conter caminhos ou argumentos de comando: os destinos precisam tratar a sensibilidade deles. Isto não é um replay completo de eventos nem um diário de conformidade; chamadas bloqueadas antes do PermissionEngine não ganham registros de decisão adicionais nesta implementação.

Cada destino recebe o próprio snapshot e uma fila ordenada. Um destino que lança erro não bloqueia outro destino nem muda a decisão sobre uma ferramenta. As filas têm limite de 1.000 eventos pendentes por destino e avisam/descartam o excesso. O desligamento esvazia as filas antes de fechar os recursos dos destinos, sujeito ao timeout de limpeza. audit: false desativa os eventos de auditoria do runtime, assim como o registro de permissões existente.

Exemplo de configuração de destino em arquivo (dentro de um plugin):

const file = await open(config.path, "a", 0o600); // node:fs/promises
ctx.onDispose(() => file.close());
ctx.audit.registerSink(async (event) => {
  await file.appendFile(JSON.stringify(event) + "\n");
});

Isso preserva a semântica de auditoria de melhor esforço já existente no engine. Tornar um plugin de auditoria obrigatório garante a inicialização, não gravações com falha fechada. Uma auditoria durável obrigatória exigiria uma política de execução com espera, projetada à parte.

Plugin de conector

Use createConnectorPlugin como uma factory de conveniência ou implemente RuntimePlugin diretamente. A factory retorna um RuntimePlugin comum, não outro mecanismo:

const plugin = createConnectorPlugin({
  id: "my-connector",
  version: "1.0.0",
  parseConfig: validateConnectorConfig,
  async connect(config, { signal, logger }) {
    // Wrap your selected SDK. Bind a trusted account here, not from model input.
    return createMyConnectorClient(config, { signal, logger });
  },
});

ConnectorClient expõe tools(): Promise<readonly Tool[]> e close(). Se a descoberta de ferramentas falhar, o cliente conectado ainda é fechado. O helper força readOnly e sessionLocal como desativados para ferramentas externas, para que os metadados do fornecedor não consigam contornar em silêncio a aprovação ou as restrições do modo plan. Exponha apenas ferramentas aprovadas, use namespaces, valide os argumentos e propague o sinal de cada chamada para o SDK. Não envie o catálogo inteiro de um fornecedor em todo prompt do modelo.

O host não implementa OAuth de fornecedores, vinculação de contas, webhooks nem armazenamento por tenant. O conector do Cloud fornece a criação de links hospedados, a interação Web e o vínculo verificado; ele não muda o protocolo do host. A identidade de conta multiusuário precisa vir do contexto confiável do aplicativo, nunca de um ID de conta escolhido pelo modelo. O plugin host em si não exige nenhuma mudança de infraestrutura na nuvem.

Ciclo de vida e compatibilidade

  1. Resolver as entradas habilitadas e validar o contrato/a configuração.
  2. Rodar os setups na ordem configurada; registrar a posse de registros e recursos.
  3. Rodar os starts quando todos os setups terminarem; expor o status ativo.
  4. Em caso de falha, reverter o plugin que falhou; abortar e reverter tudo se ele for obrigatório.
  5. Ao fechar o Harness, impedir novas execuções do host, cancelar/esvaziar as execuções existentes, abortar chamadas de plugins, cancelar o registro das contribuições e então fechar os recursos na ordem inversa.

A limpeza é idempotente e continua após falhas individuais. O esvaziamento do Agent e o esvaziamento/limpeza de cada plugin têm limite de cinco segundos. Os timeouts são informados; eles não conseguem parar à força JavaScript que não coopera, e pode sobrar trabalho depois de um timeout. Uma ferramenta de plugin clonada e desatualizada rejeita chamadas depois que o plugin dela para.

O /plugins da CLI e o /context da Web mostram o estado do runtime ao lado dos pacotes declarativos. Os erros omitem as mensagens de exceção fornecidas pelos plugins para não vazar segredos da configuração. Se precisar, adicione diagnósticos sanitizados com o logger do escopo do plugin.

A V1 deliberadamente não tem descarregamento em tempo real, grafo de dependências/serviços entre plugins, instalador de pacotes remotos, sandbox, carregador arbitrário de módulos de frontend nem núcleo Agent/Session substituível. Plugins instalados no mesmo processo têm a autoridade do processo: a API de contexto é um limite de manutenibilidade, não uma sandbox de segurança.

Veja limites de configuração para migrar a configuração inline legada. Arquivos de plugin separados são configuração confiável do usuário e nunca são carregados de um diretório de projeto arbitrário.