diff --git a/sink/agent-vault.test.ts b/sink/agent-vault.test.ts index 1f8f74d..9043ec5 100644 --- a/sink/agent-vault.test.ts +++ b/sink/agent-vault.test.ts @@ -1,6 +1,6 @@ import { inspect } from "node:util"; import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; -import { InfisicalSDK, InfisicalSDKError, InfisicalSDKRequestError } from "../src"; +import { AgentVaultInlineServiceInput, InfisicalSDK, InfisicalSDKError, InfisicalSDKRequestError } from "../src"; import { FetchHttpError } from "../src/api/fetch-errors"; import { AgentVaultDelegationScope, @@ -75,8 +75,6 @@ const INLINE_SERVICE = { id: "8e2f4a6c-1b3d-4e5f-9a7b-2c4d6e8f0a1b", name: "github", type: "http", - serverUrl: null, - transport: null, hostPattern: "api.github.com", allowedMethods: ["GET"], allowedPathPrefixes: null, @@ -90,8 +88,6 @@ const SERVICE = { accessBundleId: BUNDLE_ID, name: "github", type: "http", - serverUrl: null, - transport: null, hostPattern: "api.github.com", allowedMethods: null, allowedPathPrefixes: null, @@ -606,6 +602,59 @@ describe("agent vault traffic policy", () => { }); }); +describe("agent vault typed services", () => { + const { hostPattern, allowedMethods, allowedPathPrefixes, ...SERVICE_BASE } = SERVICE; + const POSTGRES_SERVICE = { + ...SERVICE_BASE, + name: "orders-db", + type: "postgres", + host: "db.internal", + port: 5432, + database: "orders", + tls: { enabled: true, verify: true, ca: null }, + credential: { type: "basic", memberProvidedFields: [] } + }; + const SSH_SERVICE = { + ...SERVICE_BASE, + id: "7d1e3f5a-9b2c-4d6e-8f0a-1b3c5d7e9f2a", + name: "bastion", + type: "ssh", + host: "bastion.internal", + port: 22, + credential: { type: "ssh-key", hasCertificate: false } + }; + it("access bundle get reads a database and an SSH service by type", async () => { + respondWith({ accessBundle: { ...BUNDLE, services: [POSTGRES_SERVICE, SSH_SERVICE] } }); + const { services } = await agentVault.accessBundles.get(BUNDLE_ID); + expect(services).toEqual([POSTGRES_SERVICE, SSH_SERVICE]); + const [database, server] = services; + if (database.type !== "postgres" || server.type !== "ssh") throw new Error("services were not narrowed"); + expect(database.tls).toEqual({ enabled: true, verify: true, ca: null }); + expect([database.host, database.port, database.database]).toEqual(["db.internal", 5432, "orders"]); + expect([server.host, server.port]).toEqual(["bastion.internal", 22]); + }); + + it("sessions read an inline database service by type", async () => { + const inline = { + id: INLINE_SERVICE.id, + name: "orders-db", + description: "Orders replica", + type: "mysql", + host: "db.internal", + port: 3306, + database: "orders", + tls: { enabled: false, verify: false, ca: null }, + credential: { type: "basic" } + }; + respondWith({ session: { ...SESSION, services: [inline] } }); + const [service] = (await agentVault.sessions.get(SESSION_ID)).services; + expect(service).toEqual(inline); + if (service.type !== "mysql") throw new Error("service was not narrowed"); + expect(service.database).toBe("orders"); + expect(service.description).toBe("Orders replica"); + }); +}); + describe("agent vault inline services", () => { it("create sends services without an access bundle", async () => { respondWith({ session: { ...CREATED_SESSION, accessBundle: null, services: [INLINE_SERVICE] } }); @@ -615,6 +664,29 @@ describe("agent vault inline services", () => { expect(created.accessBundle).toBeNull(); }); + it("create sends database and SSH services", async () => { + const services: AgentVaultInlineServiceInput[] = [ + { + type: "postgres", + name: "orders-db", + description: "Orders replica", + host: "db.internal", + database: "orders", + tls: { verify: false }, + credential: { type: "basic", username: "agent", password: "db-password" } + }, + { + type: "ssh", + name: "bastion", + host: "bastion.internal", + credential: { type: "ssh-key", username: "deploy", privateKey: "ssh-private-key" } + } + ]; + respondWith({ session: { ...CREATED_SESSION, accessBundle: null } }); + await agentVault.sessions.create({ ...PROJECT, services }); + expect(lastRequest().body).toEqual({ projectId: PROJECT_ID, services }); + }); + it("create sends an access bundle and services together", async () => { respondWith({ session: { ...CREATED_SESSION, services: [INLINE_SERVICE] } }); await agentVault.sessions.create({ ...PROJECT, accessBundle: "release", services: [INLINE_SERVICE_INPUT] }); diff --git a/sink/test-agent-vault.ts b/sink/test-agent-vault.ts index 3c7e78c..327595d 100644 --- a/sink/test-agent-vault.ts +++ b/sink/test-agent-vault.ts @@ -249,7 +249,7 @@ const expectRejection = async (label: string, run: () => Promise, patte } ); await step("sessions.get returns the inline services", () => adminVault.sessions.get(inline.id), (res) => - res.services.length === 1 && res.services[0].hostPattern === "httpbin.org" && res.services[0].customHeaders[0]?.name === "X-Team" + res.services.length === 1 && res.services[0].type === "http" && res.services[0].hostPattern === "httpbin.org" && res.services[0].customHeaders[0]?.name === "X-Team" ? undefined : `got ${JSON.stringify(res.services)}` ); diff --git a/src/api/types/agent-vault.ts b/src/api/types/agent-vault.ts index 2c8ee7f..18b42ca 100644 --- a/src/api/types/agent-vault.ts +++ b/src/api/types/agent-vault.ts @@ -178,6 +178,8 @@ export interface AgentVaultInlineHttpServiceInput { type: "http"; /** A name for the service, in lowercase letters, numbers and hyphens. Without one, the service goes by its first host. */ name?: string; + /** What the service is for, up to 256 characters. Agents in the session can read it, so never put a secret in it. */ + description?: string; /** The hosts the service covers, separated by commas, such as "api.github.com". A leading "*." matches exactly one label. */ hostPattern: string; /** The HTTP methods the service allows. Defaults to every method. */ @@ -196,6 +198,8 @@ export interface AgentVaultInlineMcpServiceInput { type: "mcp"; /** A name for the service, in lowercase letters, numbers and hyphens. Without one, the service goes by its host. */ name?: string; + /** What the service is for, up to 256 characters. Agents in the session can read it, so never put a secret in it. */ + description?: string; /** The MCP server's URL, such as "https://mcp.example.com/mcp". The service covers its host. */ serverUrl: string; transport: AgentVaultMcpTransport; @@ -204,27 +208,154 @@ export interface AgentVaultInlineMcpServiceInput { customHeaders?: AgentVaultInlineCustomHeader[]; } -/** A service sent with a session: an HTTP API or an MCP server. */ -export type AgentVaultInlineServiceInput = AgentVaultInlineHttpServiceInput | AgentVaultInlineMcpServiceInput; +/** How the proxy connects to a database. Each field defaults to true, and `ca` to the system's certificate authorities. */ +export interface AgentVaultInlineTlsInput { + enabled?: boolean; + /** Whether the proxy checks the database's certificate. */ + verify?: boolean; + /** PEM certificate(s) of the authority that signed the database's certificate, used only while `verify` is on. */ + ca?: string; +} -/** A service sent with a session, as Infisical returns it: without its token, password or values. */ -export interface AgentVaultInlineService { +/** The account the proxy logs in to a database or SSH server as. */ +export interface AgentVaultInlineLoginCredential { + type: "basic"; + username: string; + password?: string; +} + +/** An SSH key the proxy logs in with, in OpenSSH or PEM format and without a passphrase. */ +export interface AgentVaultInlineSshKeyCredential { + type: "ssh-key"; + username: string; + privateKey: string; + /** An SSH certificate for the key: the contents of its `-cert.pub` file. */ + certificate?: string; +} + +/** A PostgreSQL or MySQL database sent with a session. The agent reaches it with `infisical agent-vault connect`. */ +export interface AgentVaultInlineDatabaseServiceInput { + type: "postgres" | "mysql"; + /** The name the agent connects with, in lowercase letters, numbers and hyphens, up to 63 characters. */ + name: string; + /** What the service is for, up to 256 characters. Agents in the session can read it, so never put a secret in it. */ + description?: string; + /** The database server's host name or IP address, as the proxy reaches it. No scheme, port or path. */ + host: string; + /** Defaults to 5432 for postgres and 3306 for mysql. */ + port?: number; + /** The database the proxy logs in to. */ + database: string; + tls?: AgentVaultInlineTlsInput; + /** The account the proxy logs in as. */ + credential: AgentVaultInlineLoginCredential; +} + +/** An SSH server sent with a session. The agent reaches it with `infisical agent-vault connect`. */ +export interface AgentVaultInlineSshServiceInput { + type: "ssh"; + /** The name the agent connects with, in lowercase letters, numbers and hyphens, up to 63 characters. */ + name: string; + /** What the service is for, up to 256 characters. Agents in the session can read it, so never put a secret in it. */ + description?: string; + /** The SSH server's host name or IP address, as the proxy reaches it. No scheme, port or path. */ + host: string; + /** Defaults to 22. */ + port?: number; + /** The user and password, or the user and SSH key, the proxy logs in with. */ + credential: AgentVaultInlineLoginCredential | AgentVaultInlineSshKeyCredential; +} + +/** A service sent with a session: an HTTP API, an MCP server, or a database or SSH server. */ +export type AgentVaultInlineServiceInput = + | AgentVaultInlineHttpServiceInput + | AgentVaultInlineMcpServiceInput + | AgentVaultInlineDatabaseServiceInput + | AgentVaultInlineSshServiceInput; + +/** The kinds of service Agent Vault serves. Each returns only its own fields. */ +export type AgentVaultServiceType = "http" | "mcp" | "postgres" | "mysql" | "ssh"; + +/** How Agent Vault connects to a database service over TLS. */ +export interface AgentVaultServiceTls { + enabled: boolean; + /** Whether the server's certificate is checked. */ + verify: boolean; + /** The CA certificate the server's certificate is checked against, or null for the system's. */ + ca: string | null; +} + +/** The fields every service sent with a session returns, whatever its type. */ +export interface AgentVaultInlineServiceBase { id: string; - /** The name the service was sent with, or its first host if it was sent without one. */ + /** The name the service was sent with, or its host if it was sent without one. */ name: string; - type: "http" | "mcp"; - serverUrl: string | null; - transport: AgentVaultMcpTransport | null; + /** What the service is for, or null if it was sent without a description. */ + description?: string | null; + type: AgentVaultServiceType; +} + +/** A bearer credential's settings, without its value. */ +export interface AgentVaultInlineBearerCredentialSummary { + type: "bearer"; + headerName: string; + headerPrefix: string; +} + +/** The names of the headers the proxy adds, without their values. */ +export type AgentVaultInlineCustomHeaderSummary = { name: string; prefix: string }; + +/** An HTTP API sent with a session, as Infisical returns it. */ +export interface AgentVaultInlineHttpService extends AgentVaultInlineServiceBase { + type: "http"; hostPattern: string; /** The HTTP methods the service allows, or null for every method. */ allowedMethods: AgentVaultHttpMethod[] | null; /** The path prefixes the service allows, or null for every path. */ allowedPathPrefixes: string[] | null; - credential: { type: "bearer"; headerName: string; headerPrefix: string } | { type: "basic" } | { type: "passthrough" }; - customHeaders: { name: string; prefix: string }[]; + credential: AgentVaultInlineBearerCredentialSummary | { type: "basic" } | { type: "passthrough" }; + customHeaders: AgentVaultInlineCustomHeaderSummary[]; substitutions: { placeholder: string; surfaces: AgentVaultSubstitutionSurface[] }[]; } +/** An MCP server sent with a session, as Infisical returns it. */ +export interface AgentVaultInlineMcpService extends AgentVaultInlineServiceBase { + type: "mcp"; + serverUrl: string; + transport: AgentVaultMcpTransport; + hostPattern: string; + credential: AgentVaultInlineBearerCredentialSummary; + customHeaders: AgentVaultInlineCustomHeaderSummary[]; +} + +/** A Postgres or MySQL database sent with a session, as Infisical returns it. */ +export interface AgentVaultInlineDatabaseService extends AgentVaultInlineServiceBase { + type: "postgres" | "mysql"; + host: string; + port: number; + database: string; + tls: AgentVaultServiceTls; + credential: { type: "basic" }; +} + +/** An SSH server sent with a session, as Infisical returns it. */ +export interface AgentVaultInlineSshService extends AgentVaultInlineServiceBase { + type: "ssh"; + host: string; + port: number; + credential: { type: "basic" } | { type: "ssh-key"; hasCertificate: boolean }; +} + +/** + * A service sent with a session, as Infisical returns it: without its token, password or values. Narrow on + * `type` to read the fields that type has. + */ +export type AgentVaultInlineService = + | AgentVaultInlineHttpService + | AgentVaultInlineMcpService + | AgentVaultInlineDatabaseService + | AgentVaultInlineSshService; + export interface AgentVaultSession { id: string; label: string | null; @@ -313,25 +444,17 @@ export interface AgentVaultAccessBundleSummary extends AgentVaultAccessBundle { } /** - * A service in an Agent Vault access bundle. The SDK doesn't manage services, so a service's nested - * settings, such as its credential, are left as the API returns them. + * The fields every service in an Agent Vault access bundle returns, whatever its type. The SDK doesn't manage + * services, so a service's nested settings, such as its credential, are left as the API returns them. */ -export interface AgentVaultService { +export interface AgentVaultServiceBase { id: string; accessBundleId: string; name: string; - /** The kind of service, such as "http" or "mcp". */ - type: string; - serverUrl: string | null; - transport: string | null; - hostPattern: string; - allowedMethods: string[] | null; - allowedPathPrefixes: string[] | null; - /** The MCP tools that agents can call through the service, by exact name, or null for every tool. */ - allowedTools?: string[] | null; + /** What the service is for, or null if it has no description. */ + description?: string | null; + type: AgentVaultServiceType; credential: Record; - customHeaders: Record[]; - substitutions: Record[]; /** Only set for admins. */ variableReferences?: Record[]; /** Your own values for the service, or null when the admin sets the whole credential. */ @@ -340,6 +463,48 @@ export interface AgentVaultService { updatedAt: string; } +/** An HTTP API in an Agent Vault access bundle. */ +export interface AgentVaultHttpService extends AgentVaultServiceBase { + type: "http"; + hostPattern: string; + /** The HTTP methods the service allows, or null for every method. */ + allowedMethods: string[] | null; + /** The path prefixes the service allows, or null for every path. */ + allowedPathPrefixes: string[] | null; + customHeaders: Record[]; + substitutions: Record[]; +} + +/** An MCP server in an Agent Vault access bundle. */ +export interface AgentVaultMcpService extends AgentVaultServiceBase { + type: "mcp"; + serverUrl: string; + transport: string; + hostPattern: string; + customHeaders: Record[]; + /** The MCP tools that agents can call through the service, by exact name, or null for every tool. */ + allowedTools?: string[] | null; +} + +/** A Postgres or MySQL database in an Agent Vault access bundle. */ +export interface AgentVaultDatabaseService extends AgentVaultServiceBase { + type: "postgres" | "mysql"; + host: string; + port: number; + database: string; + tls: AgentVaultServiceTls; +} + +/** An SSH server in an Agent Vault access bundle. */ +export interface AgentVaultSshService extends AgentVaultServiceBase { + type: "ssh"; + host: string; + port: number; +} + +/** A service in an Agent Vault access bundle. Narrow on `type` to read the fields that type has. */ +export type AgentVaultService = AgentVaultHttpService | AgentVaultMcpService | AgentVaultDatabaseService | AgentVaultSshService; + /** An Agent Vault access bundle with its services. */ export interface AgentVaultAccessBundleWithServices extends AgentVaultAccessBundle { services: AgentVaultService[]; @@ -707,9 +872,10 @@ export type CreateAgentVaultSessionOptions = { accessBundle?: string; /** * Services the session carries itself, instead of or beside its access bundle's: up to 20. Only Agent Vault admins - * can send them. Each has the same fields as a service in an access bundle, except that its name is optional, its - * values can't use variables, it can't use OAuth or member-provided values, and an MCP server can't be limited to - * some of its tools. To limit an MCP server's tools, add it to an access bundle instead. + * can send them. Each has the same fields as a service in an access bundle, except that its name is optional for + * HTTP and MCP services (database and SSH services need one), its values can't use variables, it can't use OAuth + * or member-provided values, and an MCP server can't be limited to some of its tools. To limit an MCP server's + * tools, add it to an access bundle instead. */ services?: AgentVaultInlineServiceInput[]; /** How long the session lasts: a duration such as "30m", "8h" or "7d" (at least 1m), or "never". Defaults to "7d", or to "1h" for a session on behalf of a user. */