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 explicitbaseUrl. 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.secretIdis a reusable Secret; its value is never exposed to a managed workspace. Per-operation OpenAPIsecurityreshapes where the token goes (see Operation mapping) without widening the bound origin. - Authless:
auth: { type: 'none' }, or simply omittingauth, registers a connection that sends noAuthorizationheader. Use it for public / no-auth APIs. This avoids minting a throwaway Secret, and it is the correct choice for endpoints that reject an unexpectedAuthorizationheader.
// 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 nooperationIdis 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
securityrequirement: abearer/oauth2/openIdConnectscheme usesAuthorization: Bearer <token>(the default),basicrewrites theAuthorizationscheme, and anapiKeyscheme places the token in the named header, query parameter, or cookie. - Mark a side-effect-free operation with the
x-novo-read-only: truevendor 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: falseto force one operation's output through the untrusted-data envelope even when the connection is trusted.x-novo-trusted: truedoes not elevate an untrusted connection; set connection-leveltrusted: trueonly for first-party APIs you fully control. - Mark operation-specific schema loading with
x-novo-defer: falseto keep a high-frequency operation hot, orx-novo-defer: trueto defer it even when the connection is configured hot. Operation hints override the connection-leveldefervalue. - 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-levelrequiredSkills. 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 (fromhttpConnections.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 returnsconnected: truewithoperationCount: 0.