TypeScript client for the OpenHermit gateway and agent APIs. Targets external integrations that need to provision agents, manage their configuration, and talk to live sessions over HTTP/WS without re-implementing the wire format.
npm install @openhermit/sdkimport { GatewayClient } from '@openhermit/sdk';
const gw = new GatewayClient({
baseUrl: 'https://test.openhermit.ai',
token: process.env.OPENHERMIT_ADMIN_TOKEN,
});
const agents = await gw.listAgents();There are two distinct caller identities. Pick the right token for each call:
- Admin token — long-lived value from gateway config. Authorises every
/api/admin/...route plus all per-agent CRUD. Use for back-office / server-to-server provisioning. - User JWT — issued either via
POST /api/auth/token(device-key exchange) orGatewayClient.issueUserToken(...)(admin-minted on behalf of a user authenticated by your own system). Scoped to that user's agents.
GatewayClient accepts either as token. AgentClient (live session calls)
expects a user JWT.
Auth / identity
GatewayClient.issueUserToken({ baseUrl, adminToken, channel, channelUserId, displayName?, purpose?, ttlSeconds? })— admin-only mint of a user JWT for a(channel, channelUserId)identity. Use to bridge users authenticated by an external system into OpenHermit without device-key exchange.purpose: 'session'(default) returns a normal 24h credential.purpose: 'exchange'returns a single-use short-lived token (≤600s, default 120s) intended for the web/connect#token=…deep-link flow.
GatewayClient.exchangeConnectToken({ baseUrl, token })— swap anexchangetoken for a 24h session JWT. The token is the credential (no admin token needed) and is rejected on a second redemption. Backs the/connectSSO flow described below.GatewayClient.linkUserIdentity({ baseUrl, adminToken, userId, channel, channelUserId })— admin-only: attach a(channel, channelUserId)pair to an existing user. Stitches identities together — e.g. when the same human has logged in via two channels and you want both pointing at one canonical user record. If the pair is already linked to a different user, the gateway reassigns it and prunes the orphan source user.
For platforms that already authenticate their own users, the simplest way to drop them into the OpenHermit web UI is a short-lived exchange JWT:
- Server-side (on the trusted platform), mint an exchange token:
const { token } = await GatewayClient.issueUserToken({ baseUrl: 'https://hermit.example.com', adminToken: process.env.OPENHERMIT_ADMIN_TOKEN!, channel: 'my-platform', channelUserId: user.id, displayName: user.name, purpose: 'exchange', }); const url = `https://hermit.example.com/connect?agent_id=${agentId}#token=${token}`;
- Redirect the user to
url. The web app reads the token from the URL fragment (so it never hits the server log orreferer), strips it, swaps it viaPOST /api/auth/exchange, and persists the resulting session JWT inlocalStorage. - The exchange token is single-use — a replay (e.g. someone scraping the URL out of browser history) is rejected with 401.
The browser's existing device-key identity, if any, is left untouched so the same browser can hold both an SSO-issued identity and a device-key identity.
Catalog
listProviders(): ProviderCatalogEntry[]— provider/model snapshot from pi-ai.
Agents
listAgents()— admin: every agent in the fleet.createAgent(request)/deleteAgent(agentId).manageAgent(agentId, 'start' | 'stop' | 'restart' | 'enable' | 'disable').agentHealth(agentId).
Per-agent config
getAgentConfig(agentId)/putAgentConfig(agentId, config).getAgentSecurity(agentId)/putAgentSecurity(agentId, policy).
Secrets (per-agent, encrypted server-side)
getAgentSecrets(agentId)— returns masked previews +passThroughflags.setAgentSecret(agentId, name, value, { passThrough? }).deleteAgentSecret(agentId, name).
Channels (built-in adapters and external webhook channels)
listAgentChannels(agentId)— all channels with runtime status.createExternalChannel(agentId, input)— returns the plaintext token once.updateAgentChannel(agentId, channelId, input).deleteAgentChannel(agentId, channelId).
Sandboxes (per-agent exec environments)
listSandboxes(agentId).createSandbox(agentId, input).deleteSandbox(agentId, alias).
Skills (admin)
listSkills()/scanSkills()/getSkill(...).registerSkill(input)/deleteSkill(skillId).listSkillAssignments()/enableSkill(skillId, agentId)/disableSkill(skillId, agentId).
MCP servers (admin)
listMcpServers()/registerMcpServer(input).listMcpAssignments()/enableMcpServer(mcpServerId, agentId)/disableMcpServer(...).
Instructions (per-agent prompts)
listInstructions(agentId)/getInstruction(agentId, key).setInstruction(agentId, key, content)/deleteInstruction(agentId, key).fanoutInstruction(input)— admin: push the same key to many agents.
Schedules (per-agent crons)
listSchedules(agentId)/createSchedule(agentId, input).updateSchedule(agentId, scheduleId, input)/deleteSchedule(agentId, scheduleId).listScheduleRuns(agentId, scheduleId, limit?).
Policies (per-agent resource access)
listPolicies(agentId, resourceType?).upsertPolicy(agentId, input)/deletePolicy(agentId, resourceType, resourceKey).
Approvals (per-agent tool-call approvals)
listApprovalRequests(agentId, status?)/getApprovalRequest(agentId, id).reviewApprovalRequest(agentId, id, { decision, ... }).
Gateway-level config (admin)
getGatewayConfig()/putGatewayConfig(config).getAdminStats().
For talking to a running agent over HTTP/SSE/WS as a user.
openSession(spec)/listSessions(query?)/listSessionMessages(sessionId).spec.customInstruction?: string— optional per-session prompt addendum. Stored once on the session row at create time and appended to the system prompt as a dedicated section after agent-level instructions. Immutable for the lifetime of the session (subsequentopenSessioncalls for the samesessionIdignore the field).
postMessage(...)/appendMessage(...)— non-streaming.postMessageSync(...)— wait for the assistant turn to complete.postMessageStream(...)— async iterable of session events (SSE under the hood).submitApproval(...)/checkpointSession(...).reviewApprovalRequest(...)/reviewApprovalRequestByShortId(...).
sessionOpen/sessionMessage/sessionApprove/sessionCheckpoint.sessionList/sessionHistory.subscribe(sessionId, lastEventId?)/unsubscribe(sessionId).
The gateway exposes more routes than the SDK currently wraps. If you need any
of these, drop a method in packages/sdk/src/index.ts matching the existing
style. Prioritised by the hole they represent:
Multi-tenant / membership
POST /api/users— create users from outside the device-key flow.GET /api/agents/:agentId/members— list users with access to an agent.POST /api/agents/:agentId/members— share an agent with a user.DELETE /api/agents/:agentId/members/:userId— revoke access.POST /api/agents/:agentId/users/:userId/promote-to-owner.GET /api/agents/:agentId/ownership/GET /api/agents/:agentId/info.GET /api/users/me/agents— current-user's accessible agents (currently onlylistAgents()admin variant is wrapped).
Admin observability
GET /health,GET /metrics.GET /api/admin/agents/fleet(admin fleet dashboard).GET /api/admin/sandboxes(cross-agent sandbox view).GET /api/admin/users/GET /api/admin/users/:userId/identities/GET /api/admin/users/:userId/agents.GET /api/admin/logs(server log tail).GET /api/admin/schedules(cross-agent schedule view).GET /api/sandbox-presets(read-only catalog of sandbox templates).
Sessions / events
DELETE /api/agents/:agentId/sessions/:sessionId— delete a session.GET /api/agents/:agentId/sessions/:sessionId/events— raw SSE event stream (AgentClient.postMessageStreamcovers the message-driven case but not arbitrary subscription).
Channels
POST /api/agents/:agentId/channels/:namespace/webhook— inbound webhook delivery. External integrations call this directly with the token fromcreateExternalChannel; the SDK currently doesn't wrap it because typical consumers are receiving webhooks, not posting them programmatically.
Schedules
POST /api/agents/:agentId/schedules/:scheduleId/trigger— manual fire of a cron entry (useful for "test run now" buttons).
Approvals
POST /api/agents/:agentId/approvals/by-short/:shortId/review— review by short-id (the long-id form is wrapped).
Semver-ish. Additive methods are minor/patch bumps; type-breaking changes are
minor (we're pre-1.0). The ESM build is published from dist/; the
development export condition is stripped at pack time via prepack so
in-monorepo dev keeps reading src/ directly without polluting the published
tarball.
See packages/sdk/src/index.ts for the
authoritative type surface.