From 1be86cbe043483e0bda6469fd6c89c74b5e31149 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Mon, 9 Feb 2026 12:37:25 -0500 Subject: [PATCH] unsandbox tools: add 29 missing API endpoint tools Sessions: extendSession, boostSession, unboostSession Services: updateService, checkServicePort, getUpgradeProgress, getUpgradeLogs, updateServiceDomains, exportServiceEnv, restoreService Images: listOwnedImages, listSharedImages, listPublicImages, cloneImage System: getPoolStats API Keys: validateKey, getKeyInfo, updateKeySettings, listContacts, addContact, updateContact, removeContact, getAuditLog Live Tokens: mintToken, revokeToken, listTokens Total tools: 53 -> 85. Full coverage of openapi.yaml v2.2.0. Also adds PATCH to apiRequest method type union. --- packages/tools/official/unsandbox/block.ts | 55 + .../tools/official/unsandbox/package.json | 125 +++ .../tools/official/unsandbox/src/index.ts | 1000 ++++++++++++++++- 3 files changed, 1179 insertions(+), 1 deletion(-) diff --git a/packages/tools/official/unsandbox/block.ts b/packages/tools/official/unsandbox/block.ts index 5b6a915..827f026 100644 --- a/packages/tools/official/unsandbox/block.ts +++ b/packages/tools/official/unsandbox/block.ts @@ -3,6 +3,10 @@ * This file provides metadata for the blocks validator */ import { + addContact, + boostSession, + checkServicePort, + cloneImage, cloneSnapshot, // Service tools createService, @@ -23,15 +27,20 @@ import { executeCodeAsync, executeInService, executeInSession, + exportServiceEnv, + extendSession, freezeService, freezeSession, + getAuditLog, getClusterStatus, getImage, getImageTrustedKeys, // Job tools getJob, + getKeyInfo, // Languages tools getLanguages, + getPoolStats, getService, getServiceEnv, getServiceLogs, @@ -39,37 +48,55 @@ import { getShells, getSnapshot, getSystemStats, + getUpgradeLogs, + getUpgradeProgress, grantImageAccess, // System tools healthCheck, + listContacts, listImages, listJobs, + listOwnedImages, listPools, + listPublicImages, listServices, listSessions, + listSharedImages, listSnapshots, + listTokens, lockImage, lockService, lockSession, lockSnapshot, + mintToken, // Image tools publishImage, redeployService, + removeContact, + restoreService, restoreSession, restoreSnapshot, revokeImageAccess, + revokeToken, run, runAsync, setImageVisibility, setServiceEnv, spawnFromImage, transferImage, + unboostSession, unfreezeService, unfreezeSession, unlockImage, unlockService, unlockSession, unlockSnapshot, + updateContact, + updateKeySettings, + updateService, + updateServiceDomains, + // API Key tools + validateKey, } from './src/index.js'; export const block = { @@ -100,6 +127,9 @@ export const block = { createSessionSnapshot, restoreSession, deleteSession, + extendSession, + boostSession, + unboostSession, // Service tools createService, getService, @@ -116,6 +146,13 @@ export const block = { setServiceEnv, deleteServiceEnv, deleteService, + updateService, + checkServicePort, + getUpgradeProgress, + getUpgradeLogs, + updateServiceDomains, + exportServiceEnv, + restoreService, // Snapshot tools createSnapshot, getSnapshot, @@ -138,11 +175,29 @@ export const block = { spawnFromImage, getImageTrustedKeys, deleteImage, + listOwnedImages, + listSharedImages, + listPublicImages, + cloneImage, // System tools healthCheck, getClusterStatus, getSystemStats, listPools, + getPoolStats, + // API Key tools + validateKey, + getKeyInfo, + updateKeySettings, + listContacts, + addContact, + updateContact, + removeContact, + getAuditLog, + // Live Token tools + mintToken, + revokeToken, + listTokens, }, }; diff --git a/packages/tools/official/unsandbox/package.json b/packages/tools/official/unsandbox/package.json index aa3bece..386998a 100644 --- a/packages/tools/official/unsandbox/package.json +++ b/packages/tools/official/unsandbox/package.json @@ -126,6 +126,21 @@ "name": "deleteSession", "description": "Delete a session and release all associated resources." }, + { + "name": "extendSession", + "description": "Add additional time to a session's TTL.", + "healthCheck": { "skipExecution": true } + }, + { + "name": "boostSession", + "description": "Increase session vCPU and memory. Each vCPU includes 2GB RAM.", + "healthCheck": { "skipExecution": true } + }, + { + "name": "unboostSession", + "description": "Return session to base vCPU and memory allocation.", + "healthCheck": { "skipExecution": true } + }, { "name": "createService", "description": "Create a long-running service with persistent state, networking, and auto-restart capabilities." @@ -186,6 +201,41 @@ "name": "deleteService", "description": "Delete a service and release all associated resources." }, + { + "name": "updateService", + "description": "Update service settings: vCPU resize, auto-unfreeze on HTTP, or freeze page display.", + "healthCheck": { "skipExecution": true } + }, + { + "name": "checkServicePort", + "description": "Check if a specific port is listening inside the service container.", + "healthCheck": { "skipExecution": true } + }, + { + "name": "getUpgradeProgress", + "description": "Get OS upgrade progress including state, percentage, and version chain.", + "healthCheck": { "skipExecution": true } + }, + { + "name": "getUpgradeLogs", + "description": "Get OS upgrade log output for a service.", + "healthCheck": { "skipExecution": true } + }, + { + "name": "updateServiceDomains", + "description": "Manage custom domain names for a service (replace, add, or remove domains).", + "healthCheck": { "skipExecution": true } + }, + { + "name": "exportServiceEnv", + "description": "Export all environment variables as a .env formatted string.", + "healthCheck": { "skipExecution": true } + }, + { + "name": "restoreService", + "description": "Restore a service to a previous snapshot state.", + "healthCheck": { "skipExecution": true } + }, { "name": "createSnapshot", "description": "Create a snapshot from any source (session, service, or existing snapshot)." @@ -266,6 +316,23 @@ "name": "deleteImage", "description": "Delete an image and free associated storage." }, + { + "name": "listOwnedImages", + "description": "List only images owned by the authenticated API key." + }, + { + "name": "listSharedImages", + "description": "List images shared with you via trusted_keys." + }, + { + "name": "listPublicImages", + "description": "List all public images available in the marketplace." + }, + { + "name": "cloneImage", + "description": "Create a copy of an image owned by you with private visibility.", + "healthCheck": { "skipExecution": true } + }, { "name": "healthCheck", "description": "Check the health status of the Unsandbox API service." @@ -281,6 +348,64 @@ { "name": "listPools", "description": "List available execution pools with their configurations and current status." + }, + { + "name": "getPoolStats", + "description": "Get detailed stats for a specific pool including load, memory, and network breakdown.", + "healthCheck": { "skipExecution": true } + }, + { + "name": "validateKey", + "description": "Validate API key pair and retrieve rate limits, concurrency, and expiration." + }, + { + "name": "getKeyInfo", + "description": "Get metadata for the authenticated API key including email, tier, and contacts." + }, + { + "name": "updateKeySettings", + "description": "Update email or sudo settings for the authenticated API key.", + "healthCheck": { "skipExecution": true } + }, + { + "name": "listContacts", + "description": "List all contacts associated with the authenticated API key." + }, + { + "name": "addContact", + "description": "Add a contact with role (owner, operator, reader). Sends invitation email.", + "healthCheck": { "skipExecution": true } + }, + { + "name": "updateContact", + "description": "Change a contact's role. Only owners can manage contacts.", + "healthCheck": { "skipExecution": true } + }, + { + "name": "removeContact", + "description": "Remove a contact from the API key.", + "healthCheck": { "skipExecution": true } + }, + { + "name": "getAuditLog", + "description": "Retrieve audit log entries for all major operations on the API key." + }, + { + "name": "mintToken", + "description": "Create a short-lived token for client-side use (e.g., browser WebSocket).", + "healthCheck": { + "testParams": {}, + "cleanup": [{ "tool": "revokeToken", "mapping": { "token": "token" } }] + } + }, + { + "name": "revokeToken", + "description": "Immediately invalidate a previously minted live token.", + "healthCheck": { "skipExecution": true } + }, + { + "name": "listTokens", + "description": "List all active live tokens for the authenticated API key." } ] }, diff --git a/packages/tools/official/unsandbox/src/index.ts b/packages/tools/official/unsandbox/src/index.ts index a3e83b9..d7f3f46 100644 --- a/packages/tools/official/unsandbox/src/index.ts +++ b/packages/tools/official/unsandbox/src/index.ts @@ -117,7 +117,7 @@ function computeSignature( * Make an authenticated request to the Unsandbox API */ async function apiRequest( - method: 'GET' | 'POST' | 'PUT' | 'DELETE', + method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE', path: string, body?: unknown ): Promise { @@ -1299,6 +1299,127 @@ export const deleteSession = tool({ }, }); +// ============================================================================ +// Extend Session TTL +// ============================================================================ + +export interface ExtendSessionInput { + session_id: string; + seconds?: number; +} + +export interface ExtendSessionResult { + ttl: number; +} + +/** + * Extend a session's time-to-live. + */ +export const extendSession = tool({ + description: 'Add additional time to a session\'s TTL. Default adds 3600 seconds (1 hour).', + inputSchema: jsonSchema({ + type: 'object', + properties: { + session_id: { type: 'string', description: 'The session ID' }, + seconds: { + type: 'number', + description: 'Seconds to add to TTL. Default: 3600', + }, + }, + required: ['session_id'], + additionalProperties: false, + }), + async execute(input: ExtendSessionInput): Promise { + const body: Record = {}; + if (input.seconds !== undefined) body.seconds = input.seconds; + + return apiRequest( + 'POST', + `/sessions/${encodeURIComponent(input.session_id)}/extend`, + body + ); + }, +}); + +// ============================================================================ +// Boost Session Resources +// ============================================================================ + +export interface BoostSessionInput { + session_id: string; + vcpu?: number; +} + +export interface BoostSessionResult { + success: boolean; + message: string; + session: { + session_id: string; + status: string; + vcpu: number; + }; +} + +/** + * Boost session vCPU and memory. + */ +export const boostSession = tool({ + description: 'Increase session vCPU and memory. Each vCPU includes 2GB RAM. Consumes additional concurrency slots.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + session_id: { type: 'string', description: 'The session ID' }, + vcpu: { + type: 'number', + description: 'Target vCPU count (1-8). Default: 2', + }, + }, + required: ['session_id'], + additionalProperties: false, + }), + async execute(input: BoostSessionInput): Promise { + const body: Record = {}; + if (input.vcpu !== undefined) { + if (input.vcpu < 1 || input.vcpu > 8) { + throw new Error('vCPU must be between 1 and 8'); + } + body.vcpu = input.vcpu; + } + + return apiRequest( + 'POST', + `/sessions/${encodeURIComponent(input.session_id)}/boost`, + body + ); + }, +}); + +// ============================================================================ +// Unboost Session Resources +// ============================================================================ + +/** + * Return session to base resources. + */ +export const unboostSession = tool({ + description: 'Reduce session back to default vCPU and memory allocation.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + session_id: { type: 'string', description: 'The session ID' }, + }, + required: ['session_id'], + additionalProperties: false, + }), + async execute(input: SessionIdInput): Promise { + return apiRequest( + 'POST', + `/sessions/${encodeURIComponent(input.session_id)}/unboost`, + {} + ); + }, +}); + // ============================================================================ // Services // ============================================================================ @@ -1771,6 +1892,318 @@ export const deleteService = tool({ }, }); +// ============================================================================ +// Update Service Settings +// ============================================================================ + +export interface UpdateServiceInput { + service_id: string; + vcpu?: number; + unfreeze_on_demand?: boolean; + show_freeze_page?: boolean; +} + +export interface UpdateServiceResult { + success: boolean; + service_id: string; + vcpu?: number; + unfreeze_on_demand?: boolean; + show_freeze_page?: boolean; +} + +/** + * Update service settings (resize, auto-unfreeze, freeze page). + */ +export const updateService = tool({ + description: 'Update service settings. Supports vCPU resize (1-8, each gets 2GB RAM), auto-unfreeze on HTTP request, and branded freeze page. One setting per request.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + service_id: { type: 'string', description: 'The service ID' }, + vcpu: { + type: 'number', + description: 'Target vCPU count (1-8, each gets 2GB RAM)', + }, + unfreeze_on_demand: { + type: 'boolean', + description: 'Auto-unfreeze on first HTTP request when frozen', + }, + show_freeze_page: { + type: 'boolean', + description: 'Show branded freeze page instead of 502 when frozen', + }, + }, + required: ['service_id'], + additionalProperties: false, + }), + async execute(input: UpdateServiceInput): Promise { + const body: Record = {}; + if (input.vcpu !== undefined) { + if (input.vcpu < 1 || input.vcpu > 8) { + throw new Error('vCPU must be between 1 and 8'); + } + body.vcpu = input.vcpu; + } + if (input.unfreeze_on_demand !== undefined) body.unfreeze_on_demand = input.unfreeze_on_demand; + if (input.show_freeze_page !== undefined) body.show_freeze_page = input.show_freeze_page; + + if (Object.keys(body).length === 0) { + throw new Error('Must provide vcpu, unfreeze_on_demand, or show_freeze_page'); + } + + return apiRequest( + 'PATCH', + `/services/${encodeURIComponent(input.service_id)}`, + body + ); + }, +}); + +// ============================================================================ +// Check Service Port +// ============================================================================ + +export interface CheckServicePortInput { + service_id: string; + port: number; +} + +export interface CheckServicePortResult { + listening: boolean; + port: number; + service_id: string; +} + +/** + * Check if a port is listening inside the service container. + */ +export const checkServicePort = tool({ + description: 'Check if a specific port is listening inside the service container.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + service_id: { type: 'string', description: 'The service ID' }, + port: { + type: 'number', + description: 'Port number to check (1-65535)', + }, + }, + required: ['service_id', 'port'], + additionalProperties: false, + }), + async execute(input: CheckServicePortInput): Promise { + if (input.port < 1 || input.port > 65535) { + throw new Error('Port must be between 1 and 65535'); + } + + return apiRequest( + 'GET', + `/services/${encodeURIComponent(input.service_id)}/port?port=${input.port}`, + undefined + ); + }, +}); + +// ============================================================================ +// Service Upgrade Progress +// ============================================================================ + +export interface UpgradeProgressResult { + upgrading: boolean; + state?: string; + progress?: number; + current_version?: string; + target_version?: string; +} + +/** + * Get OS upgrade progress for a service. + */ +export const getUpgradeProgress = tool({ + description: 'Get OS upgrade progress for a service including state, progress percentage, and version chain.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + service_id: { type: 'string', description: 'The service ID' }, + }, + required: ['service_id'], + additionalProperties: false, + }), + async execute(input: ServiceIdInput): Promise { + return apiRequest( + 'GET', + `/services/${encodeURIComponent(input.service_id)}/upgrade/progress`, + undefined + ); + }, +}); + +// ============================================================================ +// Service Upgrade Logs +// ============================================================================ + +export interface GetUpgradeLogsInput { + service_id: string; + lines?: number; +} + +export interface UpgradeLogsResult { + log: string; + message?: string; +} + +/** + * Get OS upgrade logs for a service. + */ +export const getUpgradeLogs = tool({ + description: 'Get OS upgrade log output for a service. Optionally limit to last N lines.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + service_id: { type: 'string', description: 'The service ID' }, + lines: { + type: 'number', + description: 'Limit to last N lines (default: all)', + }, + }, + required: ['service_id'], + additionalProperties: false, + }), + async execute(input: GetUpgradeLogsInput): Promise { + let path = `/services/${encodeURIComponent(input.service_id)}/upgrade/logs`; + if (input.lines !== undefined) { + path += `?lines=${input.lines}`; + } + + return apiRequest('GET', path, undefined); + }, +}); + +// ============================================================================ +// Update Service Domains +// ============================================================================ + +export interface UpdateServiceDomainsInput { + service_id: string; + custom_domains?: string[]; + add?: string[]; + remove?: string[]; +} + +export interface UpdateServiceDomainsResult { + success: boolean; + service_id: string; + custom_domains: string[]; +} + +/** + * Update custom domains for a service. + */ +export const updateServiceDomains = tool({ + description: 'Manage custom domain names for a service. Use custom_domains to replace all, add to append, or remove to delete specific domains.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + service_id: { type: 'string', description: 'The service ID' }, + custom_domains: { + type: 'array', + items: { type: 'string' }, + description: 'Replace all domains with this list', + }, + add: { + type: 'array', + items: { type: 'string' }, + description: 'Add these domains to existing list', + }, + remove: { + type: 'array', + items: { type: 'string' }, + description: 'Remove these domains from existing list', + }, + }, + required: ['service_id'], + additionalProperties: false, + }), + async execute(input: UpdateServiceDomainsInput): Promise { + const body: Record = {}; + if (input.custom_domains) body.custom_domains = input.custom_domains; + if (input.add) body.add = input.add; + if (input.remove) body.remove = input.remove; + + if (Object.keys(body).length === 0) { + throw new Error('Must provide custom_domains, add, or remove'); + } + + return apiRequest( + 'PUT', + `/services/${encodeURIComponent(input.service_id)}/domains`, + body + ); + }, +}); + +// ============================================================================ +// Export Service Env +// ============================================================================ + +export interface ExportServiceEnvResult { + env: string; +} + +/** + * Export service environment variables as .env format. + */ +export const exportServiceEnv = tool({ + description: 'Export all environment variables as a .env formatted string. Requires HMAC authentication.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + service_id: { type: 'string', description: 'The service ID' }, + }, + required: ['service_id'], + additionalProperties: false, + }), + async execute(input: ServiceIdInput): Promise { + return apiRequest( + 'POST', + `/services/${encodeURIComponent(input.service_id)}/env/export`, + {} + ); + }, +}); + +// ============================================================================ +// Restore Service from Snapshot +// ============================================================================ + +export interface RestoreServiceInput { + service_id: string; + snapshot_id: string; +} + +/** + * Restore a service from a snapshot. + */ +export const restoreService = tool({ + description: 'Restore a service to a previous snapshot state.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + service_id: { type: 'string', description: 'The service ID' }, + snapshot_id: { type: 'string', description: 'The snapshot ID to restore from' }, + }, + required: ['service_id', 'snapshot_id'], + additionalProperties: false, + }), + async execute(input: RestoreServiceInput): Promise { + return apiRequest( + 'POST', + `/services/${encodeURIComponent(input.service_id)}/restore`, + { snapshot_id: input.snapshot_id } + ); + }, +}); + // ============================================================================ // Snapshots // ============================================================================ @@ -2384,6 +2817,104 @@ export const deleteImage = tool({ }, }); +// ============================================================================ +// List Owned Images +// ============================================================================ + +/** + * List images owned by the authenticated API key. + */ +export const listOwnedImages = tool({ + description: 'List only images owned by the authenticated API key. Includes management fields like trusted_keys.', + inputSchema: jsonSchema>({ + type: 'object', + properties: {}, + additionalProperties: false, + }), + async execute(): Promise { + const result = await apiRequest('GET', '/images/owned', undefined); + return { images: result.images || [] }; + }, +}); + +// ============================================================================ +// List Shared Images +// ============================================================================ + +/** + * List images shared with the authenticated API key. + */ +export const listSharedImages = tool({ + description: 'List images shared with you via trusted_keys (images you can use but don\'t own).', + inputSchema: jsonSchema>({ + type: 'object', + properties: {}, + additionalProperties: false, + }), + async execute(): Promise { + const result = await apiRequest('GET', '/images/shared', undefined); + return { images: result.images || [] }; + }, +}); + +// ============================================================================ +// List Public Images +// ============================================================================ + +/** + * List all public images (marketplace). + */ +export const listPublicImages = tool({ + description: 'List all public images available in the marketplace.', + inputSchema: jsonSchema>({ + type: 'object', + properties: {}, + additionalProperties: false, + }), + async execute(): Promise { + const result = await apiRequest('GET', '/images/public', undefined); + return { images: result.images || [] }; + }, +}); + +// ============================================================================ +// Clone Image +// ============================================================================ + +export interface CloneImageInput { + image_id: string; + name?: string; + description?: string; +} + +/** + * Clone an image to create your own copy. + */ +export const cloneImage = tool({ + description: 'Create a copy of an image. The cloned image will be owned by you with private visibility.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + image_id: { type: 'string', description: 'The image ID to clone' }, + name: { type: 'string', description: 'Name for the cloned image (defaults to original + " (copy)")' }, + description: { type: 'string', description: 'Description for the cloned image' }, + }, + required: ['image_id'], + additionalProperties: false, + }), + async execute(input: CloneImageInput): Promise { + const body: Record = {}; + if (input.name) body.name = input.name; + if (input.description) body.description = input.description; + + return apiRequest( + 'POST', + `/images/${encodeURIComponent(input.image_id)}/clone`, + body + ); + }, +}); + // ============================================================================ // System // ============================================================================ @@ -2504,6 +3035,445 @@ export const listPools = tool({ }, }); +// ============================================================================ +// Get Pool Stats +// ============================================================================ + +export interface GetPoolStatsInput { + pool_id: string; +} + +export interface PoolStatsResult { + mode: string; + pool_size: number; + total_containers: number; + available: number; + allocated: number; + spawning: number; + network_mode: string; + load_avg?: { + load1: string; + load5: string; + load15: string; + }; + memory?: { + total_mb: number; + used_mb: number; + available_mb: number; + used_percent: number; + }; + network_breakdown?: { + zerotrust?: { total: number; available: number; allocated: number }; + semitrusted?: { total: number; available: number; allocated: number; services?: number }; + }; +} + +/** + * Get detailed stats for a specific pool. + */ +export const getPoolStats = tool({ + description: 'Get detailed stats for a specific pool including load averages, memory usage, and network breakdown.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + pool_id: { + type: 'string', + description: 'Pool identifier (e.g., ai, cammy)', + }, + }, + required: ['pool_id'], + additionalProperties: false, + }), + async execute(input: GetPoolStatsInput): Promise { + return apiRequest( + 'GET', + `/pools/${encodeURIComponent(input.pool_id)}/stats`, + undefined + ); + }, +}); + +// ============================================================================ +// API Keys - Validate +// ============================================================================ + +export interface ValidateKeyResult { + valid: boolean; + public_key: string; + rate_per_minute: number; + burst: number; + concurrency_limit: number; + expires_at: string | null; + tier: string; +} + +/** + * Validate API key pair. + */ +export const validateKey = tool({ + description: 'Validate your API key pair and retrieve its configuration including rate limits, concurrency limits, and expiration status.', + inputSchema: jsonSchema>({ + type: 'object', + properties: {}, + additionalProperties: false, + }), + async execute(): Promise { + return apiRequest('POST', '/keys/validate', {}); + }, +}); + +// ============================================================================ +// API Keys - Get Self +// ============================================================================ + +export interface KeyInfoResult { + key: string; + email: string; + sudo_required: boolean; + tier: number; + created_at: string; + contacts_count: number; +} + +/** + * Get current API key info. + */ +export const getKeyInfo = tool({ + description: 'Get metadata for the authenticated API key including email, tier, and contacts count.', + inputSchema: jsonSchema>({ + type: 'object', + properties: {}, + additionalProperties: false, + }), + async execute(): Promise { + return apiRequest('GET', '/keys/self', undefined); + }, +}); + +// ============================================================================ +// API Keys - Update Self +// ============================================================================ + +export interface UpdateKeyInput { + email?: string; + sudo_required?: boolean; +} + +export interface UpdateKeyResult { + success: boolean; + key: { + key: string; + email: string; + sudo_required: boolean; + }; +} + +/** + * Update API key settings. + */ +export const updateKeySettings = tool({ + description: 'Update email or sudo settings for the authenticated API key.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + email: { type: 'string', description: 'New email address' }, + sudo_required: { + type: 'boolean', + description: 'Require OTP for destructive operations', + }, + }, + additionalProperties: false, + }), + async execute(input: UpdateKeyInput): Promise { + const body: Record = {}; + if (input.email !== undefined) body.email = input.email; + if (input.sudo_required !== undefined) body.sudo_required = input.sudo_required; + + if (Object.keys(body).length === 0) { + throw new Error('Must provide email or sudo_required'); + } + + return apiRequest('PATCH', '/keys/self', body); + }, +}); + +// ============================================================================ +// API Keys - Contacts +// ============================================================================ + +export interface Contact { + email: string; + role: 'owner' | 'operator' | 'reader'; + verified: boolean; + invited_by?: string; + created_at?: string; +} + +export interface ListContactsResult { + contacts: Contact[]; +} + +/** + * List contacts for the authenticated API key. + */ +export const listContacts = tool({ + description: 'List all contacts associated with the authenticated API key.', + inputSchema: jsonSchema>({ + type: 'object', + properties: {}, + additionalProperties: false, + }), + async execute(): Promise { + return apiRequest('GET', '/keys/self/contacts', undefined); + }, +}); + +// ============================================================================ +// API Keys - Add Contact +// ============================================================================ + +export interface AddContactInput { + email: string; + role?: 'owner' | 'operator' | 'reader'; +} + +export interface AddContactResult { + success: boolean; + contact: Contact; +} + +/** + * Add a contact to the API key. + */ +export const addContact = tool({ + description: 'Add a new contact to the API key. Sends an invitation email. Roles: owner (full control), operator (manage services), reader (view-only).', + inputSchema: jsonSchema({ + type: 'object', + properties: { + email: { type: 'string', description: 'Contact email address' }, + role: { + type: 'string', + enum: ['owner', 'operator', 'reader'], + description: 'Contact role. Default: reader', + }, + }, + required: ['email'], + additionalProperties: false, + }), + async execute(input: AddContactInput): Promise { + const body: Record = { email: input.email }; + if (input.role) body.role = input.role; + + return apiRequest('POST', '/keys/self/contacts', body); + }, +}); + +// ============================================================================ +// API Keys - Update Contact Role +// ============================================================================ + +export interface UpdateContactInput { + email: string; + role: 'owner' | 'operator' | 'reader'; +} + +export interface UpdateContactResult { + success: boolean; + contact: { + email: string; + role: string; + }; +} + +/** + * Update a contact's role. + */ +export const updateContact = tool({ + description: 'Change a contact\'s role. Only owners can manage contacts.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + email: { type: 'string', description: 'Contact email address' }, + role: { + type: 'string', + enum: ['owner', 'operator', 'reader'], + description: 'New role for the contact', + }, + }, + required: ['email', 'role'], + additionalProperties: false, + }), + async execute(input: UpdateContactInput): Promise { + return apiRequest( + 'PATCH', + `/keys/self/contacts/${encodeURIComponent(input.email)}`, + { role: input.role } + ); + }, +}); + +// ============================================================================ +// API Keys - Remove Contact +// ============================================================================ + +export interface RemoveContactInput { + email: string; +} + +export interface RemoveContactResult { + success: boolean; +} + +/** + * Remove a contact from the API key. + */ +export const removeContact = tool({ + description: 'Remove a contact from the API key. Only owners can manage contacts.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + email: { type: 'string', description: 'Contact email to remove' }, + }, + required: ['email'], + additionalProperties: false, + }), + async execute(input: RemoveContactInput): Promise { + return apiRequest( + 'DELETE', + `/keys/self/contacts/${encodeURIComponent(input.email)}`, + undefined + ); + }, +}); + +// ============================================================================ +// API Keys - Audit Log +// ============================================================================ + +export interface AuditLogEntry { + action: string; + resource_type: string; + resource_id: string; + result: 'success' | 'failure'; + client_ip: string; + metadata?: Record; + timestamp: string; +} + +export interface AuditLogResult { + entries: AuditLogEntry[]; + count: number; +} + +/** + * Get audit log for the authenticated API key. + */ +export const getAuditLog = tool({ + description: 'Retrieve audit log entries for the authenticated API key. Logs all major operations: create, destroy, freeze, unfreeze, snapshot, etc.', + inputSchema: jsonSchema>({ + type: 'object', + properties: {}, + additionalProperties: false, + }), + async execute(): Promise { + return apiRequest('GET', '/keys/self/audit-log', undefined); + }, +}); + +// ============================================================================ +// Live Tokens - Mint +// ============================================================================ + +export interface MintTokenInput { + ttl?: number; +} + +export interface MintTokenResult { + token: string; + expires_at: string; +} + +/** + * Mint a short-lived live token. + */ +export const mintToken = tool({ + description: 'Create a short-lived token for client-side use (e.g., browser WebSocket connections). Inherits parent key permissions.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + ttl: { + type: 'number', + description: 'Token TTL in seconds', + }, + }, + additionalProperties: false, + }), + async execute(input: MintTokenInput): Promise { + const body: Record = {}; + if (input.ttl !== undefined) body.ttl = input.ttl; + + return apiRequest('POST', '/mint-token', body); + }, +}); + +// ============================================================================ +// Live Tokens - Revoke +// ============================================================================ + +export interface RevokeTokenInput { + token: string; +} + +export interface RevokeTokenResult { + success: boolean; +} + +/** + * Revoke a live token. + */ +export const revokeToken = tool({ + description: 'Immediately invalidate a previously minted live token.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + token: { type: 'string', description: 'The token to revoke' }, + }, + required: ['token'], + additionalProperties: false, + }), + async execute(input: RevokeTokenInput): Promise { + return apiRequest('POST', '/revoke-token', { token: input.token }); + }, +}); + +// ============================================================================ +// Live Tokens - List +// ============================================================================ + +export interface TokenInfo { + token: string; + expires_at: string; +} + +export interface ListTokensResult { + tokens: TokenInfo[]; +} + +/** + * List active live tokens. + */ +export const listTokens = tool({ + description: 'List all active (non-expired, non-revoked) live tokens for the authenticated API key.', + inputSchema: jsonSchema>({ + type: 'object', + properties: {}, + additionalProperties: false, + }), + async execute(): Promise { + return apiRequest('POST', '/list-tokens', {}); + }, +}); + // ============================================================================ // Default Export // ============================================================================ @@ -2533,6 +3503,9 @@ export default { createSessionSnapshot, restoreSession, deleteSession, + extendSession, + boostSession, + unboostSession, // Services createService, getService, @@ -2549,6 +3522,13 @@ export default { setServiceEnv, deleteServiceEnv, deleteService, + updateService, + checkServicePort, + getUpgradeProgress, + getUpgradeLogs, + updateServiceDomains, + exportServiceEnv, + restoreService, // Snapshots createSnapshot, getSnapshot, @@ -2571,9 +3551,27 @@ export default { spawnFromImage, getImageTrustedKeys, deleteImage, + listOwnedImages, + listSharedImages, + listPublicImages, + cloneImage, // System healthCheck, getClusterStatus, getSystemStats, listPools, + getPoolStats, + // API Keys + validateKey, + getKeyInfo, + updateKeySettings, + listContacts, + addContact, + updateContact, + removeContact, + getAuditLog, + // Live Tokens + mintToken, + revokeToken, + listTokens, };