novo agents

HTTP connections

Register an OpenAPI (or Swagger 2.0) document and its operations become callable by agents that opt in to http. Each operation maps to a tool named api__{connection}__{operation}; calling it reconstructs the HTTP request (substituting path parameters, appending query parameters, attaching the request body, and applying auth) and returns the response as { status, statusText, body }. A non-2xx response is returned to the model (not thrown), so the agent can react to it.

Bearer auth uses a reusable Secret. The token is sent only on Novo-to-API requests. It is not exposed to a managed workspace. For a public / no-auth API, use auth: { type: 'none' } (or omit auth); Novo sends no Authorization header at all. See Auth.

HTTP connections can be configured through the SDK/API or the console adapters screen, then attached to agents with capabilities.http. The console path is for the common setup fields plus operation allowlists, trust, schema-loading deferral, required-skill gates, and enable/disable. Use the SDK/API for remaining tuning knobs such as resultCapBytes, requestTimeoutMs, and metadata.

Methods

| Method | Route | |---|---| | novo.httpConnections.register({...}) | POST /v1/http-connections | | novo.httpConnections.list() | GET /v1/http-connections | | novo.httpConnections.get(id) | GET /v1/http-connections/:id | | novo.httpConnections.update(id, {...}) | PATCH /v1/http-connections/:id | | novo.httpConnections.delete(id) | DELETE /v1/http-connections/:id | | novo.httpConnections.test(id) | POST /v1/http-connections/:id/test |

Type signature

type HttpConnectionsNamespace = {
  register(input: {
    name: string;
    spec: string; // OpenAPI/Swagger document: a URL (JSON only in V1) or inline JSON
    baseUrl?: string;
    auth?: { type: 'bearer'; secretId: string } | { type: 'none' }; // omit = authless
    allowedOperations?: string[];
    approval?: 'once' | 'always' | 'never';
    gatedTools?: string[];
    resultCapBytes?: number;
    trusted?: boolean;
    defer?: boolean | null;
    requiredSkills?: string | string[] | null;
    enabled?: boolean;
    requestTimeoutMs?: number | null;
    metadata?: Metadata;
  }): Promise<HttpConnection>;
  list(): Promise<Page<HttpConnection>>;
  get(httpConnectionId: string): Promise<HttpConnection>;
  update(httpConnectionId: string, input: {
    name?: string;
    spec?: string;
    baseUrl?: string;
    auth?: { type: 'bearer'; secretId: string } | { type: 'none' }; // { type: 'none' } drops the bearer
    allowedOperations?: string[];
    approval?: 'once' | 'always' | 'never' | null;
    gatedTools?: string[] | null;
    resultCapBytes?: number;
    trusted?: boolean;
    defer?: boolean | null;
    requiredSkills?: string | string[] | null;
    enabled?: boolean;
    requestTimeoutMs?: number | null;
    metadata?: MetadataPatch;
  }): Promise<HttpConnection>;
  delete(httpConnectionId: string): Promise<{ id: string; object: 'http_connection'; deleted: true }>;
  test(httpConnectionId: string): Promise<{
    connected: true;
    latencyMs: number;
    operationCount: number;
    tools: Array<{ name: string; namespacedName: string }>;
  }>;
};

Register

const bearer = await novo.secrets.create({
  name: 'acme-api-bearer',
  value: process.env.ACME_API_TOKEN!,
  contentType: 'text/plain',
});

const connection = await novo.httpConnections.register({
  name: 'acme',
  spec: 'https://api.acme.dev/openapi.json',
  baseUrl: 'https://api.acme.dev',
  auth: { type: 'bearer', secretId: bearer.id },
  allowedOperations: ['getUser', 'createOrder'],
  requestTimeoutMs: 30000,
});

const probe = await novo.httpConnections.test(connection.id);
console.log(probe.tools.map((tool) => tool.name));

name is part of every model-facing tool name (api__{connection}__{operation}), so it must be a short slug: 1-56 letters, numbers, underscores, or hyphens, and it cannot contain the reserved __ separator. Novo exposes only operations whose final namespaced name fits provider tool-name limits (letters, numbers, underscores, or hyphens; 64 characters max).

Attach to an agent:

const agent = await novo.agents.create({
  context: '…',
  band: 'core',
  capabilities: {
    http: { connectionIds: [connection.id] },
  },
});

Auth

  • Bearer: auth: { type: 'bearer', secretId } requires an explicit baseUrl. Novo attaches the credential only when the reconstructed operation URL has that exact origin; a different scheme, host, or port is rejected instead of receiving the token. secretId is a reusable Secret; its value is never exposed to a managed workspace. Per-operation OpenAPI security reshapes where the token goes (see Operation mapping) without widening the bound origin.
  • Authless: auth: { type: 'none' }, or simply omitting auth, registers a connection that sends no Authorization header. Use it for public / no-auth APIs. This avoids minting a throwaway Secret, and it is the correct choice for endpoints that reject an unexpected Authorization header.
// Public API: no Secret required.
const weather = await novo.httpConnections.register({
  name: 'open-meteo',
  spec: 'https://api.open-meteo.com/openapi.json',
  auth: { type: 'none' },
});

Switch an existing connection between the two with update: auth: { type: 'none' } drops the bearer, auth: { type: 'bearer', secretId } sets one.

Get or create (idempotent setup)

Connection name is unique per organization, so re-running a setup script that calls register with a name that already exists returns 409 name_conflict rather than reusing the row. For idempotent, IaC-style setup, look the connection up by name first and only register when it is missing:

async function getOrCreateHttpConnection(input) {
  const existing = (await novo.httpConnections.list()).data.find(
    (c) => c.name === input.name,
  );
  if (existing) {
    return novo.httpConnections.update(existing.id, input);
  }
  return novo.httpConnections.register(input);
}

update reconciles the found row to your desired config (it bumps configVersion on any structural change), so the script converges whether or not the connection already existed.

Spec source

spec is either a URL Novo fetches (JSON only in V1; the fetch is SSRF-guarded, time-bounded, and size-capped) or an inline JSON OpenAPI/Swagger document. YAML specs are not supported in V1; convert to JSON first.

For authless connections, the base URL operations are joined against is, in order: an explicit baseUrl on the connection, then the document's first usable servers[] entry (OpenAPI 3.x) or host/basePath (Swagger 2.0). A bearer connection must set baseUrl; a document cannot steer where its credential goes. Every selected URL is treated as untrusted: it is HTTPS-only, SSRF-checked at load, and checked again on every reconstructed request URL.

The derefed spec is cached per (connection, configVersion) across turns, so a connection's spec is fetched and parsed at most once per cache window instead of on every turn. Bumping any structural field (via update) bumps configVersion, which invalidates the cache immediately. The cache also expires on a short time-to-live, so a spec served from a URL is re-fetched periodically even without a structural change. A spec-load failure degrades that connection to zero tools for the turn rather than failing the run.

Operation mapping

  • Each path/method pair becomes one tool. The tool name is the operation's operationId (sanitized to provider-legal characters), falling back to {method}_{path} when no operationId is present; collisions are disambiguated with a numeric suffix.
  • Path, query, header, and cookie parameters become top-level input properties; a request body is nested under body.
  • Security is derived from the operation's effective security requirement: a bearer/oauth2/openIdConnect scheme uses Authorization: Bearer <token> (the default), basic rewrites the Authorization scheme, and an apiKey scheme places the token in the named header, query parameter, or cookie.
  • Mark a side-effect-free operation with the x-novo-read-only: true vendor extension so Novo may schedule it concurrently with other read-only calls in the same step; operations without it run serialized. Use it only for operations that genuinely have no side effects; the hint is taken on faith.
  • Mark operation-specific trust with x-novo-trusted: false to force one operation's output through the untrusted-data envelope even when the connection is trusted. x-novo-trusted: true does not elevate an untrusted connection; set connection-level trusted: true only for first-party APIs you fully control.
  • Mark operation-specific schema loading with x-novo-defer: false to keep a high-frequency operation hot, or x-novo-defer: true to defer it even when the connection is configured hot. Operation hints override the connection-level defer value.
  • Gate a specific operation behind a Skill with x-novo-required-skills: "skill-name" (or an array of names); the model must load that skill before the operation runs. Overrides the connection-level requiredSkills. See Required skills.

Trust boundary

By default, operation output is wrapped in an "untrusted data" envelope before the model sees it, as defense against prompt injection from external data sources. Set trusted: true only for first-party APIs you fully control. For mixed first-party platform connections, register the connection as trusted and mark live external-pull operations with x-novo-trusted: false so only those outputs stay wrapped.

allowedOperations

Cap which operations the agent can call. Omit allowedOperations to expose every operation in the spec. Set a non-empty list for least privilege. A large spec can expose many tools, so a tight list is recommended.

Declarative approval

Set approval: 'once' | 'always' | 'never' for a stable connection policy without custom webhook logic. once remembers an accepted tool-and-input pair for the session; always reviews every matching call; never bypasses review. Add gatedTools to apply the mode only to selected original operation names; unlisted operations behave as never. gatedTools requires an approval mode and cannot accompany approval: 'never'.

The connection policy runs before an agent-level approval hook. A call waived by the policy skips the hook; a call that still needs review uses the hook when configured and otherwise pauses for a human.

Deferred tool loading

A connection's operations are deferred by default: the model sees each operation's name and description, and loads the full input schema on demand via load_tools (see tool servers for the full behavior). Set connection-level defer: false to keep this connection's operation schemas always loaded (hot); omit it (or null) for the default. Use operation-level x-novo-defer: false for a small set of high-frequency operations and leave long-tail or schema-heavy operations deferred. Because a deferred operation's description is the only thing the model sees until it loads the schema, keep operation descriptions in your spec clear and self-contained.

Required skills

Set requiredSkills to gate this connection's operations behind a Skill: the model must load the named skill before any operation can run. Skill loading is separate from deferred schema loading. If an operation is deferred and also requires a Skill, the model should call load_tools for the operation and load_skill for the Skill in the same loader step, then call the operation after both loader results are visible. Pass a skill name or an array of names at the connection level, or per operation with x-novo-required-skills (which overrides the connection value).

await novo.httpConnections.register({
  name: 'billing-api',
  spec: '…',
  // ...auth...
  requiredSkills: 'billing-tools', // connection-level default
});

The named skill must be one the agent has (an inline skill in capabilities.skills, or one implied by a managed capability); a requiredSkills naming a skill the agent lacks leaves the operation permanently un-callable. Because loading the skill is how the model learns the required Skill-specific guidance, write the skill to document the relevant operations. Same behavior as tool servers.

Result cap

resultCapBytes bounds what an operation result puts in the model's context, with the same recovery semantics as tool servers: an over-cap result is spilled to the workspace (or archived to a result handler) and the model receives a compact preview. Workspace spills include a deterministic recovery hint for read_file / grep / glob, and list-shaped results prefer a digest preview over an arbitrary byte slice. Overflows surface as data-novo-tool-result-overflow chunks. Without recovery configured the result is head-truncated.

Rotate auth

Rotate the underlying Secret value, or switch the connection to a different Secret:

await novo.secrets.rotate(bearer.id, { value: process.env.ACME_API_TOKEN_NEXT! });

await novo.httpConnections.update(connection.id, {
  auth: { type: 'bearer', secretId: otherSecret.id },
});

Test

novo.httpConnections.test(id) loads and parses the spec through the SSRF guard, maps operations, applies allowedOperations, and returns the exposed operation names with their Novo namespaced forms (api__{connection}__{operation}), the names that appear in stream/tool telemetry. Use it after registration and after Secret rotation before attaching to production agents.

Enabled

enabled: false registers a connection in disabled state. While disabled, every run emits a data-novo-http-connection-unavailable chunk with errorCode: 'policy_denied' and the spec is never loaded. The same chunk carries errorCode: 'spec_unreachable' | 'spec_invalid' | 'base_url_missing' when an enabled connection's spec fails to load or parse at run time, so a broken connection is as visible on the stream as a disabled one, rather than a silent capability gap the agent runs into mid-task. Branch on the chunk type and order by the SSE frame id rather than its arrival position (see Streaming). Flip enabled: true to bring a disabled connection online without re-registering.

SSRF guard

Novo verifies the spec URL and every reconstructed request URL is publicly reachable and not in a private IP range before delivery. Internal network URLs (10.*, 172.16.*, 192.168.*, link-local including the cloud metadata endpoint, loopback, and bare hostnames) are rejected, including a servers[] base URL declared inside the spec.

Errors

  • 404 http_connection_not_found: bad ID.
  • 400 invalid_request_error: bad name, spec URL in a private IP range, missing secret, or (from httpConnections.test(id)) a spec that resolves to no base URL.
  • 502 service_unavailable: httpConnections.test(id) could not load or parse the spec (unreachable/404 URL, non-JSON, or malformed OpenAPI document). A spec that loads cleanly but exposes zero operations is not an error; it returns connected: true with operationCount: 0.