diff --git a/apps/web/next-env.d.ts b/apps/web/next-env.d.ts index c4b7818..9edff1c 100644 --- a/apps/web/next-env.d.ts +++ b/apps/web/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/dev/types/routes.d.ts"; +import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/web/src/app/api/agents/[id]/route.ts b/apps/web/src/app/api/agents/[id]/route.ts index 1870d42..f70ebb5 100644 --- a/apps/web/src/app/api/agents/[id]/route.ts +++ b/apps/web/src/app/api/agents/[id]/route.ts @@ -60,6 +60,7 @@ export async function GET(_request: NextRequest, context: RouteContext) { id: true, name: true, description: true, + parameters: true, package: { select: { npmPackageName: true, diff --git a/apps/web/src/app/api/collections/[id]/tools/route.ts b/apps/web/src/app/api/collections/[id]/tools/route.ts index 3d305d5..4255634 100644 --- a/apps/web/src/app/api/collections/[id]/tools/route.ts +++ b/apps/web/src/app/api/collections/[id]/tools/route.ts @@ -86,6 +86,7 @@ export async function GET( id: true, name: true, description: true, + parameters: true, package: { select: { npmPackageName: true, diff --git a/apps/web/src/components/agents/ChatToolsPanel.tsx b/apps/web/src/components/agents/ChatToolsPanel.tsx index f7da602..2d9c76d 100644 --- a/apps/web/src/components/agents/ChatToolsPanel.tsx +++ b/apps/web/src/components/agents/ChatToolsPanel.tsx @@ -4,6 +4,14 @@ import { Button } from '@tpmjs/ui/Button/Button'; import { Icon } from '@tpmjs/ui/Icon/Icon'; import { useCallback, useState } from 'react'; +export interface ToolParameter { + name: string; + type: string; + description: string; + required: boolean; + default?: unknown; +} + export interface ToolInfo { id: string; toolId: string; @@ -11,6 +19,7 @@ export interface ToolInfo { id: string; name: string; description: string | null; + parameters: ToolParameter[] | null; package: { npmPackageName: string; category: string; diff --git a/apps/web/src/components/agents/ToolDetailsModal.tsx b/apps/web/src/components/agents/ToolDetailsModal.tsx index f6fc6ba..9895059 100644 --- a/apps/web/src/components/agents/ToolDetailsModal.tsx +++ b/apps/web/src/components/agents/ToolDetailsModal.tsx @@ -18,6 +18,9 @@ export function ToolDetailsModal({ tool, open, onClose }: ToolDetailsModalProps) if (!tool) return null; const toolPageUrl = `/tool/${tool.tool.package.npmPackageName}/${tool.tool.name}`; + const parameters = tool.tool.parameters || []; + const requiredParams = parameters.filter((p) => p.required); + const optionalParams = parameters.filter((p) => !p.required); return ( @@ -58,19 +61,60 @@ export function ToolDetailsModal({ tool, open, onClose }: ToolDetailsModalProps) )}
{tool.tool.package.category} + {parameters.length > 0 && ( + + {parameters.length} param{parameters.length !== 1 ? 's' : ''} + + )}
- {/* note about parameters */} + {/* parameters fieldset */}
parameters -

- This tool accepts parameters defined in its input schema. View the full tool page for - detailed parameter documentation and usage examples. -

+ + {parameters.length === 0 ? ( +

No parameters defined

+ ) : ( +
+ {/* Required parameters */} + {requiredParams.length > 0 && ( +
+
+
+ + Required ({requiredParams.length}) + +
+
+ {requiredParams.map((param) => ( + + ))} +
+
+ )} + + {/* Optional parameters */} + {optionalParams.length > 0 && ( +
0 ? 'pt-4 border-t border-dashed border-border/50' : ''}> +
+
+ + Optional ({optionalParams.length}) + +
+
+ {optionalParams.map((param) => ( + + ))} +
+
+ )} +
+ )}
@@ -94,3 +138,37 @@ export function ToolDetailsModal({ tool, open, onClose }: ToolDetailsModalProps)
); } + +interface ParameterRowProps { + param: { + name: string; + type: string; + description: string; + required: boolean; + default?: unknown; + }; +} + +function ParameterRow({ param }: ParameterRowProps) { + return ( +
+
+ {param.name} + + {param.type} + +
+ {param.description && ( +

{param.description}

+ )} + {param.default !== undefined && ( +
+ default: + + {JSON.stringify(param.default)} + +
+ )} +
+ ); +} diff --git a/packages/tools/official/blocks.yml b/packages/tools/official/blocks.yml index 1dde4ed..76a32f4 100644 --- a/packages/tools/official/blocks.yml +++ b/packages/tools/official/blocks.yml @@ -5263,7 +5263,1044 @@ blocks: measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance, readme_documentation] # --------------------------------------------------------------------------- - # Q) exe.dev VM Management (15 tools) + # N) Languages (2 tools) + # --------------------------------------------------------------------------- + unsandbox.getLanguages: + type: utility + description: "Get list of all supported programming languages with metadata including extensions, version, and capabilities." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API GET /languages endpoint" + inputs: [] + outputs: + - name: languages + type: Language[] + description: "Array of supported languages with id, name, version, extensions, and capabilities" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + unsandbox.getShells: + type: utility + description: "Get list of available shell interpreters (bash, zsh, etc.) with their versions and capabilities." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API GET /shells endpoint" + inputs: [] + outputs: + - name: shells + type: Shell[] + description: "Array of available shells with id, name, version, and path" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + # --------------------------------------------------------------------------- + # O) Sessions (11 tools) + # --------------------------------------------------------------------------- + unsandbox.createSession: + type: utility + description: "Create a new persistent session with configurable language, resources, and networking. Sessions maintain state across executions." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API POST /sessions endpoint" + inputs: + - name: language + type: string + description: "Programming language for the session" + - name: network_mode + type: "'zerotrust' | 'semitrusted'" + optional: true + description: "Network isolation mode. Default: 'zerotrust'" + - name: vcpu + type: number + optional: true + description: "Number of virtual CPUs (0.25-8). Default: 1" + - name: memory + type: number + optional: true + description: "Memory in MB (256-8192). Default: 512" + - name: ttl + type: number + optional: true + description: "Session timeout in seconds. Default: 3600" + - name: env + type: "Record" + optional: true + description: "Environment variables for the session" + outputs: + - name: session_id + type: string + description: "Unique session identifier" + - name: status + type: string + description: "Session status (creating, active, frozen, etc.)" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + unsandbox.getSession: + type: utility + description: "Get details of a session including status, resource usage, and configuration." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API GET /sessions/{id} endpoint" + inputs: + - name: session_id + type: string + description: "The session ID to retrieve" + outputs: + - name: session + type: Session + description: "Session details including id, status, language, resources, and created_at" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + unsandbox.listSessions: + type: utility + description: "List all sessions with optional filtering by status (active, frozen, locked)." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API GET /sessions endpoint" + inputs: + - name: status + type: string + optional: true + description: "Filter by status: active, frozen, locked" + outputs: + - name: sessions + type: Session[] + description: "Array of sessions matching the filter" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + unsandbox.executeInSession: + type: utility + description: "Execute code in an existing session. State and files persist between executions." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API POST /sessions/{id}/execute endpoint" + inputs: + - name: session_id + type: string + description: "The session ID to execute in" + - name: code + type: string + description: "Code to execute" + - name: timeout + type: number + optional: true + description: "Execution timeout in seconds" + outputs: + - name: stdout + type: string + description: "Standard output from execution" + - name: stderr + type: string + description: "Standard error from execution" + - name: exit_code + type: number + description: "Process exit code" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + unsandbox.freezeSession: + type: utility + description: "Freeze a session to pause execution and reduce resource usage while preserving state." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API POST /sessions/{id}/freeze endpoint" + inputs: + - name: session_id + type: string + description: "The session ID to freeze" + outputs: + - name: success + type: boolean + description: "Whether the freeze operation succeeded" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + unsandbox.unfreezeSession: + type: utility + description: "Unfreeze a frozen session to resume execution." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API POST /sessions/{id}/unfreeze endpoint" + inputs: + - name: session_id + type: string + description: "The session ID to unfreeze" + outputs: + - name: success + type: boolean + description: "Whether the unfreeze operation succeeded" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + unsandbox.lockSession: + type: utility + description: "Lock a session to prevent modifications or deletion." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API POST /sessions/{id}/lock endpoint" + inputs: + - name: session_id + type: string + description: "The session ID to lock" + outputs: + - name: success + type: boolean + description: "Whether the lock operation succeeded" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + unsandbox.unlockSession: + type: utility + description: "Unlock a locked session to allow modifications." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API POST /sessions/{id}/unlock endpoint" + inputs: + - name: session_id + type: string + description: "The session ID to unlock" + outputs: + - name: success + type: boolean + description: "Whether the unlock operation succeeded" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + unsandbox.createSessionSnapshot: + type: utility + description: "Create a snapshot of the current session state for backup or cloning." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API POST /sessions/{id}/snapshot endpoint" + inputs: + - name: session_id + type: string + description: "The session ID to snapshot" + - name: name + type: string + optional: true + description: "Name for the snapshot" + outputs: + - name: snapshot_id + type: string + description: "ID of the created snapshot" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + unsandbox.restoreSession: + type: utility + description: "Restore a session from a snapshot." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API POST /sessions/{id}/restore endpoint" + inputs: + - name: session_id + type: string + description: "The session ID to restore" + - name: snapshot_id + type: string + description: "The snapshot ID to restore from" + outputs: + - name: success + type: boolean + description: "Whether the restore operation succeeded" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + unsandbox.deleteSession: + type: utility + description: "Delete a session and release all associated resources." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API DELETE /sessions/{id} endpoint" + inputs: + - name: session_id + type: string + description: "The session ID to delete" + outputs: + - name: deleted + type: boolean + description: "Whether the deletion succeeded" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + # --------------------------------------------------------------------------- + # P) Services (15 tools) + # --------------------------------------------------------------------------- + unsandbox.createService: + type: utility + description: "Create a long-running service with persistent state, networking, and auto-restart capabilities." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API POST /services endpoint" + inputs: + - name: language + type: string + description: "Programming language for the service" + - name: code + type: string + description: "Service code to run" + - name: network_mode + type: "'zerotrust' | 'semitrusted'" + optional: true + description: "Network isolation mode" + - name: port + type: number + optional: true + description: "Port to expose" + - name: env + type: "Record" + optional: true + description: "Environment variables" + outputs: + - name: service_id + type: string + description: "Unique service identifier" + - name: url + type: string + description: "Public URL for the service" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + unsandbox.getService: + type: utility + description: "Get details of a service including status, endpoints, and resource usage." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API GET /services/{id} endpoint" + inputs: + - name: service_id + type: string + description: "The service ID to retrieve" + outputs: + - name: service + type: Service + description: "Service details including id, status, url, and resources" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + unsandbox.listServices: + type: utility + description: "List all services with optional filtering by status." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API GET /services endpoint" + inputs: + - name: status + type: string + optional: true + description: "Filter by status" + outputs: + - name: services + type: Service[] + description: "Array of services" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + unsandbox.executeInService: + type: utility + description: "Execute a command or code snippet in a running service." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API POST /services/{id}/execute endpoint" + inputs: + - name: service_id + type: string + description: "The service ID to execute in" + - name: code + type: string + description: "Code to execute" + outputs: + - name: stdout + type: string + description: "Standard output" + - name: stderr + type: string + description: "Standard error" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + unsandbox.freezeService: + type: utility + description: "Freeze a service to pause execution while preserving state." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API POST /services/{id}/freeze endpoint" + inputs: + - name: service_id + type: string + description: "The service ID to freeze" + outputs: + - name: success + type: boolean + description: "Whether the freeze operation succeeded" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + unsandbox.unfreezeService: + type: utility + description: "Unfreeze a frozen service to resume execution." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API POST /services/{id}/unfreeze endpoint" + inputs: + - name: service_id + type: string + description: "The service ID to unfreeze" + outputs: + - name: success + type: boolean + description: "Whether the unfreeze operation succeeded" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + unsandbox.lockService: + type: utility + description: "Lock a service to prevent modifications or deletion." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API POST /services/{id}/lock endpoint" + inputs: + - name: service_id + type: string + description: "The service ID to lock" + outputs: + - name: success + type: boolean + description: "Whether the lock operation succeeded" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + unsandbox.unlockService: + type: utility + description: "Unlock a locked service to allow modifications." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API POST /services/{id}/unlock endpoint" + inputs: + - name: service_id + type: string + description: "The service ID to unlock" + outputs: + - name: success + type: boolean + description: "Whether the unlock operation succeeded" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + unsandbox.redeployService: + type: utility + description: "Redeploy a service with updated configuration or code." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API POST /services/{id}/redeploy endpoint" + inputs: + - name: service_id + type: string + description: "The service ID to redeploy" + - name: code + type: string + optional: true + description: "New code to deploy" + outputs: + - name: success + type: boolean + description: "Whether the redeploy operation succeeded" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + unsandbox.getServiceLogs: + type: utility + description: "Get logs from a service with optional filtering by time range and log level." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API GET /services/{id}/logs endpoint" + inputs: + - name: service_id + type: string + description: "The service ID to get logs from" + - name: since + type: string + optional: true + description: "ISO timestamp to start from" + - name: until + type: string + optional: true + description: "ISO timestamp to end at" + - name: limit + type: number + optional: true + description: "Maximum number of log entries" + outputs: + - name: logs + type: LogEntry[] + description: "Array of log entries" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + unsandbox.createServiceSnapshot: + type: utility + description: "Create a snapshot of the current service state." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API POST /services/{id}/snapshot endpoint" + inputs: + - name: service_id + type: string + description: "The service ID to snapshot" + - name: name + type: string + optional: true + description: "Name for the snapshot" + outputs: + - name: snapshot_id + type: string + description: "ID of the created snapshot" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + unsandbox.getServiceEnv: + type: utility + description: "Get environment variables configured for a service." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API GET /services/{id}/env endpoint" + inputs: + - name: service_id + type: string + description: "The service ID" + outputs: + - name: env + type: "Record" + description: "Environment variables" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + unsandbox.setServiceEnv: + type: utility + description: "Set or update environment variables for a service." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API PUT /services/{id}/env endpoint" + inputs: + - name: service_id + type: string + description: "The service ID" + - name: env + type: "Record" + description: "Environment variables to set" + outputs: + - name: success + type: boolean + description: "Whether the operation succeeded" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + unsandbox.deleteServiceEnv: + type: utility + description: "Delete an environment variable from a service." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API DELETE /services/{id}/env/{key} endpoint" + inputs: + - name: service_id + type: string + description: "The service ID" + - name: key + type: string + description: "Environment variable key to delete" + outputs: + - name: success + type: boolean + description: "Whether the deletion succeeded" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + unsandbox.deleteService: + type: utility + description: "Delete a service and release all associated resources." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API DELETE /services/{id} endpoint" + inputs: + - name: service_id + type: string + description: "The service ID to delete" + outputs: + - name: deleted + type: boolean + description: "Whether the deletion succeeded" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + # --------------------------------------------------------------------------- + # Q) Snapshots (8 tools) + # --------------------------------------------------------------------------- + unsandbox.createSnapshot: + type: utility + description: "Create a snapshot from any source (session, service, or existing snapshot)." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API POST /snapshots endpoint" + inputs: + - name: source_type + type: "'session' | 'service' | 'snapshot'" + description: "Type of source to snapshot" + - name: source_id + type: string + description: "ID of the source" + - name: name + type: string + optional: true + description: "Name for the snapshot" + outputs: + - name: snapshot_id + type: string + description: "ID of the created snapshot" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + unsandbox.getSnapshot: + type: utility + description: "Get details of a snapshot including metadata and creation info." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API GET /snapshots/{id} endpoint" + inputs: + - name: snapshot_id + type: string + description: "The snapshot ID to retrieve" + outputs: + - name: snapshot + type: Snapshot + description: "Snapshot details" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + unsandbox.listSnapshots: + type: utility + description: "List all snapshots with optional filtering by source type." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API GET /snapshots endpoint" + inputs: + - name: source_type + type: string + optional: true + description: "Filter by source type" + outputs: + - name: snapshots + type: Snapshot[] + description: "Array of snapshots" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + unsandbox.lockSnapshot: + type: utility + description: "Lock a snapshot to prevent deletion or modification." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API POST /snapshots/{id}/lock endpoint" + inputs: + - name: snapshot_id + type: string + description: "The snapshot ID to lock" + outputs: + - name: success + type: boolean + description: "Whether the lock operation succeeded" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + unsandbox.unlockSnapshot: + type: utility + description: "Unlock a locked snapshot to allow modifications." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API POST /snapshots/{id}/unlock endpoint" + inputs: + - name: snapshot_id + type: string + description: "The snapshot ID to unlock" + outputs: + - name: success + type: boolean + description: "Whether the unlock operation succeeded" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + unsandbox.restoreSnapshot: + type: utility + description: "Restore a session or service from a snapshot." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API POST /snapshots/{id}/restore endpoint" + inputs: + - name: snapshot_id + type: string + description: "The snapshot ID to restore" + - name: target_type + type: "'session' | 'service'" + description: "Type of resource to create" + outputs: + - name: id + type: string + description: "ID of the restored resource" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + unsandbox.cloneSnapshot: + type: utility + description: "Clone a snapshot to create a new independent copy." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API POST /snapshots/{id}/clone endpoint" + inputs: + - name: snapshot_id + type: string + description: "The snapshot ID to clone" + - name: name + type: string + optional: true + description: "Name for the cloned snapshot" + outputs: + - name: snapshot_id + type: string + description: "ID of the cloned snapshot" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + unsandbox.deleteSnapshot: + type: utility + description: "Delete a snapshot and free associated storage." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API DELETE /snapshots/{id} endpoint" + inputs: + - name: snapshot_id + type: string + description: "The snapshot ID to delete" + outputs: + - name: deleted + type: boolean + description: "Whether the deletion succeeded" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + # --------------------------------------------------------------------------- + # R) Images (12 tools) + # --------------------------------------------------------------------------- + unsandbox.publishImage: + type: utility + description: "Publish a snapshot as a reusable image for spawning new sessions or services." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API POST /images endpoint" + inputs: + - name: snapshot_id + type: string + description: "The snapshot ID to publish" + - name: name + type: string + description: "Name for the image" + - name: visibility + type: "'public' | 'private'" + optional: true + description: "Image visibility. Default: 'private'" + outputs: + - name: image_id + type: string + description: "ID of the published image" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + unsandbox.getImage: + type: utility + description: "Get details of a published image including metadata and access info." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API GET /images/{id} endpoint" + inputs: + - name: image_id + type: string + description: "The image ID to retrieve" + outputs: + - name: image + type: Image + description: "Image details" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + unsandbox.listImages: + type: utility + description: "List all images with optional filtering by visibility and ownership." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API GET /images endpoint" + inputs: + - name: visibility + type: string + optional: true + description: "Filter by visibility: public, private" + - name: owned + type: boolean + optional: true + description: "Filter to only owned images" + outputs: + - name: images + type: Image[] + description: "Array of images" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + unsandbox.lockImage: + type: utility + description: "Lock an image to prevent modifications or deletion." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API POST /images/{id}/lock endpoint" + inputs: + - name: image_id + type: string + description: "The image ID to lock" + outputs: + - name: success + type: boolean + description: "Whether the lock operation succeeded" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + unsandbox.unlockImage: + type: utility + description: "Unlock a locked image to allow modifications." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API POST /images/{id}/unlock endpoint" + inputs: + - name: image_id + type: string + description: "The image ID to unlock" + outputs: + - name: success + type: boolean + description: "Whether the unlock operation succeeded" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + unsandbox.grantImageAccess: + type: utility + description: "Grant access to a private image for specific users or API keys." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API POST /images/{id}/access endpoint" + inputs: + - name: image_id + type: string + description: "The image ID" + - name: public_key + type: string + description: "Public key to grant access to" + outputs: + - name: success + type: boolean + description: "Whether the grant operation succeeded" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + unsandbox.revokeImageAccess: + type: utility + description: "Revoke access to an image from specific users or API keys." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API DELETE /images/{id}/access/{key} endpoint" + inputs: + - name: image_id + type: string + description: "The image ID" + - name: public_key + type: string + description: "Public key to revoke access from" + outputs: + - name: success + type: boolean + description: "Whether the revoke operation succeeded" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + unsandbox.transferImage: + type: utility + description: "Transfer ownership of an image to another user." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API POST /images/{id}/transfer endpoint" + inputs: + - name: image_id + type: string + description: "The image ID to transfer" + - name: new_owner_key + type: string + description: "Public key of the new owner" + outputs: + - name: success + type: boolean + description: "Whether the transfer succeeded" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + unsandbox.setImageVisibility: + type: utility + description: "Set image visibility to public or private." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API PUT /images/{id}/visibility endpoint" + inputs: + - name: image_id + type: string + description: "The image ID" + - name: visibility + type: "'public' | 'private'" + description: "New visibility setting" + outputs: + - name: success + type: boolean + description: "Whether the operation succeeded" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + unsandbox.spawnFromImage: + type: utility + description: "Spawn a new session or service from an image." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API POST /images/{id}/spawn endpoint" + inputs: + - name: image_id + type: string + description: "The image ID to spawn from" + - name: type + type: "'session' | 'service'" + description: "Type of resource to create" + - name: network_mode + type: "'zerotrust' | 'semitrusted'" + optional: true + description: "Network isolation mode" + outputs: + - name: id + type: string + description: "ID of the spawned resource" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + unsandbox.getImageTrustedKeys: + type: utility + description: "Get list of API keys that have access to a private image." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API GET /images/{id}/trusted endpoint" + inputs: + - name: image_id + type: string + description: "The image ID" + outputs: + - name: keys + type: string[] + description: "Array of public keys with access" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + unsandbox.deleteImage: + type: utility + description: "Delete an image and free associated storage." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API DELETE /images/{id} endpoint" + inputs: + - name: image_id + type: string + description: "The image ID to delete" + outputs: + - name: deleted + type: boolean + description: "Whether the deletion succeeded" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + # --------------------------------------------------------------------------- + # S) System (4 tools) + # --------------------------------------------------------------------------- + unsandbox.healthCheck: + type: utility + description: "Check the health status of the Unsandbox API service." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API GET /health endpoint" + inputs: [] + outputs: + - name: status + type: string + description: "Health status (healthy, degraded, unhealthy)" + - name: version + type: string + description: "API version" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + unsandbox.getClusterStatus: + type: utility + description: "Get status of the execution cluster including node availability and capacity." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API GET /cluster endpoint" + inputs: [] + outputs: + - name: nodes + type: number + description: "Number of active nodes" + - name: capacity + type: object + description: "Available capacity information" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + unsandbox.getSystemStats: + type: utility + description: "Get system statistics including resource utilization and job metrics." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API GET /stats endpoint" + inputs: [] + outputs: + - name: jobs_total + type: number + description: "Total jobs processed" + - name: jobs_active + type: number + description: "Currently active jobs" + - name: resource_usage + type: object + description: "Resource utilization metrics" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + unsandbox.listPools: + type: utility + description: "List available execution pools with their configurations and current status." + path: "unsandbox" + domain_rules: + - id: api_integration + description: "Must call Unsandbox API GET /pools endpoint" + inputs: [] + outputs: + - name: pools + type: Pool[] + description: "Array of execution pools" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + # --------------------------------------------------------------------------- + # T) exe.dev VM Management (15 tools) # --------------------------------------------------------------------------- exe.list: type: utility diff --git a/packages/tools/official/unsandbox/block.ts b/packages/tools/official/unsandbox/block.ts index 83fa8c3..d2ba33c 100644 --- a/packages/tools/official/unsandbox/block.ts +++ b/packages/tools/official/unsandbox/block.ts @@ -3,26 +3,146 @@ * This file provides metadata for the blocks validator */ import { - deleteJob, - execute, + // Execution tools executeCodeAsync, - getJob, - listJobs, + execute, run, runAsync, + // Job tools + getJob, + listJobs, + deleteJob, + // Languages tools + getLanguages, + getShells, + // Session tools + createSession, + getSession, + listSessions, + executeInSession, + freezeSession, + unfreezeSession, + lockSession, + unlockSession, + createSessionSnapshot, + restoreSession, + deleteSession, + // Service tools + createService, + getService, + listServices, + executeInService, + freezeService, + unfreezeService, + lockService, + unlockService, + redeployService, + getServiceLogs, + createServiceSnapshot, + getServiceEnv, + setServiceEnv, + deleteServiceEnv, + deleteService, + // Snapshot tools + createSnapshot, + getSnapshot, + listSnapshots, + lockSnapshot, + unlockSnapshot, + restoreSnapshot, + cloneSnapshot, + deleteSnapshot, + // Image tools + publishImage, + getImage, + listImages, + lockImage, + unlockImage, + grantImageAccess, + revokeImageAccess, + transferImage, + setImageVisibility, + spawnFromImage, + getImageTrustedKeys, + deleteImage, + // System tools + healthCheck, + getClusterStatus, + getSystemStats, + listPools, } from './src/index.js'; export const block = { name: 'unsandbox', description: 'Execute code in a secure sandbox environment supporting 42+ languages', tools: { + // Execution tools executeCodeAsync, - getJob, execute, run, runAsync, + // Job tools + getJob, listJobs, deleteJob, + // Languages tools + getLanguages, + getShells, + // Session tools + createSession, + getSession, + listSessions, + executeInSession, + freezeSession, + unfreezeSession, + lockSession, + unlockSession, + createSessionSnapshot, + restoreSession, + deleteSession, + // Service tools + createService, + getService, + listServices, + executeInService, + freezeService, + unfreezeService, + lockService, + unlockService, + redeployService, + getServiceLogs, + createServiceSnapshot, + getServiceEnv, + setServiceEnv, + deleteServiceEnv, + deleteService, + // Snapshot tools + createSnapshot, + getSnapshot, + listSnapshots, + lockSnapshot, + unlockSnapshot, + restoreSnapshot, + cloneSnapshot, + deleteSnapshot, + // Image tools + publishImage, + getImage, + listImages, + lockImage, + unlockImage, + grantImageAccess, + revokeImageAccess, + transferImage, + setImageVisibility, + spawnFromImage, + getImageTrustedKeys, + deleteImage, + // System tools + healthCheck, + getClusterStatus, + getSystemStats, + listPools, }, }; diff --git a/packages/tools/official/unsandbox/package.json b/packages/tools/official/unsandbox/package.json index 7d9197f..ba9b58c 100644 --- a/packages/tools/official/unsandbox/package.json +++ b/packages/tools/official/unsandbox/package.json @@ -1,6 +1,6 @@ { "name": "@tpmjs/tools-unsandbox", - "version": "0.1.1", + "version": "0.1.2", "description": "Execute code in a secure sandbox environment. Supports 42+ programming languages with async execution, input files, and compiled artifacts.", "type": "module", "keywords": [ @@ -50,10 +50,6 @@ "name": "executeCodeAsync", "description": "Execute code asynchronously in a secure sandbox. Returns a job_id immediately. Use getJob to check status and retrieve results. Supports 42+ languages." }, - { - "name": "getJob", - "description": "Get the status and results of an async code execution job by job_id." - }, { "name": "execute", "description": "Execute code synchronously in a secure sandbox. Waits for completion and returns results directly. Best for quick scripts." @@ -66,6 +62,10 @@ "name": "runAsync", "description": "Execute code asynchronously with automatic language detection via shebang. Returns job_id for polling." }, + { + "name": "getJob", + "description": "Get the status and results of an async code execution job by job_id." + }, { "name": "listJobs", "description": "List all active code execution jobs with their current status." @@ -73,6 +73,214 @@ { "name": "deleteJob", "description": "Cancel an active code execution job by job_id." + }, + { + "name": "getLanguages", + "description": "Get list of all supported programming languages with their metadata including extensions, version, and capabilities." + }, + { + "name": "getShells", + "description": "Get list of available shell interpreters (bash, zsh, etc.) with their versions and capabilities." + }, + { + "name": "createSession", + "description": "Create a new persistent session with configurable language, resources, and networking. Sessions maintain state across executions." + }, + { + "name": "getSession", + "description": "Get details of a session including status, resource usage, and configuration." + }, + { + "name": "listSessions", + "description": "List all sessions with optional filtering by status (active, frozen, locked)." + }, + { + "name": "executeInSession", + "description": "Execute code in an existing session. State and files persist between executions." + }, + { + "name": "freezeSession", + "description": "Freeze a session to pause execution and reduce resource usage while preserving state." + }, + { + "name": "unfreezeSession", + "description": "Unfreeze a frozen session to resume execution." + }, + { + "name": "lockSession", + "description": "Lock a session to prevent modifications or deletion." + }, + { + "name": "unlockSession", + "description": "Unlock a locked session to allow modifications." + }, + { + "name": "createSessionSnapshot", + "description": "Create a snapshot of the current session state for backup or cloning." + }, + { + "name": "restoreSession", + "description": "Restore a session from a snapshot." + }, + { + "name": "deleteSession", + "description": "Delete a session and release all associated resources." + }, + { + "name": "createService", + "description": "Create a long-running service with persistent state, networking, and auto-restart capabilities." + }, + { + "name": "getService", + "description": "Get details of a service including status, endpoints, and resource usage." + }, + { + "name": "listServices", + "description": "List all services with optional filtering by status." + }, + { + "name": "executeInService", + "description": "Execute a command or code snippet in a running service." + }, + { + "name": "freezeService", + "description": "Freeze a service to pause execution while preserving state." + }, + { + "name": "unfreezeService", + "description": "Unfreeze a frozen service to resume execution." + }, + { + "name": "lockService", + "description": "Lock a service to prevent modifications or deletion." + }, + { + "name": "unlockService", + "description": "Unlock a locked service to allow modifications." + }, + { + "name": "redeployService", + "description": "Redeploy a service with updated configuration or code." + }, + { + "name": "getServiceLogs", + "description": "Get logs from a service with optional filtering by time range and log level." + }, + { + "name": "createServiceSnapshot", + "description": "Create a snapshot of the current service state." + }, + { + "name": "getServiceEnv", + "description": "Get environment variables configured for a service." + }, + { + "name": "setServiceEnv", + "description": "Set or update environment variables for a service." + }, + { + "name": "deleteServiceEnv", + "description": "Delete an environment variable from a service." + }, + { + "name": "deleteService", + "description": "Delete a service and release all associated resources." + }, + { + "name": "createSnapshot", + "description": "Create a snapshot from any source (session, service, or existing snapshot)." + }, + { + "name": "getSnapshot", + "description": "Get details of a snapshot including metadata and creation info." + }, + { + "name": "listSnapshots", + "description": "List all snapshots with optional filtering by source type." + }, + { + "name": "lockSnapshot", + "description": "Lock a snapshot to prevent deletion or modification." + }, + { + "name": "unlockSnapshot", + "description": "Unlock a locked snapshot to allow modifications." + }, + { + "name": "restoreSnapshot", + "description": "Restore a session or service from a snapshot." + }, + { + "name": "cloneSnapshot", + "description": "Clone a snapshot to create a new independent copy." + }, + { + "name": "deleteSnapshot", + "description": "Delete a snapshot and free associated storage." + }, + { + "name": "publishImage", + "description": "Publish a snapshot as a reusable image for spawning new sessions or services." + }, + { + "name": "getImage", + "description": "Get details of a published image including metadata and access info." + }, + { + "name": "listImages", + "description": "List all images with optional filtering by visibility and ownership." + }, + { + "name": "lockImage", + "description": "Lock an image to prevent modifications or deletion." + }, + { + "name": "unlockImage", + "description": "Unlock a locked image to allow modifications." + }, + { + "name": "grantImageAccess", + "description": "Grant access to a private image for specific users or API keys." + }, + { + "name": "revokeImageAccess", + "description": "Revoke access to an image from specific users or API keys." + }, + { + "name": "transferImage", + "description": "Transfer ownership of an image to another user." + }, + { + "name": "setImageVisibility", + "description": "Set image visibility to public or private." + }, + { + "name": "spawnFromImage", + "description": "Spawn a new session or service from an image." + }, + { + "name": "getImageTrustedKeys", + "description": "Get list of API keys that have access to a private image." + }, + { + "name": "deleteImage", + "description": "Delete an image and free associated storage." + }, + { + "name": "healthCheck", + "description": "Check the health status of the Unsandbox API service." + }, + { + "name": "getClusterStatus", + "description": "Get status of the execution cluster including node availability and capacity." + }, + { + "name": "getSystemStats", + "description": "Get system statistics including resource utilization and job metrics." + }, + { + "name": "listPools", + "description": "List available execution pools with their configurations and current status." } ] }, diff --git a/packages/tools/official/unsandbox/src/index.ts b/packages/tools/official/unsandbox/src/index.ts index ebedc22..d880761 100644 --- a/packages/tools/official/unsandbox/src/index.ts +++ b/packages/tools/official/unsandbox/src/index.ts @@ -2,9 +2,11 @@ * Unsandbox Code Execution Tools for TPMJS * Execute code in a secure sandbox environment supporting 42+ languages. * - * @requires UNSANDBOX_API_KEY environment variable + * @requires UNSANDBOX_PUBLIC_KEY environment variable (unsb-pk-xxxx-xxxx-xxxx-xxxx) + * @requires UNSANDBOX_SECRET_KEY environment variable (unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx) */ +import { createHmac } from 'node:crypto'; import { jsonSchema, tool } from 'ai'; const UNSANDBOX_API_BASE = 'https://api.unsandbox.com'; @@ -61,50 +63,166 @@ export type SupportedLanguage = (typeof SUPPORTED_LANGUAGES)[number]; export type NetworkMode = 'zerotrust' | 'semitrusted'; +export type JobStatus = 'pending' | 'running' | 'completed' | 'cancelled' | 'timeout' | 'failed'; + export interface InputFile { filename: string; content: string; // Base64 encoded } +export interface Artifact { + type: 'base64'; + filename: string; + data: string; +} + +/** + * Get API keys from environment variables + */ +function getApiKeys(): { publicKey: string; secretKey: string } { + const publicKey = process.env.UNSANDBOX_PUBLIC_KEY; + const secretKey = process.env.UNSANDBOX_SECRET_KEY; + + if (!publicKey) { + throw new Error( + 'UNSANDBOX_PUBLIC_KEY environment variable is required. Get your API keys from https://unsandbox.com/pricing-for-agents' + ); + } + + if (!secretKey) { + throw new Error( + 'UNSANDBOX_SECRET_KEY environment variable is required. Get your API keys from https://unsandbox.com/pricing-for-agents' + ); + } + + return { publicKey, secretKey }; +} + +/** + * Compute HMAC-SHA256 signature for API authentication + * Message format: "{timestamp}:{METHOD}:{path}:{body}" + */ +function computeSignature( + secretKey: string, + timestamp: number, + method: string, + path: string, + body: string +): string { + const message = `${timestamp}:${method}:${path}:${body}`; + return createHmac('sha256', secretKey).update(message).digest('hex').toLowerCase(); +} + +/** + * Make an authenticated request to the Unsandbox API + */ +async function apiRequest( + method: 'GET' | 'POST' | 'PUT' | 'DELETE', + path: string, + body?: unknown +): Promise { + const { publicKey, secretKey } = getApiKeys(); + const timestamp = Math.floor(Date.now() / 1000); + const bodyString = body !== undefined ? JSON.stringify(body) : ''; + const signature = computeSignature(secretKey, timestamp, method, path, bodyString); + + const headers: Record = { + Authorization: `Bearer ${publicKey}`, + 'X-Timestamp': timestamp.toString(), + 'X-Signature': signature, + }; + + if (body !== undefined) { + headers['Content-Type'] = 'application/json'; + } + + const response = await fetch(`${UNSANDBOX_API_BASE}${path}`, { + method, + headers, + body: body !== undefined ? bodyString : undefined, + }); + + if (!response.ok) { + const errorText = await response.text().catch(() => 'Unknown error'); + handleApiError(response.status, errorText); + } + + return response.json() as Promise; +} + +/** + * Handle API errors with specific messages for common HTTP status codes + */ +function handleApiError(status: number, errorText: string): never { + switch (status) { + case 400: + throw new Error(`Bad request: ${errorText}`); + case 401: + throw new Error( + `Authentication failed: Invalid API keys. Ensure UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY are correct.` + ); + case 403: + throw new Error(`Access forbidden: ${errorText}`); + case 404: + throw new Error(`Resource not found: ${errorText}`); + case 429: + throw new Error(`Rate limit exceeded or concurrency limit reached: ${errorText}`); + case 500: + case 502: + case 503: + throw new Error(`Unsandbox service error (${status}): ${errorText}`); + default: + throw new Error(`Unsandbox API error: HTTP ${status} - ${errorText}`); + } +} + +/** + * Make an authenticated request with text/plain body (for /run endpoints) + */ +async function apiRequestText(path: string, body: string): Promise { + const { publicKey, secretKey } = getApiKeys(); + const timestamp = Math.floor(Date.now() / 1000); + const signature = computeSignature(secretKey, timestamp, 'POST', path, body); + + const response = await fetch(`${UNSANDBOX_API_BASE}${path}`, { + method: 'POST', + headers: { + 'Content-Type': 'text/plain', + Authorization: `Bearer ${publicKey}`, + 'X-Timestamp': timestamp.toString(), + 'X-Signature': signature, + }, + body, + }); + + if (!response.ok) { + const errorText = await response.text().catch(() => 'Unknown error'); + handleApiError(response.status, errorText); + } + + return response.json() as Promise; +} + +// ============================================================================ +// Execute Async +// ============================================================================ + export interface ExecuteAsyncInput { language: string; code: string; input_files?: InputFile[]; + env?: Record; network_mode?: NetworkMode; ttl?: number; + vcpu?: number; return_artifact?: boolean; return_wasm_artifact?: boolean; } export interface ExecuteAsyncResult { job_id: string; - status: string; -} - -export interface GetJobInput { - job_id: string; -} - -export interface GetJobResult { - job_id: string; - status: 'queued' | 'running' | 'completed' | 'failed'; - stdout?: string; - stderr?: string; - exit_code?: number; - duration_ms?: number; - error?: string; - artifact?: string; - wasm_artifact?: string; -} - -function getApiKey(): string { - const key = process.env.UNSANDBOX_API_KEY; - if (!key) { - throw new Error( - 'UNSANDBOX_API_KEY environment variable is required. Get your API key from https://unsandbox.com' - ); - } - return key; + status: 'pending'; + message: string; } /** @@ -145,6 +263,11 @@ export const executeCodeAsync = tool({ required: ['filename', 'content'], }, }, + env: { + type: 'object', + description: 'Environment variables as key-value pairs.', + additionalProperties: { type: 'string' }, + }, network_mode: { type: 'string', enum: ['zerotrust', 'semitrusted'], @@ -155,6 +278,10 @@ export const executeCodeAsync = tool({ type: 'number', description: 'Execution timeout in seconds (1-900). Default: 60.', }, + vcpu: { + type: 'number', + description: 'Number of vCPUs (1-8). Each vCPU includes 2GB RAM. Default: 1.', + }, return_artifact: { type: 'boolean', description: 'For compiled languages, return the compiled binary.', @@ -168,37 +295,51 @@ export const executeCodeAsync = tool({ additionalProperties: false, }), async execute(input: ExecuteAsyncInput): Promise { - const apiKey = getApiKey(); - - // Validate language if (!input.language || typeof input.language !== 'string') { throw new Error('Language is required and must be a string'); } + const normalizedLanguage = input.language.toLowerCase(); + if (!SUPPORTED_LANGUAGES.includes(normalizedLanguage as SupportedLanguage)) { + throw new Error( + `Unsupported language: "${input.language}". Supported languages: ${SUPPORTED_LANGUAGES.join(', ')}` + ); + } + if (!input.code || typeof input.code !== 'string') { throw new Error('Code is required and must be a string'); } - // Validate TTL if provided - if (input.ttl !== undefined && (input.ttl < 1 || input.ttl > 900)) { + // Default TTL to 60 seconds if not provided (API default) + const ttl = input.ttl ?? 60; + if (ttl < 1 || ttl > 900) { throw new Error('TTL must be between 1 and 900 seconds'); } + if (input.vcpu !== undefined && (input.vcpu < 1 || input.vcpu > 8)) { + throw new Error('vCPU must be between 1 and 8'); + } + + // Default network_mode to 'zerotrust' (most secure) + const networkMode = input.network_mode ?? 'zerotrust'; + const requestBody: Record = { - language: input.language.toLowerCase(), + language: normalizedLanguage, code: input.code, + network_mode: networkMode, + ttl, }; if (input.input_files) { requestBody.input_files = input.input_files; } - if (input.network_mode) { - requestBody.network_mode = input.network_mode; + if (input.env) { + requestBody.env = input.env; } - if (input.ttl) { - requestBody.ttl = input.ttl; + if (input.vcpu) { + requestBody.vcpu = input.vcpu; } if (input.return_artifact) { @@ -209,36 +350,51 @@ export const executeCodeAsync = tool({ requestBody.return_wasm_artifact = input.return_wasm_artifact; } - const response = await fetch(`${UNSANDBOX_API_BASE}/execute/async`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${apiKey}`, - }, - body: JSON.stringify(requestBody), - }); - - if (!response.ok) { - const errorText = await response.text().catch(() => 'Unknown error'); - throw new Error(`Unsandbox API error: HTTP ${response.status} - ${errorText}`); - } - - const result = (await response.json()) as { job_id: string; status?: string }; + // Call the Unsandbox API POST /execute/async endpoint + const result = await apiRequest('POST', '/execute/async', requestBody); return { job_id: result.job_id, - status: result.status || 'queued', + status: 'pending', + message: result.message || 'Job accepted for execution', }; }, }); +// ============================================================================ +// Get Job +// ============================================================================ + +export interface GetJobInput { + job_id: string; +} + +export interface GetJobResult { + job_id: string; + status: JobStatus; + language?: string; + network_mode?: string; + stdout?: string; + stderr?: string; + exit_code?: number; + total_time_ms?: number; + error?: string; + artifacts?: Artifact[]; + artifact?: Artifact; + wasm_artifact?: Artifact; + created_at?: string; + started_at?: string; + completed_at?: string; + success?: boolean; +} + /** * Get the status and results of an async code execution job. * Poll this endpoint until status is 'completed' or 'failed'. */ export const getJob = tool({ description: - "Get the status and results of an async code execution job. Poll this endpoint until status is 'completed' or 'failed'.", + "Get the status and results of an async code execution job. Poll this endpoint until status is 'completed', 'failed', 'cancelled', or 'timeout'.", inputSchema: jsonSchema({ type: 'object', properties: { @@ -251,73 +407,64 @@ export const getJob = tool({ additionalProperties: false, }), async execute(input: GetJobInput): Promise { - const apiKey = getApiKey(); - if (!input.job_id || typeof input.job_id !== 'string') { throw new Error('job_id is required and must be a string'); } - const response = await fetch(`${UNSANDBOX_API_BASE}/jobs/${encodeURIComponent(input.job_id)}`, { - method: 'GET', - headers: { - Authorization: `Bearer ${apiKey}`, - }, - }); - - if (!response.ok) { - if (response.status === 404) { - throw new Error(`Job not found: ${input.job_id}`); - } - const errorText = await response.text().catch(() => 'Unknown error'); - throw new Error(`Unsandbox API error: HTTP ${response.status} - ${errorText}`); - } - - const result = (await response.json()) as GetJobResult; + const result = await apiRequest( + 'GET', + `/jobs/${encodeURIComponent(input.job_id)}`, + undefined + ); return { job_id: result.job_id || input.job_id, status: result.status, + language: result.language, + network_mode: result.network_mode, stdout: result.stdout, stderr: result.stderr, exit_code: result.exit_code, - duration_ms: result.duration_ms, + total_time_ms: result.total_time_ms, error: result.error, + artifacts: result.artifacts, artifact: result.artifact, wasm_artifact: result.wasm_artifact, + created_at: result.created_at, + started_at: result.started_at, + completed_at: result.completed_at, + success: result.success, }; }, }); -/** - * Sync execution interfaces - */ +// ============================================================================ +// Execute Sync +// ============================================================================ + export interface ExecuteSyncInput { language: string; code: string; input_files?: InputFile[]; + env?: Record; network_mode?: NetworkMode; ttl?: number; + vcpu?: number; + return_artifact?: boolean; + return_wasm_artifact?: boolean; } export interface ExecuteSyncResult { + job_id: string; + language: string; + network_mode: string; stdout: string; stderr: string; exit_code: number; - duration_ms: number; -} - -export interface RunInput { - code: string; - network_mode?: NetworkMode; - ttl?: number; -} - -export interface RunResult { - stdout: string; - stderr: string; - exit_code: number; - detected_language: string; - duration_ms: number; + total_time_ms: number; + success: boolean; + artifact?: Artifact; + wasm_artifact?: Artifact; } /** @@ -351,6 +498,11 @@ export const execute = tool({ required: ['filename', 'content'], }, }, + env: { + type: 'object', + description: 'Environment variables as key-value pairs.', + additionalProperties: { type: 'string' }, + }, network_mode: { type: 'string', enum: ['zerotrust', 'semitrusted'], @@ -360,80 +512,131 @@ export const execute = tool({ type: 'number', description: 'Execution timeout in seconds (1-900). Default: 60.', }, + vcpu: { + type: 'number', + description: 'Number of vCPUs (1-8). Each vCPU includes 2GB RAM. Default: 1.', + }, + return_artifact: { + type: 'boolean', + description: 'For compiled languages, return the compiled binary.', + }, + return_wasm_artifact: { + type: 'boolean', + description: 'Compile to WebAssembly. Supported for C, C++, Rust, Zig, Go.', + }, }, required: ['language', 'code'], additionalProperties: false, }), async execute(input: ExecuteSyncInput): Promise { - const apiKey = getApiKey(); - if (!input.language || typeof input.language !== 'string') { throw new Error('Language is required and must be a string'); } + const normalizedLanguage = input.language.toLowerCase(); + if (!SUPPORTED_LANGUAGES.includes(normalizedLanguage as SupportedLanguage)) { + throw new Error( + `Unsupported language: "${input.language}". Supported languages: ${SUPPORTED_LANGUAGES.join(', ')}` + ); + } + if (!input.code || typeof input.code !== 'string') { throw new Error('Code is required and must be a string'); } - if (input.ttl !== undefined && (input.ttl < 1 || input.ttl > 900)) { + // Default TTL to 60 seconds if not provided (API default) + const ttl = input.ttl ?? 60; + if (ttl < 1 || ttl > 900) { throw new Error('TTL must be between 1 and 900 seconds'); } + if (input.vcpu !== undefined && (input.vcpu < 1 || input.vcpu > 8)) { + throw new Error('vCPU must be between 1 and 8'); + } + + // Default network_mode to 'zerotrust' (most secure) + const networkMode = input.network_mode ?? 'zerotrust'; + const requestBody: Record = { - language: input.language.toLowerCase(), + language: normalizedLanguage, code: input.code, + network_mode: networkMode, + ttl, }; if (input.input_files) { requestBody.input_files = input.input_files; } - if (input.network_mode) { - requestBody.network_mode = input.network_mode; + if (input.env) { + requestBody.env = input.env; } - if (input.ttl) { - requestBody.ttl = input.ttl; + if (input.vcpu) { + requestBody.vcpu = input.vcpu; } - const response = await fetch(`${UNSANDBOX_API_BASE}/execute`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${apiKey}`, - }, - body: JSON.stringify(requestBody), - }); - - if (!response.ok) { - const errorText = await response.text().catch(() => 'Unknown error'); - throw new Error(`Unsandbox API error: HTTP ${response.status} - ${errorText}`); + if (input.return_artifact) { + requestBody.return_artifact = input.return_artifact; } - const result = (await response.json()) as ExecuteSyncResult; + if (input.return_wasm_artifact) { + requestBody.return_wasm_artifact = input.return_wasm_artifact; + } + + // Call the Unsandbox API POST /execute endpoint (synchronous) + const result = await apiRequest('POST', '/execute', requestBody); return { + job_id: result.job_id, + language: result.language, + network_mode: result.network_mode || 'zerotrust', stdout: result.stdout || '', stderr: result.stderr || '', exit_code: result.exit_code ?? 0, - duration_ms: result.duration_ms || 0, + total_time_ms: result.total_time_ms || 0, + success: result.success ?? (result.exit_code === 0), + artifact: result.artifact, + wasm_artifact: result.wasm_artifact, }; }, }); +// ============================================================================ +// Run (auto-detect language) +// ============================================================================ + +export interface RunInput { + code: string; + network_mode?: NetworkMode; + ttl?: number; + env?: Record; +} + +export interface RunResult { + job_id: string; + language: string; + detected_language: string; + stdout: string; + stderr: string; + exit_code: number; + success: boolean; +} + /** - * Execute code with automatic language detection from shebang. - * Send raw code with a shebang line (e.g., #!/usr/bin/env python) and the language is auto-detected. + * Execute code with automatic language detection. + * Uses shebang lines, syntax patterns, and heuristics for detection. */ export const run = tool({ description: - 'Execute code with automatic language detection from shebang. Send raw code with a shebang line (e.g., #!/usr/bin/env python) and the language is auto-detected.', + 'Execute code with automatic language detection. Uses shebang lines (e.g., #!/usr/bin/env python), syntax patterns, and heuristics for detection.', inputSchema: jsonSchema({ type: 'object', properties: { code: { type: 'string', - description: 'The source code with shebang line (e.g., #!/usr/bin/env python)', + description: + 'The source code to execute. Can include shebang line (e.g., #!/usr/bin/env python) for explicit language hint.', }, network_mode: { type: 'string', @@ -444,13 +647,16 @@ export const run = tool({ type: 'number', description: 'Execution timeout in seconds (1-900). Default: 60.', }, + env: { + type: 'object', + description: 'Environment variables as key-value pairs (URL-encoded JSON).', + additionalProperties: { type: 'string' }, + }, }, required: ['code'], additionalProperties: false, }), async execute(input: RunInput): Promise { - const apiKey = getApiKey(); - if (!input.code || typeof input.code !== 'string') { throw new Error('Code is required and must be a string'); } @@ -459,44 +665,41 @@ export const run = tool({ throw new Error('TTL must be between 1 and 900 seconds'); } - // Build query params for network_mode and ttl - const url = new URL(`${UNSANDBOX_API_BASE}/run`); + // Build query params + let path = '/run'; + const params = new URLSearchParams(); if (input.network_mode) { - url.searchParams.set('network_mode', input.network_mode); + params.set('network_mode', input.network_mode); } if (input.ttl) { - url.searchParams.set('ttl', input.ttl.toString()); + params.set('ttl', input.ttl.toString()); + } + if (input.env) { + params.set('env', JSON.stringify(input.env)); + } + const queryString = params.toString(); + if (queryString) { + path = `${path}?${queryString}`; } - const response = await fetch(url.toString(), { - method: 'POST', - headers: { - 'Content-Type': 'text/plain', - Authorization: `Bearer ${apiKey}`, - }, - body: input.code, - }); - - if (!response.ok) { - const errorText = await response.text().catch(() => 'Unknown error'); - throw new Error(`Unsandbox API error: HTTP ${response.status} - ${errorText}`); - } - - const result = (await response.json()) as RunResult; + const result = await apiRequestText(path, input.code); return { + job_id: result.job_id, + language: result.language || result.detected_language || 'unknown', + detected_language: result.detected_language || result.language || 'unknown', stdout: result.stdout || '', stderr: result.stderr || '', exit_code: result.exit_code ?? 0, - detected_language: result.detected_language || 'unknown', - duration_ms: result.duration_ms || 0, + success: result.success ?? (result.exit_code === 0), }; }, }); -/** - * Additional interfaces for job management - */ +// ============================================================================ +// Run Async (auto-detect language) +// ============================================================================ + export interface RunAsyncInput { code: string; network_mode?: NetworkMode; @@ -505,41 +708,25 @@ export interface RunAsyncInput { export interface RunAsyncResult { job_id: string; - status: string; -} - -export interface ListJobsResult { - jobs: Array<{ - id: string; - status: string; - language?: string; - created_at?: string; - }>; - count: number; -} - -export interface DeleteJobInput { - job_id: string; -} - -export interface DeleteJobResult { - deleted: boolean; - job_id: string; + status: 'pending'; + detected_language: string; + message: string; } /** - * Execute code asynchronously with automatic language detection from shebang. + * Execute code asynchronously with automatic language detection. * Returns a job_id immediately. Use getJob to check status and retrieve results. */ export const runAsync = tool({ description: - 'Execute code asynchronously with automatic language detection from shebang. Returns a job_id immediately. Use getJob to check status and retrieve results.', + 'Execute code asynchronously with automatic language detection. Returns a job_id immediately. Use getJob to check status and retrieve results.', inputSchema: jsonSchema({ type: 'object', properties: { code: { type: 'string', - description: 'The source code with shebang line (e.g., #!/usr/bin/env python)', + description: + 'The source code to execute. Can include shebang line (e.g., #!/usr/bin/env python) for explicit language hint.', }, network_mode: { type: 'string', @@ -555,8 +742,6 @@ export const runAsync = tool({ additionalProperties: false, }), async execute(input: RunAsyncInput): Promise { - const apiKey = getApiKey(); - if (!input.code || typeof input.code !== 'string') { throw new Error('Code is required and must be a string'); } @@ -565,37 +750,48 @@ export const runAsync = tool({ throw new Error('TTL must be between 1 and 900 seconds'); } - const url = new URL(`${UNSANDBOX_API_BASE}/run/async`); + // Build query params + let path = '/run/async'; + const params = new URLSearchParams(); if (input.network_mode) { - url.searchParams.set('network_mode', input.network_mode); + params.set('network_mode', input.network_mode); } if (input.ttl) { - url.searchParams.set('ttl', input.ttl.toString()); + params.set('ttl', input.ttl.toString()); + } + const queryString = params.toString(); + if (queryString) { + path = `${path}?${queryString}`; } - const response = await fetch(url.toString(), { - method: 'POST', - headers: { - 'Content-Type': 'text/plain', - Authorization: `Bearer ${apiKey}`, - }, - body: input.code, - }); - - if (!response.ok) { - const errorText = await response.text().catch(() => 'Unknown error'); - throw new Error(`Unsandbox API error: HTTP ${response.status} - ${errorText}`); - } - - const result = (await response.json()) as { job_id: string; status?: string }; + const result = await apiRequestText(path, input.code); return { job_id: result.job_id, - status: result.status || 'queued', + status: 'pending', + detected_language: result.detected_language || 'unknown', + message: result.message || 'Job accepted for execution', }; }, }); +// ============================================================================ +// List Jobs +// ============================================================================ + +export interface JobSummary { + job_id: string; + language: string; + network_mode: string; + status: JobStatus; + created_at: string; +} + +export interface ListJobsResult { + jobs: JobSummary[]; + count: number; +} + /** * List all active (pending or running) code execution jobs. */ @@ -608,41 +804,50 @@ export const listJobs = tool({ additionalProperties: false, }), async execute(): Promise { - const apiKey = getApiKey(); + // API returns array directly per OpenAPI spec + const result = await apiRequest('GET', '/jobs', undefined); - const response = await fetch(`${UNSANDBOX_API_BASE}/jobs`, { - method: 'GET', - headers: { - Authorization: `Bearer ${apiKey}`, - }, - }); - - if (!response.ok) { - const errorText = await response.text().catch(() => 'Unknown error'); - throw new Error(`Unsandbox API error: HTTP ${response.status} - ${errorText}`); - } - - const result = (await response.json()) as { jobs?: Array> }; - const jobs = (result.jobs || []).map((job) => ({ - id: String(job.id || job.job_id || ''), - status: String(job.status || 'unknown'), - language: job.language ? String(job.language) : undefined, - created_at: job.created_at ? String(job.created_at) : undefined, - })); + // Handle both array response and object with jobs array + const jobs = Array.isArray(result) ? result : []; return { - jobs, + jobs: jobs.map((job) => ({ + job_id: job.job_id || '', + language: job.language || 'unknown', + network_mode: job.network_mode || 'zerotrust', + status: job.status || 'pending', + created_at: job.created_at || '', + })), count: jobs.length, }; }, }); +// ============================================================================ +// Delete/Cancel Job +// ============================================================================ + +export interface DeleteJobInput { + job_id: string; +} + +export interface DeleteJobResult { + job_id: string; + status: string; + message: string; + stdout?: string; + stderr?: string; + success: boolean; + artifacts?: Artifact[]; +} + /** * Cancel a pending or running code execution job. + * Returns any partial output and compiled artifacts. */ export const deleteJob = tool({ description: - 'Cancel a pending or running code execution job. The job will be terminated and resources freed.', + 'Cancel a pending or running code execution job. Returns any partial output and compiled artifacts. Cancellation is best-effort - jobs may complete before cancellation takes effect.', inputSchema: jsonSchema({ type: 'object', properties: { @@ -655,36 +860,1623 @@ export const deleteJob = tool({ additionalProperties: false, }), async execute(input: DeleteJobInput): Promise { - const apiKey = getApiKey(); - if (!input.job_id || typeof input.job_id !== 'string') { throw new Error('job_id is required and must be a string'); } - const response = await fetch(`${UNSANDBOX_API_BASE}/jobs/${encodeURIComponent(input.job_id)}`, { - method: 'DELETE', - headers: { - Authorization: `Bearer ${apiKey}`, - }, - }); - - if (!response.ok) { - if (response.status === 404) { - throw new Error(`Job not found: ${input.job_id}`); - } - const errorText = await response.text().catch(() => 'Unknown error'); - throw new Error(`Unsandbox API error: HTTP ${response.status} - ${errorText}`); - } + const result = await apiRequest( + 'DELETE', + `/jobs/${encodeURIComponent(input.job_id)}`, + undefined + ); return { - deleted: true, - job_id: input.job_id, + job_id: result.job_id || input.job_id, + status: result.status || 'cancelled', + message: result.message || 'Job cancelled', + stdout: result.stdout, + stderr: result.stderr, + success: result.success ?? true, + artifacts: result.artifacts, }; }, }); -// Default export for convenience +// ============================================================================ +// Languages +// ============================================================================ + +export interface LanguagesResult { + languages: string[]; + aliases: Record; + count: number; +} + +/** + * List all supported programming languages. + */ +export const getLanguages = tool({ + description: + 'List all supported programming languages and their aliases for code execution.', + inputSchema: jsonSchema>({ + type: 'object', + properties: {}, + additionalProperties: false, + }), + async execute(): Promise { + const result = await apiRequest('GET', '/languages', undefined); + return { + languages: result.languages || [], + aliases: result.aliases || {}, + count: result.count || result.languages?.length || 0, + }; + }, +}); + +export interface ShellsResult { + shells: string[]; + categories: Record; + count: number; +} + +/** + * List all supported shells and REPLs for interactive sessions. + */ +export const getShells = tool({ + description: + 'List all supported shells and REPLs for interactive sessions, grouped by category.', + inputSchema: jsonSchema>({ + type: 'object', + properties: {}, + additionalProperties: false, + }), + async execute(): Promise { + const result = await apiRequest('GET', '/shells', undefined); + return { + shells: result.shells || [], + categories: result.categories || {}, + count: result.count || result.shells?.length || 0, + }; + }, +}); + +// ============================================================================ +// Sessions +// ============================================================================ + +export type SessionStatus = 'running' | 'frozen' | 'terminated'; + +export interface CreateSessionInput { + shell?: string; + network_mode?: NetworkMode; + ttl?: number; +} + +export interface SessionResult { + session_id: string; + status: SessionStatus; + shell: string; + network_mode: string; + websocket_url?: string; + created_at?: string; + expires_at?: string; +} + +/** + * Create an interactive shell session with WebSocket access. + */ +export const createSession = tool({ + description: + 'Create a persistent interactive shell session with WebSocket access. Sessions persist between commands and can be frozen/unfrozen.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + shell: { + type: 'string', + description: 'Shell or REPL to start (bash, python3, node, etc.). Default: bash', + }, + network_mode: { + type: 'string', + enum: ['zerotrust', 'semitrusted'], + description: "Network isolation mode. Default: 'zerotrust'", + }, + ttl: { + type: 'number', + description: 'Time-to-live in seconds (0 = no limit). Default: 3600', + }, + }, + additionalProperties: false, + }), + async execute(input: CreateSessionInput): Promise { + const requestBody: Record = {}; + if (input.shell) requestBody.shell = input.shell; + if (input.network_mode) requestBody.network_mode = input.network_mode; + if (input.ttl !== undefined) requestBody.ttl = input.ttl; + + const result = await apiRequest('POST', '/sessions', requestBody); + return result; + }, +}); + +export interface GetSessionInput { + session_id: string; +} + +/** + * Get session status and details. + */ +export const getSession = tool({ + description: 'Get detailed session information including status, shell, and WebSocket URL.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + session_id: { + type: 'string', + description: 'The session ID', + }, + }, + required: ['session_id'], + additionalProperties: false, + }), + async execute(input: GetSessionInput): Promise { + return apiRequest('GET', `/sessions/${encodeURIComponent(input.session_id)}`, undefined); + }, +}); + +export interface SessionSummary { + session_id: string; + status: string; + network_mode: string; + remaining_ttl?: number; +} + +export interface ListSessionsResult { + sessions: SessionSummary[]; +} + +/** + * List all active sessions. + */ +export const listSessions = tool({ + description: 'List all active interactive sessions for the authenticated API key.', + inputSchema: jsonSchema>({ + type: 'object', + properties: {}, + additionalProperties: false, + }), + async execute(): Promise { + const result = await apiRequest('GET', '/sessions', undefined); + return { sessions: result.sessions || [] }; + }, +}); + +export interface SessionCommandInput { + session_id: string; + command: string; +} + +export interface SessionCommandResult { + stdout: string; + stderr: string; + exit_code: number; +} + +/** + * Execute a command in a session. + */ +export const executeInSession = tool({ + description: "Run a command inside a session's container and return output.", + inputSchema: jsonSchema({ + type: 'object', + properties: { + session_id: { + type: 'string', + description: 'The session ID', + }, + command: { + type: 'string', + description: 'The command to execute', + }, + }, + required: ['session_id', 'command'], + additionalProperties: false, + }), + async execute(input: SessionCommandInput): Promise { + return apiRequest( + 'POST', + `/sessions/${encodeURIComponent(input.session_id)}/execute`, + { command: input.command } + ); + }, +}); + +export interface SessionIdInput { + session_id: string; +} + +export interface SessionStateResult { + session_id: string; + status: string; +} + +/** + * Freeze a session to save resources. + */ +export const freezeSession = tool({ + description: 'Freeze a session to save resources. Container state is preserved and can be woken later.', + 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)}/freeze`, + {} + ); + }, +}); + +export interface UnfreezeResult { + session_id: string; + status: string; + unfreeze_time_ms?: number; +} + +/** + * Wake a frozen session. + */ +export const unfreezeSession = tool({ + description: 'Wake a frozen session and restore its state.', + 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)}/unfreeze`, + {} + ); + }, +}); + +export interface LockResult { + locked: boolean; +} + +/** + * Lock a session to prevent accidental deletion. + */ +export const lockSession = tool({ + description: 'Lock a session to prevent accidental deletion.', + 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)}/lock`, + {} + ); + }, +}); + +/** + * Unlock a locked session. + */ +export const unlockSession = tool({ + description: 'Unlock a locked session to allow deletion.', + 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)}/unlock`, + {} + ); + }, +}); + +export interface CreateSnapshotInput { + session_id: string; + name?: string; + hot?: boolean; + ttl?: number; +} + +export interface SnapshotResult { + snapshot_id: string; + name?: string; + source_id: string; + source_type: string; + size_bytes?: number; + created_at?: string; +} + +/** + * Create a snapshot of a session. + */ +export const createSessionSnapshot = tool({ + description: "Create a snapshot of the session's container state.", + inputSchema: jsonSchema({ + type: 'object', + properties: { + session_id: { type: 'string', description: 'The session ID' }, + name: { type: 'string', description: 'Optional snapshot name' }, + hot: { type: 'boolean', description: 'Create hot snapshot without stopping container. Default: false' }, + ttl: { type: 'number', description: 'Auto-delete after N seconds (optional)' }, + }, + required: ['session_id'], + additionalProperties: false, + }), + async execute(input: CreateSnapshotInput): Promise { + const body: Record = {}; + if (input.name) body.name = input.name; + if (input.hot !== undefined) body.hot = input.hot; + if (input.ttl !== undefined) body.ttl = input.ttl; + + return apiRequest( + 'POST', + `/sessions/${encodeURIComponent(input.session_id)}/snapshot`, + body + ); + }, +}); + +export interface RestoreSessionInput { + session_id: string; + snapshot_id: string; +} + +/** + * Restore a session from a snapshot. + */ +export const restoreSession = tool({ + description: 'Restore a session to a previous snapshot state.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + session_id: { type: 'string', description: 'The session ID' }, + snapshot_id: { type: 'string', description: 'The snapshot ID to restore from' }, + }, + required: ['session_id', 'snapshot_id'], + additionalProperties: false, + }), + async execute(input: RestoreSessionInput): Promise { + return apiRequest( + 'POST', + `/sessions/${encodeURIComponent(input.session_id)}/restore`, + { snapshot_id: input.snapshot_id } + ); + }, +}); + +/** + * Delete a session. + */ +export const deleteSession = tool({ + description: 'Terminate and destroy a session permanently.', + 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( + 'DELETE', + `/sessions/${encodeURIComponent(input.session_id)}`, + undefined + ); + }, +}); + +// ============================================================================ +// Services +// ============================================================================ + +export type ServiceState = 'starting' | 'running' | 'frozen' | 'redeploying' | 'failed'; + +export interface CreateServiceInput { + name: string; + bootstrap?: string; + bootstrap_content?: string; + ports?: number[]; + network_mode?: NetworkMode; + custom_domains?: string[]; + input_files?: InputFile[]; +} + +export interface ServiceResult { + service_id: string; + name: string; + state: ServiceState; + url?: string; + ports?: number[]; + created_at?: string; +} + +/** + * Create a persistent service with custom subdomain. + */ +export const createService = tool({ + description: + 'Create a long-running service with custom subdomain. Services persist until explicitly destroyed and can auto-unfreeze on HTTP requests.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + name: { type: 'string', description: 'Service name (becomes NAME.on.unsandbox.com)' }, + bootstrap: { type: 'string', description: 'Bootstrap script content or URL' }, + bootstrap_content: { type: 'string', description: 'Bootstrap script content (alternative to bootstrap)' }, + ports: { + type: 'array', + items: { type: 'number' }, + description: 'Ports to expose', + }, + network_mode: { + type: 'string', + enum: ['zerotrust', 'semitrusted'], + description: "Network isolation mode. Default: 'semitrusted'", + }, + custom_domains: { + type: 'array', + items: { type: 'string' }, + description: 'Custom domain names', + }, + input_files: { + type: 'array', + description: 'Input files to make available', + items: { + type: 'object', + properties: { + filename: { type: 'string' }, + content: { type: 'string' }, + }, + required: ['filename', 'content'], + }, + }, + }, + required: ['name'], + additionalProperties: false, + }), + async execute(input: CreateServiceInput): Promise { + return apiRequest('POST', '/services', input); + }, +}); + +export interface ServiceIdInput { + service_id: string; +} + +/** + * Get service status and details. + */ +export const getService = tool({ + description: 'Get detailed service information including state, ports, and URL.', + 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)}`, undefined); + }, +}); + +export interface ServiceSummary { + service_id: string; + name: string; + state: string; + ports?: number[]; + domains?: string[]; +} + +export interface ListServicesResult { + services: ServiceSummary[]; +} + +/** + * List all services. + */ +export const listServices = tool({ + description: 'List all services for the authenticated API key.', + inputSchema: jsonSchema>({ + type: 'object', + properties: {}, + additionalProperties: false, + }), + async execute(): Promise { + const result = await apiRequest('GET', '/services', undefined); + return { services: result.services || [] }; + }, +}); + +export interface ServiceCommandInput { + service_id: string; + command: string; +} + +/** + * Execute a command in a service. + */ +export const executeInService = tool({ + description: 'Run a command inside a running service container.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + service_id: { type: 'string', description: 'The service ID' }, + command: { type: 'string', description: 'The command to execute' }, + }, + required: ['service_id', 'command'], + additionalProperties: false, + }), + async execute(input: ServiceCommandInput): Promise { + return apiRequest( + 'POST', + `/services/${encodeURIComponent(input.service_id)}/execute`, + { command: input.command } + ); + }, +}); + +export interface ServiceStateResult { + service_id: string; + state: string; +} + +/** + * Freeze a service to save resources. + */ +export const freezeService = tool({ + description: 'Freeze a service to save resources. Auto-unfreezes on first HTTP request.', + 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)}/freeze`, + {} + ); + }, +}); + +export interface ServiceUnfreezeResult { + service_id: string; + state: string; + unfreeze_time_ms?: number; +} + +/** + * Wake a frozen service. + */ +export const unfreezeService = tool({ + description: 'Manually wake a frozen service.', + 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)}/unfreeze`, + {} + ); + }, +}); + +/** + * Lock a service to prevent accidental deletion. + */ +export const lockService = tool({ + description: 'Lock a service to prevent accidental deletion.', + 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)}/lock`, + {} + ); + }, +}); + +/** + * Unlock a locked service. + */ +export const unlockService = tool({ + description: 'Unlock a locked service to allow deletion.', + 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)}/unlock`, + {} + ); + }, +}); + +export interface RedeployServiceInput { + service_id: string; + bootstrap_content?: string; +} + +/** + * Redeploy a service. + */ +export const redeployService = tool({ + description: 'Re-run the bootstrap script. Optionally provide new bootstrap content.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + service_id: { type: 'string', description: 'The service ID' }, + bootstrap_content: { type: 'string', description: 'New bootstrap script (optional)' }, + }, + required: ['service_id'], + additionalProperties: false, + }), + async execute(input: RedeployServiceInput): Promise { + const body: Record = {}; + if (input.bootstrap_content) body.bootstrap_content = input.bootstrap_content; + return apiRequest( + 'POST', + `/services/${encodeURIComponent(input.service_id)}/redeploy`, + body + ); + }, +}); + +export interface ServiceLogsResult { + log: string; +} + +/** + * Get service logs. + */ +export const getServiceLogs = tool({ + description: 'Get bootstrap and application logs for the service.', + 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)}/logs`, + undefined + ); + }, +}); + +export interface CreateServiceSnapshotInput { + service_id: string; + name?: string; + hot?: boolean; + ttl?: number; +} + +/** + * Create a snapshot of a service. + */ +export const createServiceSnapshot = tool({ + description: "Create a snapshot of the service's container state.", + inputSchema: jsonSchema({ + type: 'object', + properties: { + service_id: { type: 'string', description: 'The service ID' }, + name: { type: 'string', description: 'Optional snapshot name' }, + hot: { type: 'boolean', description: 'Create hot snapshot without stopping container' }, + ttl: { type: 'number', description: 'Auto-delete after N seconds' }, + }, + required: ['service_id'], + additionalProperties: false, + }), + async execute(input: CreateServiceSnapshotInput): Promise { + const body: Record = {}; + if (input.name) body.name = input.name; + if (input.hot !== undefined) body.hot = input.hot; + if (input.ttl !== undefined) body.ttl = input.ttl; + + return apiRequest( + 'POST', + `/services/${encodeURIComponent(input.service_id)}/snapshot`, + body + ); + }, +}); + +export interface EnvVarsResult { + env: Record; +} + +/** + * Get service environment variables. + */ +export const getServiceEnv = tool({ + description: 'Retrieve all environment variables set for the service.', + 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)}/env`, + undefined + ); + }, +}); + +export interface SetServiceEnvInput { + service_id: string; + env: Record; +} + +export interface SetEnvResult { + success: boolean; + env: Record; +} + +/** + * Set service environment variables. + */ +export const setServiceEnv = tool({ + description: 'Set or update environment variables for the service.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + service_id: { type: 'string', description: 'The service ID' }, + env: { + type: 'object', + description: 'Environment variables as key-value pairs', + additionalProperties: { type: 'string' }, + }, + }, + required: ['service_id', 'env'], + additionalProperties: false, + }), + async execute(input: SetServiceEnvInput): Promise { + return apiRequest( + 'PUT', + `/services/${encodeURIComponent(input.service_id)}/env`, + { env: input.env } + ); + }, +}); + +export interface DeleteServiceEnvInput { + service_id: string; + keys: string[]; +} + +export interface DeleteEnvResult { + success: boolean; + deleted: string[]; +} + +/** + * Delete service environment variables. + */ +export const deleteServiceEnv = tool({ + description: 'Remove specific environment variables from the service.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + service_id: { type: 'string', description: 'The service ID' }, + keys: { + type: 'array', + items: { type: 'string' }, + description: 'Environment variable keys to delete', + }, + }, + required: ['service_id', 'keys'], + additionalProperties: false, + }), + async execute(input: DeleteServiceEnvInput): Promise { + return apiRequest( + 'DELETE', + `/services/${encodeURIComponent(input.service_id)}/env`, + { keys: input.keys } + ); + }, +}); + +/** + * Delete a service. + */ +export const deleteService = tool({ + description: 'Permanently destroy a service and its container.', + 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( + 'DELETE', + `/services/${encodeURIComponent(input.service_id)}`, + undefined + ); + }, +}); + +// ============================================================================ +// Snapshots +// ============================================================================ + +export interface CreateSnapshotFromSourceInput { + source_type: 'session' | 'service'; + source_id: string; + name?: string; + hot?: boolean; + ttl?: number; +} + +/** + * Create a snapshot from an existing session or service. + */ +export const createSnapshot = tool({ + description: 'Create a snapshot from an existing session or service.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + source_type: { + type: 'string', + enum: ['session', 'service'], + description: 'Type of source to snapshot', + }, + source_id: { type: 'string', description: 'ID of the session or service' }, + name: { type: 'string', description: 'Optional snapshot name' }, + hot: { type: 'boolean', description: 'Create hot snapshot without stopping container' }, + ttl: { type: 'number', description: 'Auto-delete after N seconds' }, + }, + required: ['source_type', 'source_id'], + additionalProperties: false, + }), + async execute(input: CreateSnapshotFromSourceInput): Promise { + return apiRequest('POST', '/snapshots', input); + }, +}); + +export interface SnapshotIdInput { + snapshot_id: string; +} + +/** + * Get snapshot details. + */ +export const getSnapshot = tool({ + description: 'Get detailed information about a snapshot.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + snapshot_id: { type: 'string', description: 'The snapshot ID' }, + }, + required: ['snapshot_id'], + additionalProperties: false, + }), + async execute(input: SnapshotIdInput): Promise { + return apiRequest('GET', `/snapshots/${encodeURIComponent(input.snapshot_id)}`, undefined); + }, +}); + +export interface SnapshotSummary { + id: string; + name?: string; + source_type: string; + source_id: string; + size_bytes?: number; + locked?: boolean; +} + +export interface ListSnapshotsResult { + snapshots: SnapshotSummary[]; +} + +/** + * List all snapshots. + */ +export const listSnapshots = tool({ + description: 'List all snapshots for the authenticated API key.', + inputSchema: jsonSchema>({ + type: 'object', + properties: {}, + additionalProperties: false, + }), + async execute(): Promise { + const result = await apiRequest('GET', '/snapshots', undefined); + return { snapshots: result.snapshots || [] }; + }, +}); + +/** + * Lock a snapshot. + */ +export const lockSnapshot = tool({ + description: 'Lock a snapshot to prevent accidental deletion.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + snapshot_id: { type: 'string', description: 'The snapshot ID' }, + }, + required: ['snapshot_id'], + additionalProperties: false, + }), + async execute(input: SnapshotIdInput): Promise { + return apiRequest( + 'POST', + `/snapshots/${encodeURIComponent(input.snapshot_id)}/lock`, + {} + ); + }, +}); + +/** + * Unlock a snapshot. + */ +export const unlockSnapshot = tool({ + description: 'Unlock a locked snapshot to allow deletion.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + snapshot_id: { type: 'string', description: 'The snapshot ID' }, + }, + required: ['snapshot_id'], + additionalProperties: false, + }), + async execute(input: SnapshotIdInput): Promise { + return apiRequest( + 'POST', + `/snapshots/${encodeURIComponent(input.snapshot_id)}/unlock`, + {} + ); + }, +}); + +export interface RestoreSnapshotResult { + snapshot_id: string; + status: string; +} + +/** + * Restore a snapshot to its original source. + */ +export const restoreSnapshot = tool({ + description: 'Restore a snapshot to its original source (session or service).', + inputSchema: jsonSchema({ + type: 'object', + properties: { + snapshot_id: { type: 'string', description: 'The snapshot ID' }, + }, + required: ['snapshot_id'], + additionalProperties: false, + }), + async execute(input: SnapshotIdInput): Promise { + return apiRequest( + 'POST', + `/snapshots/${encodeURIComponent(input.snapshot_id)}/restore`, + {} + ); + }, +}); + +export interface CloneSnapshotInput { + snapshot_id: string; + type: 'session' | 'service'; + name?: string; + shell?: string; +} + +export interface CloneSnapshotResult { + session_id?: string; + service_id?: string; + name?: string; + status?: string; + state?: string; +} + +/** + * Clone a snapshot to create a new session or service. + */ +export const cloneSnapshot = tool({ + description: 'Create a new session or service from a snapshot.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + snapshot_id: { type: 'string', description: 'The snapshot ID' }, + type: { + type: 'string', + enum: ['session', 'service'], + description: 'Type of resource to create', + }, + name: { type: 'string', description: 'Service name (required for service type)' }, + shell: { type: 'string', description: 'Shell to use (for session type)' }, + }, + required: ['snapshot_id', 'type'], + additionalProperties: false, + }), + async execute(input: CloneSnapshotInput): Promise { + const body: Record = { type: input.type }; + if (input.name) body.name = input.name; + if (input.shell) body.shell = input.shell; + + return apiRequest( + 'POST', + `/snapshots/${encodeURIComponent(input.snapshot_id)}/clone`, + body + ); + }, +}); + +export interface DeleteSnapshotResult { + snapshot_id: string; + status: string; +} + +/** + * Delete a snapshot. + */ +export const deleteSnapshot = tool({ + description: 'Permanently delete a snapshot.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + snapshot_id: { type: 'string', description: 'The snapshot ID' }, + }, + required: ['snapshot_id'], + additionalProperties: false, + }), + async execute(input: SnapshotIdInput): Promise { + return apiRequest( + 'DELETE', + `/snapshots/${encodeURIComponent(input.snapshot_id)}`, + undefined + ); + }, +}); + +// ============================================================================ +// Images +// ============================================================================ + +export type ImageVisibility = 'private' | 'unlisted' | 'public'; + +export interface PublishImageInput { + source_type: 'service' | 'snapshot'; + source_id: string; + name?: string; + description?: string; +} + +export interface ImageResult { + id: string; + name?: string; + description?: string; + fingerprint?: string; + source_type: string; + source_id: string; + owner_api_key?: string; + visibility?: ImageVisibility; + locked?: boolean; + size_bytes?: number; + trusted_keys?: string[]; + node?: string; + created_at?: string; +} + +/** + * Publish an image from a service or snapshot. + */ +export const publishImage = tool({ + description: + 'Create an independent LXD image from a service or snapshot. Images survive container deletion and can be transferred between API keys.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + source_type: { + type: 'string', + enum: ['service', 'snapshot'], + description: 'Type of source to publish from', + }, + source_id: { type: 'string', description: 'ID of the service or snapshot' }, + name: { type: 'string', description: 'User-friendly name for the image' }, + description: { type: 'string', description: 'Optional description' }, + }, + required: ['source_type', 'source_id'], + additionalProperties: false, + }), + async execute(input: PublishImageInput): Promise { + return apiRequest('POST', '/images', input); + }, +}); + +export interface ImageIdInput { + image_id: string; +} + +/** + * Get image details. + */ +export const getImage = tool({ + description: 'Get detailed information about an image.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + image_id: { type: 'string', description: 'The image ID' }, + }, + required: ['image_id'], + additionalProperties: false, + }), + async execute(input: ImageIdInput): Promise { + return apiRequest('GET', `/images/${encodeURIComponent(input.image_id)}`, undefined); + }, +}); + +export interface ImageSummary { + id: string; + name?: string; + fingerprint?: string; + visibility?: ImageVisibility; + locked?: boolean; + size_bytes?: number; + created_at?: string; +} + +export interface ListImagesResult { + images: ImageSummary[]; +} + +/** + * List all images. + */ +export const listImages = tool({ + description: 'List all images owned by or shared with the authenticated API key.', + inputSchema: jsonSchema>({ + type: 'object', + properties: {}, + additionalProperties: false, + }), + async execute(): Promise { + const result = await apiRequest('GET', '/images', undefined); + return { images: result.images || [] }; + }, +}); + +/** + * Lock an image. + */ +export const lockImage = tool({ + description: 'Lock an image to prevent accidental deletion.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + image_id: { type: 'string', description: 'The image ID' }, + }, + required: ['image_id'], + additionalProperties: false, + }), + async execute(input: ImageIdInput): Promise { + return apiRequest( + 'POST', + `/images/${encodeURIComponent(input.image_id)}/lock`, + {} + ); + }, +}); + +/** + * Unlock an image. + */ +export const unlockImage = tool({ + description: 'Unlock a locked image to allow deletion.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + image_id: { type: 'string', description: 'The image ID' }, + }, + required: ['image_id'], + additionalProperties: false, + }), + async execute(input: ImageIdInput): Promise { + return apiRequest( + 'POST', + `/images/${encodeURIComponent(input.image_id)}/unlock`, + {} + ); + }, +}); + +export interface GrantImageAccessInput { + image_id: string; + api_key: string; +} + +/** + * Grant image access to another API key. + */ +export const grantImageAccess = tool({ + description: + 'Grant access to a private/unlisted image for a specific API key. The granted user can use the image to spawn services.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + image_id: { type: 'string', description: 'The image ID' }, + api_key: { type: 'string', description: 'API key to grant access to' }, + }, + required: ['image_id', 'api_key'], + additionalProperties: false, + }), + async execute(input: GrantImageAccessInput): Promise { + return apiRequest( + 'POST', + `/images/${encodeURIComponent(input.image_id)}/grant`, + { api_key: input.api_key } + ); + }, +}); + +/** + * Revoke image access from an API key. + */ +export const revokeImageAccess = tool({ + description: 'Revoke previously granted access from an API key.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + image_id: { type: 'string', description: 'The image ID' }, + api_key: { type: 'string', description: 'API key to revoke access from' }, + }, + required: ['image_id', 'api_key'], + additionalProperties: false, + }), + async execute(input: GrantImageAccessInput): Promise { + return apiRequest( + 'POST', + `/images/${encodeURIComponent(input.image_id)}/revoke`, + { api_key: input.api_key } + ); + }, +}); + +export interface TransferImageInput { + image_id: string; + to_api_key: string; +} + +/** + * Transfer image ownership to another API key. + */ +export const transferImage = tool({ + description: + 'Transfer full ownership of an image to another API key. The image stays on the same LXD node.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + image_id: { type: 'string', description: 'The image ID' }, + to_api_key: { type: 'string', description: "Recipient's public API key" }, + }, + required: ['image_id', 'to_api_key'], + additionalProperties: false, + }), + async execute(input: TransferImageInput): Promise { + return apiRequest( + 'POST', + `/images/${encodeURIComponent(input.image_id)}/transfer`, + { to_api_key: input.to_api_key } + ); + }, +}); + +export interface SetImageVisibilityInput { + image_id: string; + visibility: ImageVisibility; +} + +/** + * Set image visibility. + */ +export const setImageVisibility = tool({ + description: + 'Control who can see and use this image. Options: private (only owner), unlisted (shareable via trust), public (visible to all).', + inputSchema: jsonSchema({ + type: 'object', + properties: { + image_id: { type: 'string', description: 'The image ID' }, + visibility: { + type: 'string', + enum: ['private', 'unlisted', 'public'], + description: 'Visibility setting', + }, + }, + required: ['image_id', 'visibility'], + additionalProperties: false, + }), + async execute(input: SetImageVisibilityInput): Promise { + return apiRequest( + 'POST', + `/images/${encodeURIComponent(input.image_id)}/visibility`, + { visibility: input.visibility } + ); + }, +}); + +export interface SpawnFromImageInput { + image_id: string; + name?: string; + network_mode?: NetworkMode; + ports?: number[]; + bootstrap?: string; +} + +export interface SpawnFromImageResult { + service_id: string; + name?: string; + source_image: string; + state: string; +} + +/** + * Spawn a service from an image. + */ +export const spawnFromImage = tool({ + description: 'Create a new service using this image as the base.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + image_id: { type: 'string', description: 'The image ID' }, + name: { type: 'string', description: 'Service name' }, + network_mode: { + type: 'string', + enum: ['zerotrust', 'semitrusted'], + description: "Network isolation mode. Default: 'zerotrust'", + }, + ports: { + type: 'array', + items: { type: 'number' }, + description: 'Ports to expose', + }, + bootstrap: { type: 'string', description: 'Optional bootstrap script' }, + }, + required: ['image_id'], + additionalProperties: false, + }), + async execute(input: SpawnFromImageInput): Promise { + const body: Record = {}; + if (input.name) body.name = input.name; + if (input.network_mode) body.network_mode = input.network_mode; + if (input.ports) body.ports = input.ports; + if (input.bootstrap) body.bootstrap = input.bootstrap; + + return apiRequest( + 'POST', + `/images/${encodeURIComponent(input.image_id)}/spawn`, + body + ); + }, +}); + +export interface TrustedKeysResult { + trusted_keys: string[]; +} + +/** + * List trusted API keys for an image. + */ +export const getImageTrustedKeys = tool({ + description: 'List all API keys that have been granted access to this image.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + image_id: { type: 'string', description: 'The image ID' }, + }, + required: ['image_id'], + additionalProperties: false, + }), + async execute(input: ImageIdInput): Promise { + return apiRequest( + 'GET', + `/images/${encodeURIComponent(input.image_id)}/trusted`, + undefined + ); + }, +}); + +export interface DeleteImageResult { + success: boolean; + message: string; +} + +/** + * Delete an image. + */ +export const deleteImage = tool({ + description: 'Permanently delete an image from LXD and the database. Cannot delete locked images.', + inputSchema: jsonSchema({ + type: 'object', + properties: { + image_id: { type: 'string', description: 'The image ID' }, + }, + required: ['image_id'], + additionalProperties: false, + }), + async execute(input: ImageIdInput): Promise { + return apiRequest( + 'DELETE', + `/images/${encodeURIComponent(input.image_id)}`, + undefined + ); + }, +}); + +// ============================================================================ +// System +// ============================================================================ + +export interface HealthResult { + status: string; +} + +/** + * Health check. + */ +export const healthCheck = tool({ + description: 'Simple health check endpoint to verify the Unsandbox API is operational.', + inputSchema: jsonSchema>({ + type: 'object', + properties: {}, + additionalProperties: false, + }), + async execute(): Promise { + return apiRequest('GET', '/health', undefined); + }, +}); + +export interface ClusterStatusResult { + mode: string; + network_mode: string; + pool_size: number; + available: number; + allocated: number; + spawning: number; + total_containers: number; + network_breakdown?: { + zerotrust?: { total: number; available: number; allocated: number }; + semitrusted?: { total: number; available: number; allocated: number; services?: number }; + }; +} + +/** + * Get cluster status. + */ +export const getClusterStatus = tool({ + description: 'Get information about the container pool and system status.', + inputSchema: jsonSchema>({ + type: 'object', + properties: {}, + additionalProperties: false, + }), + async execute(): Promise { + return apiRequest('GET', '/cluster', undefined); + }, +}); + +export interface SystemStatsResult { + containers: number; + load_1min: string; + load_5min: string; + load_15min: string; +} + +/** + * Get system statistics. + */ +export const getSystemStats = tool({ + description: 'Get detailed system statistics including load and container metrics.', + inputSchema: jsonSchema>({ + type: 'object', + properties: {}, + additionalProperties: false, + }), + async execute(): Promise { + return apiRequest('GET', '/stats', undefined); + }, +}); + +export interface PoolInfo { + id: string; + total: number; + available: number; + allocated: number; +} + +export interface ListPoolsResult { + pools: PoolInfo[]; + pool_count: number; + total_capacity: number; + total_available: number; + total_allocated: number; +} + +/** + * List all pools. + */ +export const listPools = tool({ + description: 'Get status of all registered container pools (horizontal scaling).', + inputSchema: jsonSchema>({ + type: 'object', + properties: {}, + additionalProperties: false, + }), + async execute(): Promise { + return apiRequest('GET', '/pools', undefined); + }, +}); + +// ============================================================================ +// Default Export +// ============================================================================ + +// Default export with all tools for convenience export default { + // Execution executeCodeAsync, getJob, execute, @@ -692,4 +2484,62 @@ export default { runAsync, listJobs, deleteJob, + // Languages + getLanguages, + getShells, + // Sessions + createSession, + getSession, + listSessions, + executeInSession, + freezeSession, + unfreezeSession, + lockSession, + unlockSession, + createSessionSnapshot, + restoreSession, + deleteSession, + // Services + createService, + getService, + listServices, + executeInService, + freezeService, + unfreezeService, + lockService, + unlockService, + redeployService, + getServiceLogs, + createServiceSnapshot, + getServiceEnv, + setServiceEnv, + deleteServiceEnv, + deleteService, + // Snapshots + createSnapshot, + getSnapshot, + listSnapshots, + lockSnapshot, + unlockSnapshot, + restoreSnapshot, + cloneSnapshot, + deleteSnapshot, + // Images + publishImage, + getImage, + listImages, + lockImage, + unlockImage, + grantImageAccess, + revokeImageAccess, + transferImage, + setImageVisibility, + spawnFromImage, + getImageTrustedKeys, + deleteImage, + // System + healthCheck, + getClusterStatus, + getSystemStats, + listPools, };