diff --git a/.changeset/sandbox-environments.md b/.changeset/sandbox-environments.md new file mode 100644 index 000000000..52f0ac82b --- /dev/null +++ b/.changeset/sandbox-environments.md @@ -0,0 +1,6 @@ +--- +"@truefoundry/trueforge": minor +"@truefoundry/trueforge-core": minor +--- + +Add owned sandbox environments (CRUD + AgentSpec.environment) gated by tenant CREATE permission. diff --git a/.github/fern/openapi/openapi.json b/.github/fern/openapi/openapi.json index b40934b3f..502d645cf 100644 --- a/.github/fern/openapi/openapi.json +++ b/.github/fern/openapi/openapi.json @@ -1053,6 +1053,27 @@ ], "type": "object" }, + "CreateSandboxEnvironmentRequest": { + "additionalProperties": false, + "properties": { + "description": { + "description": "Optional human-readable description.", + "maxLength": 1024, + "type": "string" + }, + "manifest": { + "$ref": "#/components/schemas/SandboxEnvironmentManifest" + }, + "name": { + "$ref": "#/components/schemas/ResourceName" + } + }, + "required": [ + "name", + "manifest" + ], + "type": "object" + }, "CreateScheduleRequest": { "additionalProperties": false, "properties": { @@ -1226,6 +1247,147 @@ ], "type": "object" }, + "DaytonaDockerImage": { + "additionalProperties": false, + "properties": { + "ref": { + "description": "Container image reference passed to Daytona create-from-image.", + "minLength": 1, + "type": "string" + }, + "type": { + "description": "Build/create from a container image reference.", + "enum": [ + "docker" + ], + "type": "string" + } + }, + "required": [ + "type", + "ref" + ], + "type": "object" + }, + "DaytonaGpuType": { + "description": "Preferred Daytona GPU type.", + "enum": [ + "H100", + "H200", + "RTX-PRO-6000", + "RTX-4090", + "RTX-5090" + ], + "type": "string" + }, + "DaytonaSandboxEnvironmentImage": { + "discriminator": { + "mapping": { + "docker": "#/components/schemas/DaytonaDockerImage", + "snapshot": "#/components/schemas/DaytonaSnapshotImage", + "trueforge-default": "#/components/schemas/DaytonaTrueforgeDefaultImage" + }, + "propertyName": "type" + }, + "oneOf": [ + { + "$ref": "#/components/schemas/DaytonaTrueforgeDefaultImage" + }, + { + "$ref": "#/components/schemas/DaytonaSnapshotImage" + }, + { + "$ref": "#/components/schemas/DaytonaDockerImage" + } + ] + }, + "DaytonaSandboxEnvironmentLifecycle": { + "additionalProperties": false, + "properties": { + "auto_archive_interval_in_minutes": { + "description": "Minutes before Daytona auto-archives the sandbox (0 disables).", + "minimum": 0, + "type": "integer" + }, + "auto_delete_interval_in_minutes": { + "description": "Minutes before Daytona auto-deletes the sandbox (0 disables).", + "minimum": 0, + "type": "integer" + }, + "auto_stop_interval_in_minutes": { + "description": "Minutes of idle time before Daytona auto-stops the sandbox (0 disables).", + "minimum": 0, + "type": "integer" + } + }, + "type": "object" + }, + "DaytonaSandboxEnvironmentNetworking": { + "additionalProperties": false, + "properties": { + "domain_allow_list": { + "description": "Comma-separated allowed domains.", + "minLength": 1, + "type": "string" + }, + "network_allow_list": { + "description": "Comma-separated allowed CIDR network addresses.", + "minLength": 1, + "type": "string" + }, + "network_block_all": { + "description": "Block all outbound network access.", + "type": "boolean" + }, + "outbound_proxy_url": { + "description": "Outbound HTTP(S) proxy URL.", + "minLength": 1, + "type": "string" + } + }, + "type": "object" + }, + "DaytonaSandboxEnvironmentResources": { + "additionalProperties": false, + "properties": { + "cpu": { + "description": "CPU allocation in cores.", + "exclusiveMinimum": 0, + "type": "number" + }, + "disk": { + "description": "Disk allocation in GiB.", + "exclusiveMinimum": 0, + "type": "number" + }, + "gpu": { + "description": "GPU allocation in Daytona GPU units.", + "minimum": 0, + "type": "number" + }, + "gpu_type": { + "anyOf": [ + { + "$ref": "#/components/schemas/DaytonaGpuType" + }, + { + "items": { + "$ref": "#/components/schemas/DaytonaGpuType" + }, + "minItems": 1, + "type": "array" + } + ], + "description": "Preferred GPU type, or an ordered fallback list." + }, + "memory": { + "description": "Memory allocation in GiB.", + "exclusiveMinimum": 0, + "type": "number" + } + }, + "type": "object" + }, "DaytonaSandboxProviderAuth": { "additionalProperties": false, "description": "Daytona authentication credentials.", @@ -1241,10 +1403,52 @@ ], "type": "object" }, + "DaytonaSnapshotImage": { + "additionalProperties": false, + "properties": { + "name": { + "description": "Daytona snapshot name.", + "minLength": 1, + "type": "string" + }, + "type": { + "description": "Clone an existing Daytona snapshot by name.", + "enum": [ + "snapshot" + ], + "type": "string" + } + }, + "required": [ + "type", + "name" + ], + "type": "object" + }, + "DaytonaTrueforgeDefaultImage": { + "additionalProperties": false, + "properties": { + "type": { + "description": "Use the tenant provider TrueForge release snapshot.", + "enum": [ + "trueforge-default" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "type": "object" + }, "DeleteAgentResponse": { "properties": {}, "type": "object" }, + "DeleteSandboxEnvironmentResponse": { + "properties": {}, + "type": "object" + }, "DeleteScheduleResponse": { "properties": {}, "type": "object" @@ -1528,6 +1732,17 @@ ], "type": "object" }, + "GetSandboxEnvironmentResponse": { + "properties": { + "data": { + "$ref": "#/components/schemas/SandboxEnvironment" + } + }, + "required": [ + "data" + ], + "type": "object" + }, "GetSandboxProviderCatalogResponse": { "properties": { "data": { @@ -2277,7 +2492,7 @@ }, "type": "array" }, - "description": "For agent/schedule/session: keyed by resource id. For tenant: keyed by entity kind (e.g. `agent` → `CREATE`).", + "description": "For agent/schedule/session: keyed by resource id. For tenant: keyed by entity kind (e.g. `agent` or `sandbox-environment` → `CREATE`).", "type": "object" }, "type": { @@ -2322,6 +2537,24 @@ ], "type": "object" }, + "ListSandboxEnvironmentsResponse": { + "properties": { + "data": { + "items": { + "$ref": "#/components/schemas/SandboxEnvironment" + }, + "type": "array" + }, + "pagination": { + "$ref": "#/components/schemas/TokenPagination" + } + }, + "required": [ + "data", + "pagination" + ], + "type": "object" + }, "ListScheduleRunsResponse": { "properties": { "data": { @@ -3658,6 +3891,11 @@ "description": "Give the agent a sandbox. Required for skills and Code Mode.", "type": "boolean" }, + "environment": { + "description": "Caller-owned sandbox environment name. Omit to use the tenant provider defaults.", + "minLength": 1, + "type": "string" + }, "file_downloads": { "default": true, "description": "Allow downloading agent-produced files via the turn download endpoint. Default: true.", @@ -3707,6 +3945,97 @@ ], "type": "object" }, + "SandboxEnvironment": { + "additionalProperties": false, + "properties": { + "created_at": { + "description": "ISO-8601 create time.", + "format": "date-time", + "type": "string" + }, + "created_by_subject": { + "$ref": "#/components/schemas/CreatedBySubject" + }, + "description": { + "description": "Optional human-readable description.", + "type": [ + "string", + "null" + ] + }, + "id": { + "description": "Immutable server-generated environment identifier.", + "minLength": 1, + "type": "string" + }, + "manifest": { + "$ref": "#/components/schemas/SandboxEnvironmentManifest" + }, + "name": { + "$ref": "#/components/schemas/ResourceName" + }, + "updated_at": { + "description": "ISO-8601 last update time.", + "format": "date-time", + "type": "string" + } + }, + "required": [ + "id", + "name", + "description", + "manifest", + "created_by_subject", + "created_at", + "updated_at" + ], + "type": "object" + }, + "SandboxEnvironmentManifest": { + "additionalProperties": false, + "properties": { + "image": { + "$ref": "#/components/schemas/DaytonaSandboxEnvironmentImage" + }, + "lifecycle": { + "$ref": "#/components/schemas/DaytonaSandboxEnvironmentLifecycle" + }, + "networking": { + "$ref": "#/components/schemas/DaytonaSandboxEnvironmentNetworking" + }, + "provider": { + "description": "Must match the configured sandbox provider name/type.", + "enum": [ + "daytona" + ], + "type": "string" + }, + "resources": { + "$ref": "#/components/schemas/DaytonaSandboxEnvironmentResources" + }, + "secrets": { + "additionalProperties": { + "minLength": 1, + "type": "string" + }, + "description": "Map of sandbox env var name to an existing Daytona organization secret name.", + "type": "object" + }, + "type": { + "description": "Daytona sandbox environment.", + "enum": [ + "daytona" + ], + "type": "string" + } + }, + "required": [ + "type", + "provider", + "image" + ], + "type": "object" + }, "SandboxProviderManifest": { "additionalProperties": false, "properties": { @@ -5581,15 +5910,35 @@ ], "type": "object" }, - "UpdateSandboxProviderRequest": { + "UpdateSandboxEnvironmentRequest": { "additionalProperties": false, "properties": { - "manifest": { - "$ref": "#/components/schemas/SandboxProviderManifest" - } - }, - "required": [ - "manifest" + "description": { + "description": "Optional human-readable description.", + "maxLength": 1024, + "type": [ + "string", + "null" + ] + }, + "manifest": { + "$ref": "#/components/schemas/SandboxEnvironmentManifest" + } + }, + "required": [ + "manifest" + ], + "type": "object" + }, + "UpdateSandboxProviderRequest": { + "additionalProperties": false, + "properties": { + "manifest": { + "$ref": "#/components/schemas/SandboxProviderManifest" + } + }, + "required": [ + "manifest" ], "type": "object" }, @@ -7537,6 +7886,367 @@ "x-fern-sdk-method-name": "list" } }, + "/api/v1/sandbox-environments": { + "get": { + "description": "List sandbox environments created by the authenticated subject, newest first.", + "parameters": [ + { + "description": "Page size. Defaults to 25", + "in": "query", + "name": "limit", + "required": false, + "schema": { + "default": 25, + "description": "Page size. Defaults to 25", + "maximum": 25, + "minimum": 1, + "type": "integer" + } + }, + { + "description": "Opaque token from a previous response `next_page_token`.", + "in": "query", + "name": "page_token", + "required": false, + "schema": { + "description": "Opaque token from a previous response `next_page_token`.", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListSandboxEnvironmentsResponse" + } + } + }, + "description": "Paginated caller-owned sandbox environments." + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Invalid query parameters or page token." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Unauthenticated." + } + }, + "summary": "List sandbox environments", + "tags": [ + "Sandboxes" + ], + "x-fern-pagination": { + "cursor": "$request.page_token", + "next_cursor": "$response.pagination.next_page_token", + "results": "$response.data" + }, + "x-fern-sdk-group-name": [ + "sandbox_environments" + ], + "x-fern-sdk-method-name": "list" + }, + "post": { + "description": "Create a Daytona sandbox environment owned by the authenticated subject.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSandboxEnvironmentRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetSandboxEnvironmentResponse" + } + } + }, + "description": "Created sandbox environment." + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Invalid request body." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Unauthenticated." + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Name already exists in the tenant." + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Provider, snapshot, or secret validation failed." + } + }, + "summary": "Create a sandbox environment", + "tags": [ + "Sandboxes" + ], + "x-fern-sdk-group-name": [ + "sandbox_environments" + ], + "x-fern-sdk-method-name": "create" + } + }, + "/api/v1/sandbox-environments/{sandbox_environment_id}": { + "delete": { + "description": "Delete a caller-owned sandbox environment. Fails if any agent references it.", + "parameters": [ + { + "description": "Immutable sandbox environment identifier.", + "in": "path", + "name": "sandbox_environment_id", + "required": true, + "schema": { + "description": "Immutable sandbox environment identifier.", + "maxLength": 64, + "minLength": 1, + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteSandboxEnvironmentResponse" + } + } + }, + "description": "Deleted." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Unauthenticated." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Not found or you do not have access." + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Environment is referenced by one or more agents." + } + }, + "summary": "Delete a sandbox environment", + "tags": [ + "Sandboxes" + ], + "x-fern-sdk-group-name": [ + "sandbox_environments" + ], + "x-fern-sdk-method-name": "delete" + }, + "get": { + "description": "Get a sandbox environment owned by the authenticated subject.", + "parameters": [ + { + "description": "Immutable sandbox environment identifier.", + "in": "path", + "name": "sandbox_environment_id", + "required": true, + "schema": { + "description": "Immutable sandbox environment identifier.", + "maxLength": 64, + "minLength": 1, + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetSandboxEnvironmentResponse" + } + } + }, + "description": "The sandbox environment." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Unauthenticated." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Not found or you do not have access." + } + }, + "summary": "Get a sandbox environment", + "tags": [ + "Sandboxes" + ], + "x-fern-sdk-group-name": [ + "sandbox_environments" + ], + "x-fern-sdk-method-name": "get" + }, + "put": { + "description": "Replace the manifest (and optionally description). Name is immutable.", + "parameters": [ + { + "description": "Immutable sandbox environment identifier.", + "in": "path", + "name": "sandbox_environment_id", + "required": true, + "schema": { + "description": "Immutable sandbox environment identifier.", + "maxLength": 64, + "minLength": 1, + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateSandboxEnvironmentRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetSandboxEnvironmentResponse" + } + } + }, + "description": "Updated sandbox environment." + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Invalid request body." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Unauthenticated." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Not found or you do not have access." + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Provider, snapshot, or secret validation failed." + } + }, + "summary": "Update a sandbox environment", + "tags": [ + "Sandboxes" + ], + "x-fern-sdk-group-name": [ + "sandbox_environments" + ], + "x-fern-sdk-method-name": "update" + } + }, "/api/v1/schedules": { "get": { "description": "List schedules for the tenant, newest first.", diff --git a/docs/openapi.json b/docs/openapi.json index b40934b3f..502d645cf 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -1053,6 +1053,27 @@ ], "type": "object" }, + "CreateSandboxEnvironmentRequest": { + "additionalProperties": false, + "properties": { + "description": { + "description": "Optional human-readable description.", + "maxLength": 1024, + "type": "string" + }, + "manifest": { + "$ref": "#/components/schemas/SandboxEnvironmentManifest" + }, + "name": { + "$ref": "#/components/schemas/ResourceName" + } + }, + "required": [ + "name", + "manifest" + ], + "type": "object" + }, "CreateScheduleRequest": { "additionalProperties": false, "properties": { @@ -1226,6 +1247,147 @@ ], "type": "object" }, + "DaytonaDockerImage": { + "additionalProperties": false, + "properties": { + "ref": { + "description": "Container image reference passed to Daytona create-from-image.", + "minLength": 1, + "type": "string" + }, + "type": { + "description": "Build/create from a container image reference.", + "enum": [ + "docker" + ], + "type": "string" + } + }, + "required": [ + "type", + "ref" + ], + "type": "object" + }, + "DaytonaGpuType": { + "description": "Preferred Daytona GPU type.", + "enum": [ + "H100", + "H200", + "RTX-PRO-6000", + "RTX-4090", + "RTX-5090" + ], + "type": "string" + }, + "DaytonaSandboxEnvironmentImage": { + "discriminator": { + "mapping": { + "docker": "#/components/schemas/DaytonaDockerImage", + "snapshot": "#/components/schemas/DaytonaSnapshotImage", + "trueforge-default": "#/components/schemas/DaytonaTrueforgeDefaultImage" + }, + "propertyName": "type" + }, + "oneOf": [ + { + "$ref": "#/components/schemas/DaytonaTrueforgeDefaultImage" + }, + { + "$ref": "#/components/schemas/DaytonaSnapshotImage" + }, + { + "$ref": "#/components/schemas/DaytonaDockerImage" + } + ] + }, + "DaytonaSandboxEnvironmentLifecycle": { + "additionalProperties": false, + "properties": { + "auto_archive_interval_in_minutes": { + "description": "Minutes before Daytona auto-archives the sandbox (0 disables).", + "minimum": 0, + "type": "integer" + }, + "auto_delete_interval_in_minutes": { + "description": "Minutes before Daytona auto-deletes the sandbox (0 disables).", + "minimum": 0, + "type": "integer" + }, + "auto_stop_interval_in_minutes": { + "description": "Minutes of idle time before Daytona auto-stops the sandbox (0 disables).", + "minimum": 0, + "type": "integer" + } + }, + "type": "object" + }, + "DaytonaSandboxEnvironmentNetworking": { + "additionalProperties": false, + "properties": { + "domain_allow_list": { + "description": "Comma-separated allowed domains.", + "minLength": 1, + "type": "string" + }, + "network_allow_list": { + "description": "Comma-separated allowed CIDR network addresses.", + "minLength": 1, + "type": "string" + }, + "network_block_all": { + "description": "Block all outbound network access.", + "type": "boolean" + }, + "outbound_proxy_url": { + "description": "Outbound HTTP(S) proxy URL.", + "minLength": 1, + "type": "string" + } + }, + "type": "object" + }, + "DaytonaSandboxEnvironmentResources": { + "additionalProperties": false, + "properties": { + "cpu": { + "description": "CPU allocation in cores.", + "exclusiveMinimum": 0, + "type": "number" + }, + "disk": { + "description": "Disk allocation in GiB.", + "exclusiveMinimum": 0, + "type": "number" + }, + "gpu": { + "description": "GPU allocation in Daytona GPU units.", + "minimum": 0, + "type": "number" + }, + "gpu_type": { + "anyOf": [ + { + "$ref": "#/components/schemas/DaytonaGpuType" + }, + { + "items": { + "$ref": "#/components/schemas/DaytonaGpuType" + }, + "minItems": 1, + "type": "array" + } + ], + "description": "Preferred GPU type, or an ordered fallback list." + }, + "memory": { + "description": "Memory allocation in GiB.", + "exclusiveMinimum": 0, + "type": "number" + } + }, + "type": "object" + }, "DaytonaSandboxProviderAuth": { "additionalProperties": false, "description": "Daytona authentication credentials.", @@ -1241,10 +1403,52 @@ ], "type": "object" }, + "DaytonaSnapshotImage": { + "additionalProperties": false, + "properties": { + "name": { + "description": "Daytona snapshot name.", + "minLength": 1, + "type": "string" + }, + "type": { + "description": "Clone an existing Daytona snapshot by name.", + "enum": [ + "snapshot" + ], + "type": "string" + } + }, + "required": [ + "type", + "name" + ], + "type": "object" + }, + "DaytonaTrueforgeDefaultImage": { + "additionalProperties": false, + "properties": { + "type": { + "description": "Use the tenant provider TrueForge release snapshot.", + "enum": [ + "trueforge-default" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "type": "object" + }, "DeleteAgentResponse": { "properties": {}, "type": "object" }, + "DeleteSandboxEnvironmentResponse": { + "properties": {}, + "type": "object" + }, "DeleteScheduleResponse": { "properties": {}, "type": "object" @@ -1528,6 +1732,17 @@ ], "type": "object" }, + "GetSandboxEnvironmentResponse": { + "properties": { + "data": { + "$ref": "#/components/schemas/SandboxEnvironment" + } + }, + "required": [ + "data" + ], + "type": "object" + }, "GetSandboxProviderCatalogResponse": { "properties": { "data": { @@ -2277,7 +2492,7 @@ }, "type": "array" }, - "description": "For agent/schedule/session: keyed by resource id. For tenant: keyed by entity kind (e.g. `agent` → `CREATE`).", + "description": "For agent/schedule/session: keyed by resource id. For tenant: keyed by entity kind (e.g. `agent` or `sandbox-environment` → `CREATE`).", "type": "object" }, "type": { @@ -2322,6 +2537,24 @@ ], "type": "object" }, + "ListSandboxEnvironmentsResponse": { + "properties": { + "data": { + "items": { + "$ref": "#/components/schemas/SandboxEnvironment" + }, + "type": "array" + }, + "pagination": { + "$ref": "#/components/schemas/TokenPagination" + } + }, + "required": [ + "data", + "pagination" + ], + "type": "object" + }, "ListScheduleRunsResponse": { "properties": { "data": { @@ -3658,6 +3891,11 @@ "description": "Give the agent a sandbox. Required for skills and Code Mode.", "type": "boolean" }, + "environment": { + "description": "Caller-owned sandbox environment name. Omit to use the tenant provider defaults.", + "minLength": 1, + "type": "string" + }, "file_downloads": { "default": true, "description": "Allow downloading agent-produced files via the turn download endpoint. Default: true.", @@ -3707,6 +3945,97 @@ ], "type": "object" }, + "SandboxEnvironment": { + "additionalProperties": false, + "properties": { + "created_at": { + "description": "ISO-8601 create time.", + "format": "date-time", + "type": "string" + }, + "created_by_subject": { + "$ref": "#/components/schemas/CreatedBySubject" + }, + "description": { + "description": "Optional human-readable description.", + "type": [ + "string", + "null" + ] + }, + "id": { + "description": "Immutable server-generated environment identifier.", + "minLength": 1, + "type": "string" + }, + "manifest": { + "$ref": "#/components/schemas/SandboxEnvironmentManifest" + }, + "name": { + "$ref": "#/components/schemas/ResourceName" + }, + "updated_at": { + "description": "ISO-8601 last update time.", + "format": "date-time", + "type": "string" + } + }, + "required": [ + "id", + "name", + "description", + "manifest", + "created_by_subject", + "created_at", + "updated_at" + ], + "type": "object" + }, + "SandboxEnvironmentManifest": { + "additionalProperties": false, + "properties": { + "image": { + "$ref": "#/components/schemas/DaytonaSandboxEnvironmentImage" + }, + "lifecycle": { + "$ref": "#/components/schemas/DaytonaSandboxEnvironmentLifecycle" + }, + "networking": { + "$ref": "#/components/schemas/DaytonaSandboxEnvironmentNetworking" + }, + "provider": { + "description": "Must match the configured sandbox provider name/type.", + "enum": [ + "daytona" + ], + "type": "string" + }, + "resources": { + "$ref": "#/components/schemas/DaytonaSandboxEnvironmentResources" + }, + "secrets": { + "additionalProperties": { + "minLength": 1, + "type": "string" + }, + "description": "Map of sandbox env var name to an existing Daytona organization secret name.", + "type": "object" + }, + "type": { + "description": "Daytona sandbox environment.", + "enum": [ + "daytona" + ], + "type": "string" + } + }, + "required": [ + "type", + "provider", + "image" + ], + "type": "object" + }, "SandboxProviderManifest": { "additionalProperties": false, "properties": { @@ -5581,15 +5910,35 @@ ], "type": "object" }, - "UpdateSandboxProviderRequest": { + "UpdateSandboxEnvironmentRequest": { "additionalProperties": false, "properties": { - "manifest": { - "$ref": "#/components/schemas/SandboxProviderManifest" - } - }, - "required": [ - "manifest" + "description": { + "description": "Optional human-readable description.", + "maxLength": 1024, + "type": [ + "string", + "null" + ] + }, + "manifest": { + "$ref": "#/components/schemas/SandboxEnvironmentManifest" + } + }, + "required": [ + "manifest" + ], + "type": "object" + }, + "UpdateSandboxProviderRequest": { + "additionalProperties": false, + "properties": { + "manifest": { + "$ref": "#/components/schemas/SandboxProviderManifest" + } + }, + "required": [ + "manifest" ], "type": "object" }, @@ -7537,6 +7886,367 @@ "x-fern-sdk-method-name": "list" } }, + "/api/v1/sandbox-environments": { + "get": { + "description": "List sandbox environments created by the authenticated subject, newest first.", + "parameters": [ + { + "description": "Page size. Defaults to 25", + "in": "query", + "name": "limit", + "required": false, + "schema": { + "default": 25, + "description": "Page size. Defaults to 25", + "maximum": 25, + "minimum": 1, + "type": "integer" + } + }, + { + "description": "Opaque token from a previous response `next_page_token`.", + "in": "query", + "name": "page_token", + "required": false, + "schema": { + "description": "Opaque token from a previous response `next_page_token`.", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListSandboxEnvironmentsResponse" + } + } + }, + "description": "Paginated caller-owned sandbox environments." + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Invalid query parameters or page token." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Unauthenticated." + } + }, + "summary": "List sandbox environments", + "tags": [ + "Sandboxes" + ], + "x-fern-pagination": { + "cursor": "$request.page_token", + "next_cursor": "$response.pagination.next_page_token", + "results": "$response.data" + }, + "x-fern-sdk-group-name": [ + "sandbox_environments" + ], + "x-fern-sdk-method-name": "list" + }, + "post": { + "description": "Create a Daytona sandbox environment owned by the authenticated subject.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSandboxEnvironmentRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetSandboxEnvironmentResponse" + } + } + }, + "description": "Created sandbox environment." + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Invalid request body." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Unauthenticated." + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Name already exists in the tenant." + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Provider, snapshot, or secret validation failed." + } + }, + "summary": "Create a sandbox environment", + "tags": [ + "Sandboxes" + ], + "x-fern-sdk-group-name": [ + "sandbox_environments" + ], + "x-fern-sdk-method-name": "create" + } + }, + "/api/v1/sandbox-environments/{sandbox_environment_id}": { + "delete": { + "description": "Delete a caller-owned sandbox environment. Fails if any agent references it.", + "parameters": [ + { + "description": "Immutable sandbox environment identifier.", + "in": "path", + "name": "sandbox_environment_id", + "required": true, + "schema": { + "description": "Immutable sandbox environment identifier.", + "maxLength": 64, + "minLength": 1, + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteSandboxEnvironmentResponse" + } + } + }, + "description": "Deleted." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Unauthenticated." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Not found or you do not have access." + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Environment is referenced by one or more agents." + } + }, + "summary": "Delete a sandbox environment", + "tags": [ + "Sandboxes" + ], + "x-fern-sdk-group-name": [ + "sandbox_environments" + ], + "x-fern-sdk-method-name": "delete" + }, + "get": { + "description": "Get a sandbox environment owned by the authenticated subject.", + "parameters": [ + { + "description": "Immutable sandbox environment identifier.", + "in": "path", + "name": "sandbox_environment_id", + "required": true, + "schema": { + "description": "Immutable sandbox environment identifier.", + "maxLength": 64, + "minLength": 1, + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetSandboxEnvironmentResponse" + } + } + }, + "description": "The sandbox environment." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Unauthenticated." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Not found or you do not have access." + } + }, + "summary": "Get a sandbox environment", + "tags": [ + "Sandboxes" + ], + "x-fern-sdk-group-name": [ + "sandbox_environments" + ], + "x-fern-sdk-method-name": "get" + }, + "put": { + "description": "Replace the manifest (and optionally description). Name is immutable.", + "parameters": [ + { + "description": "Immutable sandbox environment identifier.", + "in": "path", + "name": "sandbox_environment_id", + "required": true, + "schema": { + "description": "Immutable sandbox environment identifier.", + "maxLength": 64, + "minLength": 1, + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateSandboxEnvironmentRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetSandboxEnvironmentResponse" + } + } + }, + "description": "Updated sandbox environment." + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Invalid request body." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Unauthenticated." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Not found or you do not have access." + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Provider, snapshot, or secret validation failed." + } + }, + "summary": "Update a sandbox environment", + "tags": [ + "Sandboxes" + ], + "x-fern-sdk-group-name": [ + "sandbox_environments" + ], + "x-fern-sdk-method-name": "update" + } + }, "/api/v1/schedules": { "get": { "description": "List schedules for the tenant, newest first.", diff --git a/packages/trueforge-core/src/agent-session/schemas/agentSpec.ts b/packages/trueforge-core/src/agent-session/schemas/agentSpec.ts index c5134870b..17a501ab1 100644 --- a/packages/trueforge-core/src/agent-session/schemas/agentSpec.ts +++ b/packages/trueforge-core/src/agent-session/schemas/agentSpec.ts @@ -166,6 +166,11 @@ const SandboxConfigSchema = z .boolean() .default(true) .describe('Allow downloading agent-produced files via the turn download endpoint. Default: true.'), + environment: z + .string() + .min(1) + .optional() + .describe('Caller-owned sandbox environment name. Omit to use the tenant provider defaults.'), }) .openapi('SandboxConfig'); diff --git a/packages/trueforge-sdk/package.json b/packages/trueforge-sdk/package.json index 8287ffb70..a8ea1ce6e 100644 --- a/packages/trueforge-sdk/package.json +++ b/packages/trueforge-sdk/package.json @@ -99,6 +99,17 @@ }, "default": "./dist/cjs/api/resources/models/exports.js" }, + "./sandboxEnvironments": { + "import": { + "types": "./dist/esm/api/resources/sandboxEnvironments/exports.d.mts", + "default": "./dist/esm/api/resources/sandboxEnvironments/exports.mjs" + }, + "require": { + "types": "./dist/cjs/api/resources/sandboxEnvironments/exports.d.ts", + "default": "./dist/cjs/api/resources/sandboxEnvironments/exports.js" + }, + "default": "./dist/cjs/api/resources/sandboxEnvironments/exports.js" + }, "./schedules": { "import": { "types": "./dist/esm/api/resources/schedules/exports.d.mts", diff --git a/packages/trueforge-sdk/reference.md b/packages/trueforge-sdk/reference.md index 0936210ae..23868bf53 100644 --- a/packages/trueforge-sdk/reference.md +++ b/packages/trueforge-sdk/reference.md @@ -896,6 +896,361 @@ await client.models.list(); + + + + +## SandboxEnvironments +
client.sandboxEnvironments.list({ ...params }) -> core.Page<TrueForge.SandboxEnvironment, TrueForge.ListSandboxEnvironmentsResponse> +
+
+ +#### 📝 Description + +
+
+ +
+
+ +List sandbox environments created by the authenticated subject, newest first. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```typescript +const pageableResponse = await client.sandboxEnvironments.list(); +for await (const item of pageableResponse) { + console.log(item); +} + +// Or you can manually iterate page-by-page +let page = await client.sandboxEnvironments.list(); +while (page.hasNextPage()) { + page = page.getNextPage(); +} + +// You can also access the underlying response +const response = page.response; + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `TrueForge.ListSandboxEnvironmentsRequest` + +
+
+ +
+
+ +**requestOptions:** `SandboxEnvironmentsClient.RequestOptions` + +
+
+
+
+ + +
+
+
+ +
client.sandboxEnvironments.create({ ...params }) -> TrueForge.GetSandboxEnvironmentResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Create a Daytona sandbox environment owned by the authenticated subject. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```typescript +await client.sandboxEnvironments.create({ + manifest: { + image: { + ref: "ref", + type: "docker" + }, + provider: "daytona", + type: "daytona" + }, + name: "name" +}); + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `TrueForge.CreateSandboxEnvironmentRequest` + +
+
+ +
+
+ +**requestOptions:** `SandboxEnvironmentsClient.RequestOptions` + +
+
+
+
+ + +
+
+
+ +
client.sandboxEnvironments.get(sandbox_environment_id) -> TrueForge.GetSandboxEnvironmentResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Get a sandbox environment owned by the authenticated subject. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```typescript +await client.sandboxEnvironments.get("sandbox_environment_id"); + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**sandbox_environment_id:** `string` — Immutable sandbox environment identifier. + +
+
+ +
+
+ +**requestOptions:** `SandboxEnvironmentsClient.RequestOptions` + +
+
+
+
+ + +
+
+
+ +
client.sandboxEnvironments.update(sandbox_environment_id, { ...params }) -> TrueForge.GetSandboxEnvironmentResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Replace the manifest (and optionally description). Name is immutable. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```typescript +await client.sandboxEnvironments.update("sandbox_environment_id", { + manifest: { + image: { + ref: "ref", + type: "docker" + }, + provider: "daytona", + type: "daytona" + } +}); + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**sandbox_environment_id:** `string` — Immutable sandbox environment identifier. + +
+
+ +
+
+ +**request:** `TrueForge.UpdateSandboxEnvironmentRequest` + +
+
+ +
+
+ +**requestOptions:** `SandboxEnvironmentsClient.RequestOptions` + +
+
+
+
+ + +
+
+
+ +
client.sandboxEnvironments.delete(sandbox_environment_id) -> TrueForge.DeleteSandboxEnvironmentResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Delete a caller-owned sandbox environment. Fails if any agent references it. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```typescript +await client.sandboxEnvironments.delete("sandbox_environment_id"); + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**sandbox_environment_id:** `string` — Immutable sandbox environment identifier. + +
+
+ +
+
+ +**requestOptions:** `SandboxEnvironmentsClient.RequestOptions` + +
+
+
+
+ +
diff --git a/packages/trueforge-sdk/src/Client.ts b/packages/trueforge-sdk/src/Client.ts index d8dab2878..757773d88 100644 --- a/packages/trueforge-sdk/src/Client.ts +++ b/packages/trueforge-sdk/src/Client.ts @@ -6,6 +6,7 @@ import { CatalogsClient } from "./api/resources/catalogs/client/Client.js"; import { InternalClient } from "./api/resources/internal/client/Client.js"; import { McpServersClient } from "./api/resources/mcpServers/client/Client.js"; import { ModelsClient } from "./api/resources/models/client/Client.js"; +import { SandboxEnvironmentsClient } from "./api/resources/sandboxEnvironments/client/Client.js"; import { SchedulesClient } from "./api/resources/schedules/client/Client.js"; import { ServerClient } from "./api/resources/server/client/Client.js"; import { SessionsClient } from "./api/resources/sessions/client/Client.js"; @@ -29,6 +30,7 @@ export class TrueForge { protected _server: ServerClient | undefined; protected _mcpServers: McpServersClient | undefined; protected _models: ModelsClient | undefined; + protected _sandboxEnvironments: SandboxEnvironmentsClient | undefined; protected _schedules: SchedulesClient | undefined; protected _sessions: SessionsClient | undefined; protected _skills: SkillsClient | undefined; @@ -63,6 +65,10 @@ export class TrueForge { return (this._models ??= new ModelsClient(this._options)); } + public get sandboxEnvironments(): SandboxEnvironmentsClient { + return (this._sandboxEnvironments ??= new SandboxEnvironmentsClient(this._options)); + } + public get schedules(): SchedulesClient { return (this._schedules ??= new SchedulesClient(this._options)); } diff --git a/packages/trueforge-sdk/src/api/resources/index.ts b/packages/trueforge-sdk/src/api/resources/index.ts index 000fb4979..bef7cbe19 100644 --- a/packages/trueforge-sdk/src/api/resources/index.ts +++ b/packages/trueforge-sdk/src/api/resources/index.ts @@ -7,6 +7,8 @@ export * as internal from "./internal/index.js"; export * from "./mcpServers/client/requests/index.js"; export * as mcpServers from "./mcpServers/index.js"; export * as models from "./models/index.js"; +export * from "./sandboxEnvironments/client/requests/index.js"; +export * as sandboxEnvironments from "./sandboxEnvironments/index.js"; export * from "./schedules/client/requests/index.js"; export * as schedules from "./schedules/index.js"; export * as server from "./server/index.js"; diff --git a/packages/trueforge-sdk/src/api/resources/sandboxEnvironments/client/Client.ts b/packages/trueforge-sdk/src/api/resources/sandboxEnvironments/client/Client.ts new file mode 100644 index 000000000..53d01c45b --- /dev/null +++ b/packages/trueforge-sdk/src/api/resources/sandboxEnvironments/client/Client.ts @@ -0,0 +1,642 @@ +// This file was auto-generated by Fern from our API Definition. + +import type { BaseClientOptions, BaseRequestOptions } from "../../../../BaseClient.js"; +import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "../../../../BaseClient.js"; +import { mergeHeaders } from "../../../../core/headers.js"; +import * as core from "../../../../core/index.js"; +import { mergeAdditionalBodyParameters } from "../../../../core/requestBody.js"; +import { handleNonStatusCodeError } from "../../../../errors/handleNonStatusCodeError.js"; +import * as errors from "../../../../errors/index.js"; +import * as serializers from "../../../../serialization/index.js"; +import * as TrueForge from "../../../index.js"; + +export declare namespace SandboxEnvironmentsClient { + export type Options = BaseClientOptions; + + export interface RequestOptions extends BaseRequestOptions {} +} + +export class SandboxEnvironmentsClient { + protected readonly _options: NormalizedClientOptionsWithAuth; + + constructor(options: SandboxEnvironmentsClient.Options) { + this._options = normalizeClientOptionsWithAuth(options); + } + + /** + * List sandbox environments created by the authenticated subject, newest first. + * + * @param {TrueForge.ListSandboxEnvironmentsRequest} request + * @param {SandboxEnvironmentsClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link TrueForge.BadRequestError} + * @throws {@link TrueForge.UnauthorizedError} + * @throws {@link errors.TrueForgeError} + * @throws {@link errors.TrueForgeTimeoutError} + * + * @example + * await client.sandboxEnvironments.list() + */ + public async list( + request: TrueForge.ListSandboxEnvironmentsRequest = {}, + requestOptions?: SandboxEnvironmentsClient.RequestOptions, + ): Promise> { + const list = core.HttpResponsePromise.interceptFunction( + async ( + request: TrueForge.ListSandboxEnvironmentsRequest, + ): Promise> => { + const { limit = 25, pageToken } = request; + const _queryParams: Record = { + limit, + page_token: pageToken, + }; + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); + const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, + this._options?.headers, + requestOptions?.headers, + ); + const _response = await (this._options.fetcher ?? core.fetcher)({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)), + "api/v1/sandbox-environments", + ), + method: "GET", + headers: _headers, + queryString: core.url + .queryBuilder() + .addMany(_queryParams) + .mergeAdditional(requestOptions?.queryParams) + .build(), + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { + data: serializers.ListSandboxEnvironmentsResponse.parseOrThrow(_response.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + skipValidation: true, + breadcrumbsPrefix: ["response"], + }), + rawResponse: _response.rawResponse, + }; + } + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 400: + throw new TrueForge.BadRequestError( + serializers.RequestErrorResponse.parseOrThrow(_response.error.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + skipValidation: true, + breadcrumbsPrefix: ["response"], + }), + _response.rawResponse, + ); + case 401: + throw new TrueForge.UnauthorizedError( + serializers.RequestErrorResponse.parseOrThrow(_response.error.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + skipValidation: true, + breadcrumbsPrefix: ["response"], + }), + _response.rawResponse, + ); + default: + throw new errors.TrueForgeError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/api/v1/sandbox-environments", + ); + }, + ); + const dataWithRawResponse = await list(request).withRawResponse(); + return new core.Page({ + response: dataWithRawResponse.data, + rawResponse: dataWithRawResponse.rawResponse, + hasNextPage: (response) => + response?.pagination.nextPageToken != null && + !(typeof response?.pagination.nextPageToken === "string" && response?.pagination.nextPageToken === ""), + getItems: (response) => response?.data ?? [], + loadPage: (response) => { + return list(core.setObjectProperty(request, "pageToken", response?.pagination.nextPageToken)); + }, + }); + } + + /** + * Create a Daytona sandbox environment owned by the authenticated subject. + * + * @param {TrueForge.CreateSandboxEnvironmentRequest} request + * @param {SandboxEnvironmentsClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link TrueForge.BadRequestError} + * @throws {@link TrueForge.UnauthorizedError} + * @throws {@link TrueForge.ConflictError} + * @throws {@link TrueForge.UnprocessableEntityError} + * @throws {@link errors.TrueForgeError} + * @throws {@link errors.TrueForgeTimeoutError} + * + * @example + * await client.sandboxEnvironments.create({ + * manifest: { + * image: { + * ref: "ref", + * type: "docker" + * }, + * provider: "daytona", + * type: "daytona" + * }, + * name: "name" + * }) + */ + public create( + request: TrueForge.CreateSandboxEnvironmentRequest, + requestOptions?: SandboxEnvironmentsClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__create(request, requestOptions)); + } + + private async __create( + request: TrueForge.CreateSandboxEnvironmentRequest, + requestOptions?: SandboxEnvironmentsClient.RequestOptions, + ): Promise> { + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); + const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, + this._options?.headers, + requestOptions?.headers, + ); + const _response = await (this._options.fetcher ?? core.fetcher)({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)), + "api/v1/sandbox-environments", + ), + method: "POST", + headers: _headers, + contentType: "application/json", + queryString: core.url.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(), + requestType: "json", + body: mergeAdditionalBodyParameters( + serializers.CreateSandboxEnvironmentRequest.jsonOrThrow(request, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + omitUndefined: true, + }), + requestOptions?.additionalBodyParameters, + ), + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { + data: serializers.GetSandboxEnvironmentResponse.parseOrThrow(_response.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + skipValidation: true, + breadcrumbsPrefix: ["response"], + }), + rawResponse: _response.rawResponse, + }; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 400: + throw new TrueForge.BadRequestError( + serializers.RequestErrorResponse.parseOrThrow(_response.error.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + skipValidation: true, + breadcrumbsPrefix: ["response"], + }), + _response.rawResponse, + ); + case 401: + throw new TrueForge.UnauthorizedError( + serializers.RequestErrorResponse.parseOrThrow(_response.error.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + skipValidation: true, + breadcrumbsPrefix: ["response"], + }), + _response.rawResponse, + ); + case 409: + throw new TrueForge.ConflictError( + serializers.RequestErrorResponse.parseOrThrow(_response.error.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + skipValidation: true, + breadcrumbsPrefix: ["response"], + }), + _response.rawResponse, + ); + case 422: + throw new TrueForge.UnprocessableEntityError( + serializers.RequestErrorResponse.parseOrThrow(_response.error.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + skipValidation: true, + breadcrumbsPrefix: ["response"], + }), + _response.rawResponse, + ); + default: + throw new errors.TrueForgeError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + + return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/api/v1/sandbox-environments"); + } + + /** + * Get a sandbox environment owned by the authenticated subject. + * + * @param {string} sandbox_environment_id - Immutable sandbox environment identifier. + * @param {SandboxEnvironmentsClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link TrueForge.UnauthorizedError} + * @throws {@link TrueForge.NotFoundError} + * @throws {@link errors.TrueForgeError} + * @throws {@link errors.TrueForgeTimeoutError} + * + * @example + * await client.sandboxEnvironments.get("sandbox_environment_id") + */ + public get( + sandbox_environment_id: string, + requestOptions?: SandboxEnvironmentsClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__get(sandbox_environment_id, requestOptions)); + } + + private async __get( + sandbox_environment_id: string, + requestOptions?: SandboxEnvironmentsClient.RequestOptions, + ): Promise> { + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); + const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, + this._options?.headers, + requestOptions?.headers, + ); + const _response = await (this._options.fetcher ?? core.fetcher)({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)), + `api/v1/sandbox-environments/${core.url.encodePathParam(sandbox_environment_id)}`, + ), + method: "GET", + headers: _headers, + queryString: core.url.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(), + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { + data: serializers.GetSandboxEnvironmentResponse.parseOrThrow(_response.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + skipValidation: true, + breadcrumbsPrefix: ["response"], + }), + rawResponse: _response.rawResponse, + }; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 401: + throw new TrueForge.UnauthorizedError( + serializers.RequestErrorResponse.parseOrThrow(_response.error.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + skipValidation: true, + breadcrumbsPrefix: ["response"], + }), + _response.rawResponse, + ); + case 404: + throw new TrueForge.NotFoundError( + serializers.RequestErrorResponse.parseOrThrow(_response.error.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + skipValidation: true, + breadcrumbsPrefix: ["response"], + }), + _response.rawResponse, + ); + default: + throw new errors.TrueForgeError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "GET", + "/api/v1/sandbox-environments/{sandbox_environment_id}", + ); + } + + /** + * Replace the manifest (and optionally description). Name is immutable. + * + * @param {string} sandbox_environment_id - Immutable sandbox environment identifier. + * @param {TrueForge.UpdateSandboxEnvironmentRequest} request + * @param {SandboxEnvironmentsClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link TrueForge.BadRequestError} + * @throws {@link TrueForge.UnauthorizedError} + * @throws {@link TrueForge.NotFoundError} + * @throws {@link TrueForge.UnprocessableEntityError} + * @throws {@link errors.TrueForgeError} + * @throws {@link errors.TrueForgeTimeoutError} + * + * @example + * await client.sandboxEnvironments.update("sandbox_environment_id", { + * manifest: { + * image: { + * ref: "ref", + * type: "docker" + * }, + * provider: "daytona", + * type: "daytona" + * } + * }) + */ + public update( + sandbox_environment_id: string, + request: TrueForge.UpdateSandboxEnvironmentRequest, + requestOptions?: SandboxEnvironmentsClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__update(sandbox_environment_id, request, requestOptions)); + } + + private async __update( + sandbox_environment_id: string, + request: TrueForge.UpdateSandboxEnvironmentRequest, + requestOptions?: SandboxEnvironmentsClient.RequestOptions, + ): Promise> { + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); + const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, + this._options?.headers, + requestOptions?.headers, + ); + const _response = await (this._options.fetcher ?? core.fetcher)({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)), + `api/v1/sandbox-environments/${core.url.encodePathParam(sandbox_environment_id)}`, + ), + method: "PUT", + headers: _headers, + contentType: "application/json", + queryString: core.url.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(), + requestType: "json", + body: mergeAdditionalBodyParameters( + serializers.UpdateSandboxEnvironmentRequest.jsonOrThrow(request, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + omitUndefined: true, + }), + requestOptions?.additionalBodyParameters, + ), + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { + data: serializers.GetSandboxEnvironmentResponse.parseOrThrow(_response.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + skipValidation: true, + breadcrumbsPrefix: ["response"], + }), + rawResponse: _response.rawResponse, + }; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 400: + throw new TrueForge.BadRequestError( + serializers.RequestErrorResponse.parseOrThrow(_response.error.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + skipValidation: true, + breadcrumbsPrefix: ["response"], + }), + _response.rawResponse, + ); + case 401: + throw new TrueForge.UnauthorizedError( + serializers.RequestErrorResponse.parseOrThrow(_response.error.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + skipValidation: true, + breadcrumbsPrefix: ["response"], + }), + _response.rawResponse, + ); + case 404: + throw new TrueForge.NotFoundError( + serializers.RequestErrorResponse.parseOrThrow(_response.error.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + skipValidation: true, + breadcrumbsPrefix: ["response"], + }), + _response.rawResponse, + ); + case 422: + throw new TrueForge.UnprocessableEntityError( + serializers.RequestErrorResponse.parseOrThrow(_response.error.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + skipValidation: true, + breadcrumbsPrefix: ["response"], + }), + _response.rawResponse, + ); + default: + throw new errors.TrueForgeError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "PUT", + "/api/v1/sandbox-environments/{sandbox_environment_id}", + ); + } + + /** + * Delete a caller-owned sandbox environment. Fails if any agent references it. + * + * @param {string} sandbox_environment_id - Immutable sandbox environment identifier. + * @param {SandboxEnvironmentsClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link TrueForge.UnauthorizedError} + * @throws {@link TrueForge.NotFoundError} + * @throws {@link TrueForge.ConflictError} + * @throws {@link errors.TrueForgeError} + * @throws {@link errors.TrueForgeTimeoutError} + * + * @example + * await client.sandboxEnvironments.delete("sandbox_environment_id") + */ + public delete( + sandbox_environment_id: string, + requestOptions?: SandboxEnvironmentsClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise(this.__delete(sandbox_environment_id, requestOptions)); + } + + private async __delete( + sandbox_environment_id: string, + requestOptions?: SandboxEnvironmentsClient.RequestOptions, + ): Promise> { + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); + const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, + this._options?.headers, + requestOptions?.headers, + ); + const _response = await (this._options.fetcher ?? core.fetcher)({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)), + `api/v1/sandbox-environments/${core.url.encodePathParam(sandbox_environment_id)}`, + ), + method: "DELETE", + headers: _headers, + queryString: core.url.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(), + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { + data: serializers.DeleteSandboxEnvironmentResponse.parseOrThrow(_response.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + skipValidation: true, + breadcrumbsPrefix: ["response"], + }), + rawResponse: _response.rawResponse, + }; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 401: + throw new TrueForge.UnauthorizedError( + serializers.RequestErrorResponse.parseOrThrow(_response.error.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + skipValidation: true, + breadcrumbsPrefix: ["response"], + }), + _response.rawResponse, + ); + case 404: + throw new TrueForge.NotFoundError( + serializers.RequestErrorResponse.parseOrThrow(_response.error.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + skipValidation: true, + breadcrumbsPrefix: ["response"], + }), + _response.rawResponse, + ); + case 409: + throw new TrueForge.ConflictError( + serializers.RequestErrorResponse.parseOrThrow(_response.error.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + skipValidation: true, + breadcrumbsPrefix: ["response"], + }), + _response.rawResponse, + ); + default: + throw new errors.TrueForgeError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "DELETE", + "/api/v1/sandbox-environments/{sandbox_environment_id}", + ); + } +} diff --git a/packages/trueforge-sdk/src/api/resources/sandboxEnvironments/client/index.ts b/packages/trueforge-sdk/src/api/resources/sandboxEnvironments/client/index.ts new file mode 100644 index 000000000..195f9aa8a --- /dev/null +++ b/packages/trueforge-sdk/src/api/resources/sandboxEnvironments/client/index.ts @@ -0,0 +1 @@ +export * from "./requests/index.js"; diff --git a/packages/trueforge-sdk/src/api/resources/sandboxEnvironments/client/requests/CreateSandboxEnvironmentRequest.ts b/packages/trueforge-sdk/src/api/resources/sandboxEnvironments/client/requests/CreateSandboxEnvironmentRequest.ts new file mode 100644 index 000000000..af9348dd0 --- /dev/null +++ b/packages/trueforge-sdk/src/api/resources/sandboxEnvironments/client/requests/CreateSandboxEnvironmentRequest.ts @@ -0,0 +1,24 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../../../index.js"; + +/** + * @example + * { + * manifest: { + * image: { + * ref: "ref", + * type: "docker" + * }, + * provider: "daytona", + * type: "daytona" + * }, + * name: "name" + * } + */ +export interface CreateSandboxEnvironmentRequest { + /** Optional human-readable description. */ + description?: string; + manifest: TrueForge.SandboxEnvironmentManifest; + name: TrueForge.ResourceName; +} diff --git a/packages/trueforge-sdk/src/api/resources/sandboxEnvironments/client/requests/ListSandboxEnvironmentsRequest.ts b/packages/trueforge-sdk/src/api/resources/sandboxEnvironments/client/requests/ListSandboxEnvironmentsRequest.ts new file mode 100644 index 000000000..2efd1793b --- /dev/null +++ b/packages/trueforge-sdk/src/api/resources/sandboxEnvironments/client/requests/ListSandboxEnvironmentsRequest.ts @@ -0,0 +1,12 @@ +// This file was auto-generated by Fern from our API Definition. + +/** + * @example + * {} + */ +export interface ListSandboxEnvironmentsRequest { + /** Page size. Defaults to 25 */ + limit?: number; + /** Opaque token from a previous response `next_page_token`. */ + pageToken?: string; +} diff --git a/packages/trueforge-sdk/src/api/resources/sandboxEnvironments/client/requests/UpdateSandboxEnvironmentRequest.ts b/packages/trueforge-sdk/src/api/resources/sandboxEnvironments/client/requests/UpdateSandboxEnvironmentRequest.ts new file mode 100644 index 000000000..e44ba8223 --- /dev/null +++ b/packages/trueforge-sdk/src/api/resources/sandboxEnvironments/client/requests/UpdateSandboxEnvironmentRequest.ts @@ -0,0 +1,22 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../../../index.js"; + +/** + * @example + * { + * manifest: { + * image: { + * ref: "ref", + * type: "docker" + * }, + * provider: "daytona", + * type: "daytona" + * } + * } + */ +export interface UpdateSandboxEnvironmentRequest { + /** Optional human-readable description. */ + description?: string | null; + manifest: TrueForge.SandboxEnvironmentManifest; +} diff --git a/packages/trueforge-sdk/src/api/resources/sandboxEnvironments/client/requests/index.ts b/packages/trueforge-sdk/src/api/resources/sandboxEnvironments/client/requests/index.ts new file mode 100644 index 000000000..f20f36488 --- /dev/null +++ b/packages/trueforge-sdk/src/api/resources/sandboxEnvironments/client/requests/index.ts @@ -0,0 +1,3 @@ +export type { CreateSandboxEnvironmentRequest } from "./CreateSandboxEnvironmentRequest.js"; +export type { ListSandboxEnvironmentsRequest } from "./ListSandboxEnvironmentsRequest.js"; +export type { UpdateSandboxEnvironmentRequest } from "./UpdateSandboxEnvironmentRequest.js"; diff --git a/packages/trueforge-sdk/src/api/resources/sandboxEnvironments/exports.ts b/packages/trueforge-sdk/src/api/resources/sandboxEnvironments/exports.ts new file mode 100644 index 000000000..fc58f1b3e --- /dev/null +++ b/packages/trueforge-sdk/src/api/resources/sandboxEnvironments/exports.ts @@ -0,0 +1,4 @@ +// This file was auto-generated by Fern from our API Definition. + +export { SandboxEnvironmentsClient } from "./client/Client.js"; +export * from "./client/index.js"; diff --git a/packages/trueforge-sdk/src/api/resources/sandboxEnvironments/index.ts b/packages/trueforge-sdk/src/api/resources/sandboxEnvironments/index.ts new file mode 100644 index 000000000..914b8c3c7 --- /dev/null +++ b/packages/trueforge-sdk/src/api/resources/sandboxEnvironments/index.ts @@ -0,0 +1 @@ +export * from "./client/index.js"; diff --git a/packages/trueforge-sdk/src/api/types/DaytonaDockerImage.ts b/packages/trueforge-sdk/src/api/types/DaytonaDockerImage.ts new file mode 100644 index 000000000..aeba33f8c --- /dev/null +++ b/packages/trueforge-sdk/src/api/types/DaytonaDockerImage.ts @@ -0,0 +1,7 @@ +// This file was auto-generated by Fern from our API Definition. + +export interface DaytonaDockerImage { + /** Container image reference passed to Daytona create-from-image. */ + ref: string; + type: "docker"; +} diff --git a/packages/trueforge-sdk/src/api/types/DaytonaGpuType.ts b/packages/trueforge-sdk/src/api/types/DaytonaGpuType.ts new file mode 100644 index 000000000..e9de0407d --- /dev/null +++ b/packages/trueforge-sdk/src/api/types/DaytonaGpuType.ts @@ -0,0 +1,11 @@ +// This file was auto-generated by Fern from our API Definition. + +/** Preferred Daytona GPU type. */ +export const DaytonaGpuType = { + H100: "H100", + H200: "H200", + RtxPro6000: "RTX-PRO-6000", + Rtx4090: "RTX-4090", + Rtx5090: "RTX-5090", +} as const; +export type DaytonaGpuType = (typeof DaytonaGpuType)[keyof typeof DaytonaGpuType]; diff --git a/packages/trueforge-sdk/src/api/types/DaytonaSandboxEnvironmentImage.ts b/packages/trueforge-sdk/src/api/types/DaytonaSandboxEnvironmentImage.ts new file mode 100644 index 000000000..765912cf9 --- /dev/null +++ b/packages/trueforge-sdk/src/api/types/DaytonaSandboxEnvironmentImage.ts @@ -0,0 +1,8 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../index.js"; + +export type DaytonaSandboxEnvironmentImage = + | TrueForge.DaytonaDockerImage + | TrueForge.DaytonaSnapshotImage + | TrueForge.DaytonaTrueforgeDefaultImage; diff --git a/packages/trueforge-sdk/src/api/types/DaytonaSandboxEnvironmentLifecycle.ts b/packages/trueforge-sdk/src/api/types/DaytonaSandboxEnvironmentLifecycle.ts new file mode 100644 index 000000000..f1443f40f --- /dev/null +++ b/packages/trueforge-sdk/src/api/types/DaytonaSandboxEnvironmentLifecycle.ts @@ -0,0 +1,10 @@ +// This file was auto-generated by Fern from our API Definition. + +export interface DaytonaSandboxEnvironmentLifecycle { + /** Minutes before Daytona auto-archives the sandbox (0 disables). */ + autoArchiveIntervalInMinutes?: number; + /** Minutes before Daytona auto-deletes the sandbox (0 disables). */ + autoDeleteIntervalInMinutes?: number; + /** Minutes of idle time before Daytona auto-stops the sandbox (0 disables). */ + autoStopIntervalInMinutes?: number; +} diff --git a/packages/trueforge-sdk/src/api/types/DaytonaSandboxEnvironmentNetworking.ts b/packages/trueforge-sdk/src/api/types/DaytonaSandboxEnvironmentNetworking.ts new file mode 100644 index 000000000..69d8e9253 --- /dev/null +++ b/packages/trueforge-sdk/src/api/types/DaytonaSandboxEnvironmentNetworking.ts @@ -0,0 +1,12 @@ +// This file was auto-generated by Fern from our API Definition. + +export interface DaytonaSandboxEnvironmentNetworking { + /** Comma-separated allowed domains. */ + domainAllowList?: string; + /** Comma-separated allowed CIDR network addresses. */ + networkAllowList?: string; + /** Block all outbound network access. */ + networkBlockAll?: boolean; + /** Outbound HTTP(S) proxy URL. */ + outboundProxyUrl?: string; +} diff --git a/packages/trueforge-sdk/src/api/types/DaytonaSandboxEnvironmentResources.ts b/packages/trueforge-sdk/src/api/types/DaytonaSandboxEnvironmentResources.ts new file mode 100644 index 000000000..168bc67a9 --- /dev/null +++ b/packages/trueforge-sdk/src/api/types/DaytonaSandboxEnvironmentResources.ts @@ -0,0 +1,16 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../index.js"; + +export interface DaytonaSandboxEnvironmentResources { + /** CPU allocation in cores. */ + cpu?: number; + /** Disk allocation in GiB. */ + disk?: number; + /** GPU allocation in Daytona GPU units. */ + gpu?: number; + /** Preferred GPU type, or an ordered fallback list. */ + gpuType?: TrueForge.DaytonaSandboxEnvironmentResourcesGpuType; + /** Memory allocation in GiB. */ + memory?: number; +} diff --git a/packages/trueforge-sdk/src/api/types/DaytonaSandboxEnvironmentResourcesGpuType.ts b/packages/trueforge-sdk/src/api/types/DaytonaSandboxEnvironmentResourcesGpuType.ts new file mode 100644 index 000000000..cb55bbd15 --- /dev/null +++ b/packages/trueforge-sdk/src/api/types/DaytonaSandboxEnvironmentResourcesGpuType.ts @@ -0,0 +1,8 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../index.js"; + +/** + * Preferred GPU type, or an ordered fallback list. + */ +export type DaytonaSandboxEnvironmentResourcesGpuType = TrueForge.DaytonaGpuType | TrueForge.DaytonaGpuType[]; diff --git a/packages/trueforge-sdk/src/api/types/DaytonaSnapshotImage.ts b/packages/trueforge-sdk/src/api/types/DaytonaSnapshotImage.ts new file mode 100644 index 000000000..2ac83bcdf --- /dev/null +++ b/packages/trueforge-sdk/src/api/types/DaytonaSnapshotImage.ts @@ -0,0 +1,7 @@ +// This file was auto-generated by Fern from our API Definition. + +export interface DaytonaSnapshotImage { + /** Daytona snapshot name. */ + name: string; + type: "snapshot"; +} diff --git a/packages/trueforge-sdk/src/api/types/DaytonaTrueforgeDefaultImage.ts b/packages/trueforge-sdk/src/api/types/DaytonaTrueforgeDefaultImage.ts new file mode 100644 index 000000000..f50ee4bbe --- /dev/null +++ b/packages/trueforge-sdk/src/api/types/DaytonaTrueforgeDefaultImage.ts @@ -0,0 +1,5 @@ +// This file was auto-generated by Fern from our API Definition. + +export interface DaytonaTrueforgeDefaultImage { + type: "trueforge-default"; +} diff --git a/packages/trueforge-sdk/src/api/types/DeleteSandboxEnvironmentResponse.ts b/packages/trueforge-sdk/src/api/types/DeleteSandboxEnvironmentResponse.ts new file mode 100644 index 000000000..f9bb1fc09 --- /dev/null +++ b/packages/trueforge-sdk/src/api/types/DeleteSandboxEnvironmentResponse.ts @@ -0,0 +1,3 @@ +// This file was auto-generated by Fern from our API Definition. + +export type DeleteSandboxEnvironmentResponse = {}; diff --git a/packages/trueforge-sdk/src/api/types/GetSandboxEnvironmentResponse.ts b/packages/trueforge-sdk/src/api/types/GetSandboxEnvironmentResponse.ts new file mode 100644 index 000000000..eaaa4d921 --- /dev/null +++ b/packages/trueforge-sdk/src/api/types/GetSandboxEnvironmentResponse.ts @@ -0,0 +1,7 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../index.js"; + +export interface GetSandboxEnvironmentResponse { + data: TrueForge.SandboxEnvironment; +} diff --git a/packages/trueforge-sdk/src/api/types/ListPermissionsData.ts b/packages/trueforge-sdk/src/api/types/ListPermissionsData.ts index e66887730..b4f62f10d 100644 --- a/packages/trueforge-sdk/src/api/types/ListPermissionsData.ts +++ b/packages/trueforge-sdk/src/api/types/ListPermissionsData.ts @@ -3,7 +3,7 @@ import type * as TrueForge from "../index.js"; export interface ListPermissionsData { - /** For agent/schedule/session: keyed by resource id. For tenant: keyed by entity kind (e.g. `agent` → `CREATE`). */ + /** For agent/schedule/session: keyed by resource id. For tenant: keyed by entity kind (e.g. `agent` or `sandbox-environment` → `CREATE`). */ permissions: Record; type: TrueForge.PermissionResourceType; } diff --git a/packages/trueforge-sdk/src/api/types/ListSandboxEnvironmentsResponse.ts b/packages/trueforge-sdk/src/api/types/ListSandboxEnvironmentsResponse.ts new file mode 100644 index 000000000..93b9b399e --- /dev/null +++ b/packages/trueforge-sdk/src/api/types/ListSandboxEnvironmentsResponse.ts @@ -0,0 +1,8 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../index.js"; + +export interface ListSandboxEnvironmentsResponse { + data: TrueForge.SandboxEnvironment[]; + pagination: TrueForge.TokenPagination; +} diff --git a/packages/trueforge-sdk/src/api/types/SandboxConfig.ts b/packages/trueforge-sdk/src/api/types/SandboxConfig.ts index ebd8ca110..f7c6bb649 100644 --- a/packages/trueforge-sdk/src/api/types/SandboxConfig.ts +++ b/packages/trueforge-sdk/src/api/types/SandboxConfig.ts @@ -3,6 +3,8 @@ export interface SandboxConfig { /** Give the agent a sandbox. Required for skills and Code Mode. */ enabled: boolean; + /** Caller-owned sandbox environment name. Omit to use the tenant provider defaults. */ + environment?: string; /** Allow downloading agent-produced files via the turn download endpoint. Default: true. */ fileDownloads?: boolean; } diff --git a/packages/trueforge-sdk/src/api/types/SandboxEnvironment.ts b/packages/trueforge-sdk/src/api/types/SandboxEnvironment.ts new file mode 100644 index 000000000..30ee28f96 --- /dev/null +++ b/packages/trueforge-sdk/src/api/types/SandboxEnvironment.ts @@ -0,0 +1,17 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../index.js"; + +export interface SandboxEnvironment { + /** ISO-8601 create time. */ + createdAt: Date; + createdBySubject: TrueForge.CreatedBySubject; + /** Optional human-readable description. */ + description: string | null; + /** Immutable server-generated environment identifier. */ + id: string; + manifest: TrueForge.SandboxEnvironmentManifest; + name: TrueForge.ResourceName; + /** ISO-8601 last update time. */ + updatedAt: Date; +} diff --git a/packages/trueforge-sdk/src/api/types/SandboxEnvironmentManifest.ts b/packages/trueforge-sdk/src/api/types/SandboxEnvironmentManifest.ts new file mode 100644 index 000000000..c3cab7ace --- /dev/null +++ b/packages/trueforge-sdk/src/api/types/SandboxEnvironmentManifest.ts @@ -0,0 +1,16 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../index.js"; + +export interface SandboxEnvironmentManifest { + image: TrueForge.DaytonaSandboxEnvironmentImage; + lifecycle?: TrueForge.DaytonaSandboxEnvironmentLifecycle; + networking?: TrueForge.DaytonaSandboxEnvironmentNetworking; + /** Must match the configured sandbox provider name/type. */ + provider: "daytona"; + resources?: TrueForge.DaytonaSandboxEnvironmentResources; + /** Map of sandbox env var name to an existing Daytona organization secret name. */ + secrets?: Record; + /** Daytona sandbox environment. */ + type: "daytona"; +} diff --git a/packages/trueforge-sdk/src/api/types/index.ts b/packages/trueforge-sdk/src/api/types/index.ts index 9b29138b5..2985092b1 100644 --- a/packages/trueforge-sdk/src/api/types/index.ts +++ b/packages/trueforge-sdk/src/api/types/index.ts @@ -49,8 +49,18 @@ export * from "./CreateScheduleRunResponse.js"; export * from "./CreateSessionAgent.js"; export * from "./CronExpression.js"; export * from "./CustomModelProvider.js"; +export * from "./DaytonaDockerImage.js"; +export * from "./DaytonaGpuType.js"; +export * from "./DaytonaSandboxEnvironmentImage.js"; +export * from "./DaytonaSandboxEnvironmentLifecycle.js"; +export * from "./DaytonaSandboxEnvironmentNetworking.js"; +export * from "./DaytonaSandboxEnvironmentResources.js"; +export * from "./DaytonaSandboxEnvironmentResourcesGpuType.js"; export * from "./DaytonaSandboxProviderAuth.js"; +export * from "./DaytonaSnapshotImage.js"; +export * from "./DaytonaTrueforgeDefaultImage.js"; export * from "./DeleteAgentResponse.js"; +export * from "./DeleteSandboxEnvironmentResponse.js"; export * from "./DeleteScheduleResponse.js"; export * from "./DynamicSubAgentsConfig.js"; export * from "./ExtendedChunkDeltaToolCall.js"; @@ -68,6 +78,7 @@ export * from "./GetMeResponse.js"; export * from "./GetMeSubject.js"; export * from "./GetModelProviderCatalogResponse.js"; export * from "./GetModelProviderResponse.js"; +export * from "./GetSandboxEnvironmentResponse.js"; export * from "./GetSandboxProviderCatalogResponse.js"; export * from "./GetSandboxProviderResponse.js"; export * from "./GetScheduleResponse.js"; @@ -92,6 +103,7 @@ export * from "./ListMcpServerToolsResponse.js"; export * from "./ListModelProvidersResponse.js"; export * from "./ListPermissionsData.js"; export * from "./ListPermissionsResponse.js"; +export * from "./ListSandboxEnvironmentsResponse.js"; export * from "./ListScheduleRunsResponse.js"; export * from "./ListSchedulesResponse.js"; export * from "./ListSessionEventsResponse.js"; @@ -155,6 +167,8 @@ export * from "./SandboxBuildStatus.js"; export * from "./SandboxCapability.js"; export * from "./SandboxConfig.js"; export * from "./SandboxCreatedEvent.js"; +export * from "./SandboxEnvironment.js"; +export * from "./SandboxEnvironmentManifest.js"; export * from "./SandboxProviderManifest.js"; export * from "./Schedule.js"; export * from "./ScheduleManifest.js"; diff --git a/packages/trueforge-sdk/src/serialization/resources/index.ts b/packages/trueforge-sdk/src/serialization/resources/index.ts index 4d4bfd3a0..fb0e61aaf 100644 --- a/packages/trueforge-sdk/src/serialization/resources/index.ts +++ b/packages/trueforge-sdk/src/serialization/resources/index.ts @@ -2,6 +2,8 @@ export * from "./agents/client/requests/index.js"; export * as agents from "./agents/index.js"; export * from "./internal/client/requests/index.js"; export * as internal from "./internal/index.js"; +export * from "./sandboxEnvironments/client/requests/index.js"; +export * as sandboxEnvironments from "./sandboxEnvironments/index.js"; export * from "./schedules/client/requests/index.js"; export * as schedules from "./schedules/index.js"; export * from "./sessions/client/requests/index.js"; diff --git a/packages/trueforge-sdk/src/serialization/resources/sandboxEnvironments/client/index.ts b/packages/trueforge-sdk/src/serialization/resources/sandboxEnvironments/client/index.ts new file mode 100644 index 000000000..195f9aa8a --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/resources/sandboxEnvironments/client/index.ts @@ -0,0 +1 @@ +export * from "./requests/index.js"; diff --git a/packages/trueforge-sdk/src/serialization/resources/sandboxEnvironments/client/requests/CreateSandboxEnvironmentRequest.ts b/packages/trueforge-sdk/src/serialization/resources/sandboxEnvironments/client/requests/CreateSandboxEnvironmentRequest.ts new file mode 100644 index 000000000..fa8f816a6 --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/resources/sandboxEnvironments/client/requests/CreateSandboxEnvironmentRequest.ts @@ -0,0 +1,24 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../../../../api/index.js"; +import * as core from "../../../../../core/index.js"; +import type * as serializers from "../../../../index.js"; +import { ResourceName } from "../../../../types/ResourceName.js"; +import { SandboxEnvironmentManifest } from "../../../../types/SandboxEnvironmentManifest.js"; + +export const CreateSandboxEnvironmentRequest: core.serialization.Schema< + serializers.CreateSandboxEnvironmentRequest.Raw, + TrueForge.CreateSandboxEnvironmentRequest +> = core.serialization.object({ + description: core.serialization.string().optional(), + manifest: SandboxEnvironmentManifest, + name: ResourceName, +}); + +export declare namespace CreateSandboxEnvironmentRequest { + export interface Raw { + description?: string | null; + manifest: SandboxEnvironmentManifest.Raw; + name: ResourceName.Raw; + } +} diff --git a/packages/trueforge-sdk/src/serialization/resources/sandboxEnvironments/client/requests/UpdateSandboxEnvironmentRequest.ts b/packages/trueforge-sdk/src/serialization/resources/sandboxEnvironments/client/requests/UpdateSandboxEnvironmentRequest.ts new file mode 100644 index 000000000..7b733a6ee --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/resources/sandboxEnvironments/client/requests/UpdateSandboxEnvironmentRequest.ts @@ -0,0 +1,21 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../../../../api/index.js"; +import * as core from "../../../../../core/index.js"; +import type * as serializers from "../../../../index.js"; +import { SandboxEnvironmentManifest } from "../../../../types/SandboxEnvironmentManifest.js"; + +export const UpdateSandboxEnvironmentRequest: core.serialization.Schema< + serializers.UpdateSandboxEnvironmentRequest.Raw, + TrueForge.UpdateSandboxEnvironmentRequest +> = core.serialization.object({ + description: core.serialization.string().optionalNullable(), + manifest: SandboxEnvironmentManifest, +}); + +export declare namespace UpdateSandboxEnvironmentRequest { + export interface Raw { + description?: (string | null | undefined) | null; + manifest: SandboxEnvironmentManifest.Raw; + } +} diff --git a/packages/trueforge-sdk/src/serialization/resources/sandboxEnvironments/client/requests/index.ts b/packages/trueforge-sdk/src/serialization/resources/sandboxEnvironments/client/requests/index.ts new file mode 100644 index 000000000..77023ee76 --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/resources/sandboxEnvironments/client/requests/index.ts @@ -0,0 +1,2 @@ +export { CreateSandboxEnvironmentRequest } from "./CreateSandboxEnvironmentRequest.js"; +export { UpdateSandboxEnvironmentRequest } from "./UpdateSandboxEnvironmentRequest.js"; diff --git a/packages/trueforge-sdk/src/serialization/resources/sandboxEnvironments/index.ts b/packages/trueforge-sdk/src/serialization/resources/sandboxEnvironments/index.ts new file mode 100644 index 000000000..914b8c3c7 --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/resources/sandboxEnvironments/index.ts @@ -0,0 +1 @@ +export * from "./client/index.js"; diff --git a/packages/trueforge-sdk/src/serialization/types/DaytonaDockerImage.ts b/packages/trueforge-sdk/src/serialization/types/DaytonaDockerImage.ts new file mode 100644 index 000000000..5dbc1058c --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/types/DaytonaDockerImage.ts @@ -0,0 +1,20 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../api/index.js"; +import * as core from "../../core/index.js"; +import type * as serializers from "../index.js"; + +export const DaytonaDockerImage: core.serialization.ObjectSchema< + serializers.DaytonaDockerImage.Raw, + TrueForge.DaytonaDockerImage +> = core.serialization.object({ + ref: core.serialization.string(), + type: core.serialization.stringLiteral("docker"), +}); + +export declare namespace DaytonaDockerImage { + export interface Raw { + ref: string; + type: "docker"; + } +} diff --git a/packages/trueforge-sdk/src/serialization/types/DaytonaGpuType.ts b/packages/trueforge-sdk/src/serialization/types/DaytonaGpuType.ts new file mode 100644 index 000000000..3640aa8ff --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/types/DaytonaGpuType.ts @@ -0,0 +1,12 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../api/index.js"; +import * as core from "../../core/index.js"; +import type * as serializers from "../index.js"; + +export const DaytonaGpuType: core.serialization.Schema = + core.serialization.enum_(["H100", "H200", "RTX-PRO-6000", "RTX-4090", "RTX-5090"]); + +export declare namespace DaytonaGpuType { + export type Raw = "H100" | "H200" | "RTX-PRO-6000" | "RTX-4090" | "RTX-5090"; +} diff --git a/packages/trueforge-sdk/src/serialization/types/DaytonaSandboxEnvironmentImage.ts b/packages/trueforge-sdk/src/serialization/types/DaytonaSandboxEnvironmentImage.ts new file mode 100644 index 000000000..5e52642cb --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/types/DaytonaSandboxEnvironmentImage.ts @@ -0,0 +1,17 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../api/index.js"; +import * as core from "../../core/index.js"; +import type * as serializers from "../index.js"; +import { DaytonaDockerImage } from "./DaytonaDockerImage.js"; +import { DaytonaSnapshotImage } from "./DaytonaSnapshotImage.js"; +import { DaytonaTrueforgeDefaultImage } from "./DaytonaTrueforgeDefaultImage.js"; + +export const DaytonaSandboxEnvironmentImage: core.serialization.Schema< + serializers.DaytonaSandboxEnvironmentImage.Raw, + TrueForge.DaytonaSandboxEnvironmentImage +> = core.serialization.undiscriminatedUnion([DaytonaDockerImage, DaytonaSnapshotImage, DaytonaTrueforgeDefaultImage]); + +export declare namespace DaytonaSandboxEnvironmentImage { + export type Raw = DaytonaDockerImage.Raw | DaytonaSnapshotImage.Raw | DaytonaTrueforgeDefaultImage.Raw; +} diff --git a/packages/trueforge-sdk/src/serialization/types/DaytonaSandboxEnvironmentLifecycle.ts b/packages/trueforge-sdk/src/serialization/types/DaytonaSandboxEnvironmentLifecycle.ts new file mode 100644 index 000000000..83132d770 --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/types/DaytonaSandboxEnvironmentLifecycle.ts @@ -0,0 +1,31 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../api/index.js"; +import * as core from "../../core/index.js"; +import type * as serializers from "../index.js"; + +export const DaytonaSandboxEnvironmentLifecycle: core.serialization.ObjectSchema< + serializers.DaytonaSandboxEnvironmentLifecycle.Raw, + TrueForge.DaytonaSandboxEnvironmentLifecycle +> = core.serialization.object({ + autoArchiveIntervalInMinutes: core.serialization.property( + "auto_archive_interval_in_minutes", + core.serialization.number().optional(), + ), + autoDeleteIntervalInMinutes: core.serialization.property( + "auto_delete_interval_in_minutes", + core.serialization.number().optional(), + ), + autoStopIntervalInMinutes: core.serialization.property( + "auto_stop_interval_in_minutes", + core.serialization.number().optional(), + ), +}); + +export declare namespace DaytonaSandboxEnvironmentLifecycle { + export interface Raw { + auto_archive_interval_in_minutes?: number | null; + auto_delete_interval_in_minutes?: number | null; + auto_stop_interval_in_minutes?: number | null; + } +} diff --git a/packages/trueforge-sdk/src/serialization/types/DaytonaSandboxEnvironmentNetworking.ts b/packages/trueforge-sdk/src/serialization/types/DaytonaSandboxEnvironmentNetworking.ts new file mode 100644 index 000000000..599ba6355 --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/types/DaytonaSandboxEnvironmentNetworking.ts @@ -0,0 +1,24 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../api/index.js"; +import * as core from "../../core/index.js"; +import type * as serializers from "../index.js"; + +export const DaytonaSandboxEnvironmentNetworking: core.serialization.ObjectSchema< + serializers.DaytonaSandboxEnvironmentNetworking.Raw, + TrueForge.DaytonaSandboxEnvironmentNetworking +> = core.serialization.object({ + domainAllowList: core.serialization.property("domain_allow_list", core.serialization.string().optional()), + networkAllowList: core.serialization.property("network_allow_list", core.serialization.string().optional()), + networkBlockAll: core.serialization.property("network_block_all", core.serialization.boolean().optional()), + outboundProxyUrl: core.serialization.property("outbound_proxy_url", core.serialization.string().optional()), +}); + +export declare namespace DaytonaSandboxEnvironmentNetworking { + export interface Raw { + domain_allow_list?: string | null; + network_allow_list?: string | null; + network_block_all?: boolean | null; + outbound_proxy_url?: string | null; + } +} diff --git a/packages/trueforge-sdk/src/serialization/types/DaytonaSandboxEnvironmentResources.ts b/packages/trueforge-sdk/src/serialization/types/DaytonaSandboxEnvironmentResources.ts new file mode 100644 index 000000000..fae5eb8ba --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/types/DaytonaSandboxEnvironmentResources.ts @@ -0,0 +1,27 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../api/index.js"; +import * as core from "../../core/index.js"; +import type * as serializers from "../index.js"; +import { DaytonaSandboxEnvironmentResourcesGpuType } from "./DaytonaSandboxEnvironmentResourcesGpuType.js"; + +export const DaytonaSandboxEnvironmentResources: core.serialization.ObjectSchema< + serializers.DaytonaSandboxEnvironmentResources.Raw, + TrueForge.DaytonaSandboxEnvironmentResources +> = core.serialization.object({ + cpu: core.serialization.number().optional(), + disk: core.serialization.number().optional(), + gpu: core.serialization.number().optional(), + gpuType: core.serialization.property("gpu_type", DaytonaSandboxEnvironmentResourcesGpuType.optional()), + memory: core.serialization.number().optional(), +}); + +export declare namespace DaytonaSandboxEnvironmentResources { + export interface Raw { + cpu?: number | null; + disk?: number | null; + gpu?: number | null; + gpu_type?: DaytonaSandboxEnvironmentResourcesGpuType.Raw | null; + memory?: number | null; + } +} diff --git a/packages/trueforge-sdk/src/serialization/types/DaytonaSandboxEnvironmentResourcesGpuType.ts b/packages/trueforge-sdk/src/serialization/types/DaytonaSandboxEnvironmentResourcesGpuType.ts new file mode 100644 index 000000000..a19a2c72a --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/types/DaytonaSandboxEnvironmentResourcesGpuType.ts @@ -0,0 +1,15 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../api/index.js"; +import * as core from "../../core/index.js"; +import type * as serializers from "../index.js"; +import { DaytonaGpuType } from "./DaytonaGpuType.js"; + +export const DaytonaSandboxEnvironmentResourcesGpuType: core.serialization.Schema< + serializers.DaytonaSandboxEnvironmentResourcesGpuType.Raw, + TrueForge.DaytonaSandboxEnvironmentResourcesGpuType +> = core.serialization.undiscriminatedUnion([DaytonaGpuType, core.serialization.list(DaytonaGpuType)]); + +export declare namespace DaytonaSandboxEnvironmentResourcesGpuType { + export type Raw = DaytonaGpuType.Raw | DaytonaGpuType.Raw[]; +} diff --git a/packages/trueforge-sdk/src/serialization/types/DaytonaSnapshotImage.ts b/packages/trueforge-sdk/src/serialization/types/DaytonaSnapshotImage.ts new file mode 100644 index 000000000..00462f552 --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/types/DaytonaSnapshotImage.ts @@ -0,0 +1,20 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../api/index.js"; +import * as core from "../../core/index.js"; +import type * as serializers from "../index.js"; + +export const DaytonaSnapshotImage: core.serialization.ObjectSchema< + serializers.DaytonaSnapshotImage.Raw, + TrueForge.DaytonaSnapshotImage +> = core.serialization.object({ + name: core.serialization.string(), + type: core.serialization.stringLiteral("snapshot"), +}); + +export declare namespace DaytonaSnapshotImage { + export interface Raw { + name: string; + type: "snapshot"; + } +} diff --git a/packages/trueforge-sdk/src/serialization/types/DaytonaTrueforgeDefaultImage.ts b/packages/trueforge-sdk/src/serialization/types/DaytonaTrueforgeDefaultImage.ts new file mode 100644 index 000000000..30cf8f0f1 --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/types/DaytonaTrueforgeDefaultImage.ts @@ -0,0 +1,18 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../api/index.js"; +import * as core from "../../core/index.js"; +import type * as serializers from "../index.js"; + +export const DaytonaTrueforgeDefaultImage: core.serialization.ObjectSchema< + serializers.DaytonaTrueforgeDefaultImage.Raw, + TrueForge.DaytonaTrueforgeDefaultImage +> = core.serialization.object({ + type: core.serialization.stringLiteral("trueforge-default"), +}); + +export declare namespace DaytonaTrueforgeDefaultImage { + export interface Raw { + type: "trueforge-default"; + } +} diff --git a/packages/trueforge-sdk/src/serialization/types/DeleteSandboxEnvironmentResponse.ts b/packages/trueforge-sdk/src/serialization/types/DeleteSandboxEnvironmentResponse.ts new file mode 100644 index 000000000..11565de1b --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/types/DeleteSandboxEnvironmentResponse.ts @@ -0,0 +1,14 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../api/index.js"; +import * as core from "../../core/index.js"; +import type * as serializers from "../index.js"; + +export const DeleteSandboxEnvironmentResponse: core.serialization.ObjectSchema< + serializers.DeleteSandboxEnvironmentResponse.Raw, + TrueForge.DeleteSandboxEnvironmentResponse +> = core.serialization.object({}); + +export declare namespace DeleteSandboxEnvironmentResponse { + export type Raw = {}; +} diff --git a/packages/trueforge-sdk/src/serialization/types/GetSandboxEnvironmentResponse.ts b/packages/trueforge-sdk/src/serialization/types/GetSandboxEnvironmentResponse.ts new file mode 100644 index 000000000..400aa98b2 --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/types/GetSandboxEnvironmentResponse.ts @@ -0,0 +1,19 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../api/index.js"; +import * as core from "../../core/index.js"; +import type * as serializers from "../index.js"; +import { SandboxEnvironment } from "./SandboxEnvironment.js"; + +export const GetSandboxEnvironmentResponse: core.serialization.ObjectSchema< + serializers.GetSandboxEnvironmentResponse.Raw, + TrueForge.GetSandboxEnvironmentResponse +> = core.serialization.object({ + data: SandboxEnvironment, +}); + +export declare namespace GetSandboxEnvironmentResponse { + export interface Raw { + data: SandboxEnvironment.Raw; + } +} diff --git a/packages/trueforge-sdk/src/serialization/types/ListSandboxEnvironmentsResponse.ts b/packages/trueforge-sdk/src/serialization/types/ListSandboxEnvironmentsResponse.ts new file mode 100644 index 000000000..c33d25772 --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/types/ListSandboxEnvironmentsResponse.ts @@ -0,0 +1,22 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../api/index.js"; +import * as core from "../../core/index.js"; +import type * as serializers from "../index.js"; +import { SandboxEnvironment } from "./SandboxEnvironment.js"; +import { TokenPagination } from "./TokenPagination.js"; + +export const ListSandboxEnvironmentsResponse: core.serialization.ObjectSchema< + serializers.ListSandboxEnvironmentsResponse.Raw, + TrueForge.ListSandboxEnvironmentsResponse +> = core.serialization.object({ + data: core.serialization.list(SandboxEnvironment), + pagination: TokenPagination, +}); + +export declare namespace ListSandboxEnvironmentsResponse { + export interface Raw { + data: SandboxEnvironment.Raw[]; + pagination: TokenPagination.Raw; + } +} diff --git a/packages/trueforge-sdk/src/serialization/types/SandboxConfig.ts b/packages/trueforge-sdk/src/serialization/types/SandboxConfig.ts index 6476134b7..e274591f1 100644 --- a/packages/trueforge-sdk/src/serialization/types/SandboxConfig.ts +++ b/packages/trueforge-sdk/src/serialization/types/SandboxConfig.ts @@ -7,12 +7,14 @@ import type * as serializers from "../index.js"; export const SandboxConfig: core.serialization.ObjectSchema = core.serialization.object({ enabled: core.serialization.boolean(), + environment: core.serialization.string().optional(), fileDownloads: core.serialization.property("file_downloads", core.serialization.boolean().optional()), }); export declare namespace SandboxConfig { export interface Raw { enabled: boolean; + environment?: string | null; file_downloads?: boolean | null; } } diff --git a/packages/trueforge-sdk/src/serialization/types/SandboxEnvironment.ts b/packages/trueforge-sdk/src/serialization/types/SandboxEnvironment.ts new file mode 100644 index 000000000..129422971 --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/types/SandboxEnvironment.ts @@ -0,0 +1,33 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../api/index.js"; +import * as core from "../../core/index.js"; +import type * as serializers from "../index.js"; +import { CreatedBySubject } from "./CreatedBySubject.js"; +import { ResourceName } from "./ResourceName.js"; +import { SandboxEnvironmentManifest } from "./SandboxEnvironmentManifest.js"; + +export const SandboxEnvironment: core.serialization.ObjectSchema< + serializers.SandboxEnvironment.Raw, + TrueForge.SandboxEnvironment +> = core.serialization.object({ + createdAt: core.serialization.property("created_at", core.serialization.date()), + createdBySubject: core.serialization.property("created_by_subject", CreatedBySubject), + description: core.serialization.string().nullable(), + id: core.serialization.string(), + manifest: SandboxEnvironmentManifest, + name: ResourceName, + updatedAt: core.serialization.property("updated_at", core.serialization.date()), +}); + +export declare namespace SandboxEnvironment { + export interface Raw { + created_at: string; + created_by_subject: CreatedBySubject.Raw; + description?: string | null; + id: string; + manifest: SandboxEnvironmentManifest.Raw; + name: ResourceName.Raw; + updated_at: string; + } +} diff --git a/packages/trueforge-sdk/src/serialization/types/SandboxEnvironmentManifest.ts b/packages/trueforge-sdk/src/serialization/types/SandboxEnvironmentManifest.ts new file mode 100644 index 000000000..a6ebc45ba --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/types/SandboxEnvironmentManifest.ts @@ -0,0 +1,34 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../api/index.js"; +import * as core from "../../core/index.js"; +import type * as serializers from "../index.js"; +import { DaytonaSandboxEnvironmentImage } from "./DaytonaSandboxEnvironmentImage.js"; +import { DaytonaSandboxEnvironmentLifecycle } from "./DaytonaSandboxEnvironmentLifecycle.js"; +import { DaytonaSandboxEnvironmentNetworking } from "./DaytonaSandboxEnvironmentNetworking.js"; +import { DaytonaSandboxEnvironmentResources } from "./DaytonaSandboxEnvironmentResources.js"; + +export const SandboxEnvironmentManifest: core.serialization.ObjectSchema< + serializers.SandboxEnvironmentManifest.Raw, + TrueForge.SandboxEnvironmentManifest +> = core.serialization.object({ + image: DaytonaSandboxEnvironmentImage, + lifecycle: DaytonaSandboxEnvironmentLifecycle.optional(), + networking: DaytonaSandboxEnvironmentNetworking.optional(), + provider: core.serialization.stringLiteral("daytona"), + resources: DaytonaSandboxEnvironmentResources.optional(), + secrets: core.serialization.record(core.serialization.string(), core.serialization.string()).optional(), + type: core.serialization.stringLiteral("daytona"), +}); + +export declare namespace SandboxEnvironmentManifest { + export interface Raw { + image: DaytonaSandboxEnvironmentImage.Raw; + lifecycle?: DaytonaSandboxEnvironmentLifecycle.Raw | null; + networking?: DaytonaSandboxEnvironmentNetworking.Raw | null; + provider: "daytona"; + resources?: DaytonaSandboxEnvironmentResources.Raw | null; + secrets?: Record | null; + type: "daytona"; + } +} diff --git a/packages/trueforge-sdk/src/serialization/types/index.ts b/packages/trueforge-sdk/src/serialization/types/index.ts index 9b29138b5..2985092b1 100644 --- a/packages/trueforge-sdk/src/serialization/types/index.ts +++ b/packages/trueforge-sdk/src/serialization/types/index.ts @@ -49,8 +49,18 @@ export * from "./CreateScheduleRunResponse.js"; export * from "./CreateSessionAgent.js"; export * from "./CronExpression.js"; export * from "./CustomModelProvider.js"; +export * from "./DaytonaDockerImage.js"; +export * from "./DaytonaGpuType.js"; +export * from "./DaytonaSandboxEnvironmentImage.js"; +export * from "./DaytonaSandboxEnvironmentLifecycle.js"; +export * from "./DaytonaSandboxEnvironmentNetworking.js"; +export * from "./DaytonaSandboxEnvironmentResources.js"; +export * from "./DaytonaSandboxEnvironmentResourcesGpuType.js"; export * from "./DaytonaSandboxProviderAuth.js"; +export * from "./DaytonaSnapshotImage.js"; +export * from "./DaytonaTrueforgeDefaultImage.js"; export * from "./DeleteAgentResponse.js"; +export * from "./DeleteSandboxEnvironmentResponse.js"; export * from "./DeleteScheduleResponse.js"; export * from "./DynamicSubAgentsConfig.js"; export * from "./ExtendedChunkDeltaToolCall.js"; @@ -68,6 +78,7 @@ export * from "./GetMeResponse.js"; export * from "./GetMeSubject.js"; export * from "./GetModelProviderCatalogResponse.js"; export * from "./GetModelProviderResponse.js"; +export * from "./GetSandboxEnvironmentResponse.js"; export * from "./GetSandboxProviderCatalogResponse.js"; export * from "./GetSandboxProviderResponse.js"; export * from "./GetScheduleResponse.js"; @@ -92,6 +103,7 @@ export * from "./ListMcpServerToolsResponse.js"; export * from "./ListModelProvidersResponse.js"; export * from "./ListPermissionsData.js"; export * from "./ListPermissionsResponse.js"; +export * from "./ListSandboxEnvironmentsResponse.js"; export * from "./ListScheduleRunsResponse.js"; export * from "./ListSchedulesResponse.js"; export * from "./ListSessionEventsResponse.js"; @@ -155,6 +167,8 @@ export * from "./SandboxBuildStatus.js"; export * from "./SandboxCapability.js"; export * from "./SandboxConfig.js"; export * from "./SandboxCreatedEvent.js"; +export * from "./SandboxEnvironment.js"; +export * from "./SandboxEnvironmentManifest.js"; export * from "./SandboxProviderManifest.js"; export * from "./Schedule.js"; export * from "./ScheduleManifest.js"; diff --git a/packages/trueforge-sdk/tests/wire/sandboxEnvironments.test.ts b/packages/trueforge-sdk/tests/wire/sandboxEnvironments.test.ts new file mode 100644 index 000000000..5324dae9a --- /dev/null +++ b/packages/trueforge-sdk/tests/wire/sandboxEnvironments.test.ts @@ -0,0 +1,691 @@ +// This file was auto-generated by Fern from our API Definition. + +import * as TrueForgeTypes from "../../src/api/index"; +import { TrueForge } from "../../src/Client"; +import { mockServerPool } from "../mock-server/MockServerPool"; + +describe("SandboxEnvironmentsClient", () => { + test("list (1)", async () => { + const server = mockServerPool.createServer(); + const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); + + const rawResponseBody = { + data: [ + { + created_at: "2024-01-15T09:30:00Z", + created_by_subject: { + subject_display_name: "subject_display_name", + subject_id: "subject_id", + subject_type: "subject_type", + }, + description: "description", + id: "id", + manifest: { image: { ref: "ref", type: "docker" }, provider: "daytona", type: "daytona" }, + name: "name", + updated_at: "2024-01-15T09:30:00Z", + }, + ], + pagination: { limit: 1, next_page_token: "next_page_token", previous_page_token: "previous_page_token" }, + }; + + server + .mockEndpoint({ once: false }) + .get("/api/v1/sandbox-environments") + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const expected = { + data: [ + { + createdAt: new Date("2024-01-15T09:30:00.000Z"), + createdBySubject: { + subjectDisplayName: "subject_display_name", + subjectId: "subject_id", + subjectType: "subject_type", + }, + description: "description", + id: "id", + manifest: { + image: { + ref: "ref", + type: "docker", + }, + provider: "daytona", + type: "daytona", + }, + name: "name", + updatedAt: new Date("2024-01-15T09:30:00.000Z"), + }, + ], + pagination: { + limit: 1, + nextPageToken: "next_page_token", + previousPageToken: "previous_page_token", + }, + }; + const page = await client.sandboxEnvironments.list(); + + expect(expected.data).toEqual(page.data); + expect(page.hasNextPage()).toBe(true); + const nextPage = await page.getNextPage(); + expect(expected.data).toEqual(nextPage.data); + }); + + test("list (2)", async () => { + const server = mockServerPool.createServer(); + const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); + + const rawResponseBody = { error: { message: "message" } }; + + server + .mockEndpoint() + .get("/api/v1/sandbox-environments") + .respondWith() + .statusCode(400) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.sandboxEnvironments.list(); + }).rejects.toThrow(TrueForgeTypes.BadRequestError); + }); + + test("list (3)", async () => { + const server = mockServerPool.createServer(); + const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); + + const rawResponseBody = { error: { message: "message" } }; + + server + .mockEndpoint() + .get("/api/v1/sandbox-environments") + .respondWith() + .statusCode(401) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.sandboxEnvironments.list(); + }).rejects.toThrow(TrueForgeTypes.UnauthorizedError); + }); + + test("create (1)", async () => { + const server = mockServerPool.createServer(); + const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); + const rawRequestBody = { + manifest: { image: { ref: "ref", type: "docker" }, provider: "daytona", type: "daytona" }, + name: "name", + }; + const rawResponseBody = { + data: { + created_at: "2024-01-15T09:30:00Z", + created_by_subject: { + subject_display_name: "subject_display_name", + subject_id: "subject_id", + subject_type: "subject_type", + }, + description: "description", + id: "id", + manifest: { + image: { ref: "ref", type: "docker" }, + provider: "daytona", + secrets: { key: "value" }, + type: "daytona", + }, + name: "name", + updated_at: "2024-01-15T09:30:00Z", + }, + }; + + server + .mockEndpoint() + .post("/api/v1/sandbox-environments") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const response = await client.sandboxEnvironments.create({ + manifest: { + image: { + ref: "ref", + type: "docker", + }, + provider: "daytona", + type: "daytona", + }, + name: "name", + }); + expect(response).toEqual({ + data: { + createdAt: new Date("2024-01-15T09:30:00.000Z"), + createdBySubject: { + subjectDisplayName: "subject_display_name", + subjectId: "subject_id", + subjectType: "subject_type", + }, + description: "description", + id: "id", + manifest: { + image: { + ref: "ref", + type: "docker", + }, + provider: "daytona", + secrets: { + key: "value", + }, + type: "daytona", + }, + name: "name", + updatedAt: new Date("2024-01-15T09:30:00.000Z"), + }, + }); + }); + + test("create (2)", async () => { + const server = mockServerPool.createServer(); + const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); + const rawRequestBody = { + manifest: { image: { ref: "x", type: "docker" }, provider: "daytona", type: "daytona" }, + name: "xy", + }; + const rawResponseBody = { error: { message: "message" } }; + + server + .mockEndpoint() + .post("/api/v1/sandbox-environments") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(400) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.sandboxEnvironments.create({ + manifest: { + image: { + ref: "x", + type: "docker", + }, + provider: "daytona", + type: "daytona", + }, + name: "xy", + }); + }).rejects.toThrow(TrueForgeTypes.BadRequestError); + }); + + test("create (3)", async () => { + const server = mockServerPool.createServer(); + const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); + const rawRequestBody = { + manifest: { image: { ref: "x", type: "docker" }, provider: "daytona", type: "daytona" }, + name: "xy", + }; + const rawResponseBody = { error: { message: "message" } }; + + server + .mockEndpoint() + .post("/api/v1/sandbox-environments") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(401) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.sandboxEnvironments.create({ + manifest: { + image: { + ref: "x", + type: "docker", + }, + provider: "daytona", + type: "daytona", + }, + name: "xy", + }); + }).rejects.toThrow(TrueForgeTypes.UnauthorizedError); + }); + + test("create (4)", async () => { + const server = mockServerPool.createServer(); + const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); + const rawRequestBody = { + manifest: { image: { ref: "x", type: "docker" }, provider: "daytona", type: "daytona" }, + name: "xy", + }; + const rawResponseBody = { error: { message: "message" } }; + + server + .mockEndpoint() + .post("/api/v1/sandbox-environments") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(409) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.sandboxEnvironments.create({ + manifest: { + image: { + ref: "x", + type: "docker", + }, + provider: "daytona", + type: "daytona", + }, + name: "xy", + }); + }).rejects.toThrow(TrueForgeTypes.ConflictError); + }); + + test("create (5)", async () => { + const server = mockServerPool.createServer(); + const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); + const rawRequestBody = { + manifest: { image: { ref: "x", type: "docker" }, provider: "daytona", type: "daytona" }, + name: "xy", + }; + const rawResponseBody = { error: { message: "message" } }; + + server + .mockEndpoint() + .post("/api/v1/sandbox-environments") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(422) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.sandboxEnvironments.create({ + manifest: { + image: { + ref: "x", + type: "docker", + }, + provider: "daytona", + type: "daytona", + }, + name: "xy", + }); + }).rejects.toThrow(TrueForgeTypes.UnprocessableEntityError); + }); + + test("get (1)", async () => { + const server = mockServerPool.createServer(); + const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); + + const rawResponseBody = { + data: { + created_at: "2024-01-15T09:30:00Z", + created_by_subject: { + subject_display_name: "subject_display_name", + subject_id: "subject_id", + subject_type: "subject_type", + }, + description: "description", + id: "id", + manifest: { + image: { ref: "ref", type: "docker" }, + provider: "daytona", + secrets: { key: "value" }, + type: "daytona", + }, + name: "name", + updated_at: "2024-01-15T09:30:00Z", + }, + }; + + server + .mockEndpoint() + .get("/api/v1/sandbox-environments/sandbox_environment_id") + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const response = await client.sandboxEnvironments.get("sandbox_environment_id"); + expect(response).toEqual({ + data: { + createdAt: new Date("2024-01-15T09:30:00.000Z"), + createdBySubject: { + subjectDisplayName: "subject_display_name", + subjectId: "subject_id", + subjectType: "subject_type", + }, + description: "description", + id: "id", + manifest: { + image: { + ref: "ref", + type: "docker", + }, + provider: "daytona", + secrets: { + key: "value", + }, + type: "daytona", + }, + name: "name", + updatedAt: new Date("2024-01-15T09:30:00.000Z"), + }, + }); + }); + + test("get (2)", async () => { + const server = mockServerPool.createServer(); + const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); + + const rawResponseBody = { error: { message: "message" } }; + + server + .mockEndpoint() + .get("/api/v1/sandbox-environments/sandbox_environment_id") + .respondWith() + .statusCode(401) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.sandboxEnvironments.get("sandbox_environment_id"); + }).rejects.toThrow(TrueForgeTypes.UnauthorizedError); + }); + + test("get (3)", async () => { + const server = mockServerPool.createServer(); + const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); + + const rawResponseBody = { error: { message: "message" } }; + + server + .mockEndpoint() + .get("/api/v1/sandbox-environments/sandbox_environment_id") + .respondWith() + .statusCode(404) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.sandboxEnvironments.get("sandbox_environment_id"); + }).rejects.toThrow(TrueForgeTypes.NotFoundError); + }); + + test("update (1)", async () => { + const server = mockServerPool.createServer(); + const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); + const rawRequestBody = { + manifest: { image: { ref: "ref", type: "docker" }, provider: "daytona", type: "daytona" }, + }; + const rawResponseBody = { + data: { + created_at: "2024-01-15T09:30:00Z", + created_by_subject: { + subject_display_name: "subject_display_name", + subject_id: "subject_id", + subject_type: "subject_type", + }, + description: "description", + id: "id", + manifest: { + image: { ref: "ref", type: "docker" }, + provider: "daytona", + secrets: { key: "value" }, + type: "daytona", + }, + name: "name", + updated_at: "2024-01-15T09:30:00Z", + }, + }; + + server + .mockEndpoint() + .put("/api/v1/sandbox-environments/sandbox_environment_id") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const response = await client.sandboxEnvironments.update("sandbox_environment_id", { + manifest: { + image: { + ref: "ref", + type: "docker", + }, + provider: "daytona", + type: "daytona", + }, + }); + expect(response).toEqual({ + data: { + createdAt: new Date("2024-01-15T09:30:00.000Z"), + createdBySubject: { + subjectDisplayName: "subject_display_name", + subjectId: "subject_id", + subjectType: "subject_type", + }, + description: "description", + id: "id", + manifest: { + image: { + ref: "ref", + type: "docker", + }, + provider: "daytona", + secrets: { + key: "value", + }, + type: "daytona", + }, + name: "name", + updatedAt: new Date("2024-01-15T09:30:00.000Z"), + }, + }); + }); + + test("update (2)", async () => { + const server = mockServerPool.createServer(); + const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); + const rawRequestBody = { + manifest: { image: { ref: "x", type: "docker" }, provider: "daytona", type: "daytona" }, + }; + const rawResponseBody = { error: { message: "message" } }; + + server + .mockEndpoint() + .put("/api/v1/sandbox-environments/sandbox_environment_id") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(400) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.sandboxEnvironments.update("sandbox_environment_id", { + manifest: { + image: { + ref: "x", + type: "docker", + }, + provider: "daytona", + type: "daytona", + }, + }); + }).rejects.toThrow(TrueForgeTypes.BadRequestError); + }); + + test("update (3)", async () => { + const server = mockServerPool.createServer(); + const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); + const rawRequestBody = { + manifest: { image: { ref: "x", type: "docker" }, provider: "daytona", type: "daytona" }, + }; + const rawResponseBody = { error: { message: "message" } }; + + server + .mockEndpoint() + .put("/api/v1/sandbox-environments/sandbox_environment_id") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(401) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.sandboxEnvironments.update("sandbox_environment_id", { + manifest: { + image: { + ref: "x", + type: "docker", + }, + provider: "daytona", + type: "daytona", + }, + }); + }).rejects.toThrow(TrueForgeTypes.UnauthorizedError); + }); + + test("update (4)", async () => { + const server = mockServerPool.createServer(); + const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); + const rawRequestBody = { + manifest: { image: { ref: "x", type: "docker" }, provider: "daytona", type: "daytona" }, + }; + const rawResponseBody = { error: { message: "message" } }; + + server + .mockEndpoint() + .put("/api/v1/sandbox-environments/sandbox_environment_id") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(404) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.sandboxEnvironments.update("sandbox_environment_id", { + manifest: { + image: { + ref: "x", + type: "docker", + }, + provider: "daytona", + type: "daytona", + }, + }); + }).rejects.toThrow(TrueForgeTypes.NotFoundError); + }); + + test("update (5)", async () => { + const server = mockServerPool.createServer(); + const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); + const rawRequestBody = { + manifest: { image: { ref: "x", type: "docker" }, provider: "daytona", type: "daytona" }, + }; + const rawResponseBody = { error: { message: "message" } }; + + server + .mockEndpoint() + .put("/api/v1/sandbox-environments/sandbox_environment_id") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(422) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.sandboxEnvironments.update("sandbox_environment_id", { + manifest: { + image: { + ref: "x", + type: "docker", + }, + provider: "daytona", + type: "daytona", + }, + }); + }).rejects.toThrow(TrueForgeTypes.UnprocessableEntityError); + }); + + test("delete (1)", async () => { + const server = mockServerPool.createServer(); + const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); + + const rawResponseBody = {}; + + server + .mockEndpoint() + .delete("/api/v1/sandbox-environments/sandbox_environment_id") + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const response = await client.sandboxEnvironments.delete("sandbox_environment_id"); + expect(response).toEqual({}); + }); + + test("delete (2)", async () => { + const server = mockServerPool.createServer(); + const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); + + const rawResponseBody = { error: { message: "message" } }; + + server + .mockEndpoint() + .delete("/api/v1/sandbox-environments/sandbox_environment_id") + .respondWith() + .statusCode(401) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.sandboxEnvironments.delete("sandbox_environment_id"); + }).rejects.toThrow(TrueForgeTypes.UnauthorizedError); + }); + + test("delete (3)", async () => { + const server = mockServerPool.createServer(); + const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); + + const rawResponseBody = { error: { message: "message" } }; + + server + .mockEndpoint() + .delete("/api/v1/sandbox-environments/sandbox_environment_id") + .respondWith() + .statusCode(404) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.sandboxEnvironments.delete("sandbox_environment_id"); + }).rejects.toThrow(TrueForgeTypes.NotFoundError); + }); + + test("delete (4)", async () => { + const server = mockServerPool.createServer(); + const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); + + const rawResponseBody = { error: { message: "message" } }; + + server + .mockEndpoint() + .delete("/api/v1/sandbox-environments/sandbox_environment_id") + .respondWith() + .statusCode(409) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.sandboxEnvironments.delete("sandbox_environment_id"); + }).rejects.toThrow(TrueForgeTypes.ConflictError); + }); +}); diff --git a/packages/trueforge/scripts/write-openapi.ts b/packages/trueforge/scripts/write-openapi.ts index 69c19657d..7aa7878ac 100644 --- a/packages/trueforge/scripts/write-openapi.ts +++ b/packages/trueforge/scripts/write-openapi.ts @@ -26,6 +26,7 @@ import { SqliteAgentStore } from '../src/db/sqlite/agent-store/SqliteAgentStore' import { createSqliteDb } from '../src/db/sqlite/client'; import { SqliteMcpServerStore } from '../src/db/sqlite/mcp-server-store/SqliteMcpServerStore'; import { SqliteModelProviderStore } from '../src/db/sqlite/model-provider-store/SqliteModelProviderStore'; +import { SqliteSandboxEnvironmentStore } from '../src/db/sqlite/sandbox-environment-store/SqliteSandboxEnvironmentStore'; import { SqliteSandboxProviderStore } from '../src/db/sqlite/sandbox-provider-store/SqliteSandboxProviderStore'; import { SqliteScheduleStore } from '../src/db/sqlite/schedule-store/SqliteScheduleStore'; import { SqliteSessionMetricsStore } from '../src/db/sqlite/session-metrics/SqliteSessionMetricsStore'; @@ -64,6 +65,7 @@ const tokenStore = new SqliteOAuthTokenStore(db); const agentStore = new SqliteAgentStore(db); const skillStore = new SqliteSkillStore(db); const sandboxProviderStore = new SqliteSandboxProviderStore(db); +const sandboxEnvironmentStore = new SqliteSandboxEnvironmentStore(db); const app = createServerApp({ modelCatalog: ModelCatalog.load(), mcpCatalog: McpCatalog.load(), @@ -78,6 +80,7 @@ const app = createServerApp({ }), resolveSkillStore: () => skillStore, resolveSandboxProviderStore: () => sandboxProviderStore, + resolveSandboxEnvironmentStore: () => sandboxEnvironmentStore, resolveAgentStore: () => agentStore, resolveImportAgentStore: () => agentStore, agentStore, diff --git a/packages/trueforge/src/apis/agents.ts b/packages/trueforge/src/apis/agents.ts index fafe32daf..ec0bcf44c 100644 --- a/packages/trueforge/src/apis/agents.ts +++ b/packages/trueforge/src/apis/agents.ts @@ -15,6 +15,7 @@ import { } from '../db/agentStore'; import type { IMcpServerStore } from '../db/mcpServerStore'; import type { IModelProviderStore } from '../db/modelProviderStore'; +import type { ISandboxEnvironmentStore } from '../db/sandboxEnvironmentStore'; import type { ISandboxProviderStore } from '../db/sandboxProviderStore'; import type { ISkillStore } from '../db/skillStore'; import type { WithTransaction } from '../db/transaction'; @@ -38,6 +39,7 @@ export interface AgentsRouterDeps { resolveMcpServerStore: (c: Context) => IMcpServerStore; resolveSkillStore: ResolveSkillStore; resolveSandboxProviderStore: (c: Context) => ISandboxProviderStore; + resolveSandboxEnvironmentStore: (c: Context) => ISandboxEnvironmentStore; withTransaction: WithTransaction; resolveRequestContext: ResolveRequestContext; authorizer: Authorizer; @@ -60,22 +62,28 @@ async function validateManifest({ mcpServerStore, skillStore, sandboxProviderStore, + sandboxEnvironmentStore, tenant_id, + subject_id, }: { spec: AgentSpec; modelProviderStore: IModelProviderStore; mcpServerStore: IMcpServerStore; skillStore: ISkillStore; sandboxProviderStore: ISandboxProviderStore; + sandboxEnvironmentStore: ISandboxEnvironmentStore; tenant_id: string; + subject_id: string; }): Promise { await validateAgentSpec({ spec, tenant_id, + subject_id, modelProviderStore, mcpServerStore, skillStore, sandboxProviderStore, + sandboxEnvironmentStore, }); return spec; } @@ -112,7 +120,9 @@ export function createAgentsRouter(deps: AgentsRouterDeps(deps: AgentsRouterDeps { + resolveSandboxEnvironmentStore: (c: Context) => ISandboxEnvironmentStore; + resolveSandboxProviderStore: (c: Context) => ISandboxProviderStore; + agentStore: IAgentStore; + withTransaction: WithTransaction; + resolveRequestContext: ResolveRequestContext; +} + +function toWire(record: SandboxEnvironmentRecord): SandboxEnvironment { + return { + id: record.id, + name: record.name, + description: record.description, + manifest: record.manifest, + created_by_subject: record.created_by_subject, + created_at: record.created_at, + updated_at: record.updated_at, + }; +} + +function isOwner(subjectId: string, record: SandboxEnvironmentRecord): boolean { + return record.created_by_subject.subject_id === subjectId; +} + +async function validateManifestAgainstProvider({ + manifest, + tenant_id, + sandboxProviderStore, +}: { + manifest: SandboxEnvironmentManifest; + tenant_id: string; + sandboxProviderStore: ISandboxProviderStore; +}): Promise { + const provider = await sandboxProviderStore.getSandboxProvider(tenant_id); + if (provider?.manifest.type !== 'daytona') { + throw new HTTPException(422, { + message: 'sandbox environments require a configured Daytona sandbox provider', + }); + } + if (manifest.provider !== provider.name) { + throw new HTTPException(422, { + message: `manifest.provider must match the configured sandbox provider ("${provider.name}")`, + }); + } + + if (manifest.image.type === 'trueforge-default' && provider.status !== 'ready') { + throw new HTTPException(422, { + message: 'trueforge-default image requires the sandbox provider status to be ready', + }); + } + + const daytona = new Daytona({ apiKey: provider.manifest.auth.api_key }); + + if (manifest.image.type === 'snapshot') { + try { + await daytona.snapshot.get(manifest.image.name); + } catch (error) { + if (error instanceof DaytonaError && error.statusCode === SANDBOX_NOT_FOUND_STATUS) { + throw new HTTPException(422, { + message: `Daytona snapshot "${manifest.image.name}" was not found`, + cause: error, + }); + } + if (isDaytonaAuthError(error)) { + throw new HTTPException(422, { + message: 'Daytona rejected the API key — check the credentials', + cause: error, + }); + } + if (isDaytonaPermissionError(error)) { + throw new HTTPException(422, { + message: 'Daytona denied access while verifying the snapshot', + cause: error, + }); + } + throw error; + } + } + + if (manifest.secrets) { + const secretNames = [...new Set(Object.values(manifest.secrets))]; + try { + for (const secretName of secretNames) { + // Prefix match on Daytona today; require an exact name among results. + const { items } = await daytona.secret.list({ name: secretName, limit: 200 }); + if (!items.some(secret => secret.name === secretName)) { + throw new HTTPException(422, { + message: `Daytona organization secret "${secretName}" was not found`, + }); + } + } + } catch (error) { + if (error instanceof HTTPException) { + throw error; + } + if (isDaytonaAuthError(error)) { + throw new HTTPException(422, { + message: 'Daytona rejected the API key — check the credentials', + cause: error, + }); + } + if (isDaytonaPermissionError(error)) { + throw new HTTPException(422, { + message: 'Daytona denied access while verifying organization secrets', + cause: error, + }); + } + throw error; + } + } +} + +export function createSandboxEnvironmentsRouter(deps: SandboxEnvironmentsRouterDeps) { + const listHandler: RouteHandler = async c => { + const { limit, page_token: pageToken } = c.req.valid('query'); + const requestContext = deps.resolveRequestContext(c); + try { + const { data, pagination } = await deps.resolveSandboxEnvironmentStore(c).listSandboxEnvironments({ + tenant_id: requestContext.tenant_id, + created_by_subject_id: requestContext.subject.id, + limit, + page_token: pageToken, + }); + return c.json({ data: data.map(toWire), pagination }, 200); + } catch (error) { + if (error instanceof InvalidPageTokenError) { + return c.json({ error: { message: error.message } }, 400); + } + throw error; + } + }; + + const createHandler: RouteHandler = async c => { + const body: CreateSandboxEnvironmentRequest = c.req.valid('json'); + const requestContext = deps.resolveRequestContext(c); + try { + await validateManifestAgainstProvider({ + manifest: body.manifest, + tenant_id: requestContext.tenant_id, + sandboxProviderStore: deps.resolveSandboxProviderStore(c), + }); + const record = await deps.resolveSandboxEnvironmentStore(c).createSandboxEnvironment({ + tenant_id: requestContext.tenant_id, + name: body.name, + description: body.description ?? null, + manifest: body.manifest, + created_by_subject: createdBySubjectFromRequestContext(requestContext), + }); + return c.json({ data: toWire(record) }, 201); + } catch (error) { + if (error instanceof SandboxEnvironmentNameConflictError) { + return c.json({ error: { message: error.message } }, 409); + } + throw error; + } + }; + + const getHandler: RouteHandler = async c => { + const { sandbox_environment_id: id } = c.req.valid('param'); + const requestContext = deps.resolveRequestContext(c); + const record = await deps.resolveSandboxEnvironmentStore(c).getSandboxEnvironment({ + tenant_id: requestContext.tenant_id, + id, + }); + if (!record || !isOwner(requestContext.subject.id, record)) { + return c.json({ error: { message: `Sandbox environment not found or you don't have access to it: ${id}` } }, 404); + } + return c.json({ data: toWire(record) }, 200); + }; + + const putHandler: RouteHandler = async c => { + const { sandbox_environment_id: id } = c.req.valid('param'); + const body: UpdateSandboxEnvironmentRequest = c.req.valid('json'); + const requestContext = deps.resolveRequestContext(c); + const existing = await deps.resolveSandboxEnvironmentStore(c).getSandboxEnvironment({ + tenant_id: requestContext.tenant_id, + id, + }); + if (!existing || !isOwner(requestContext.subject.id, existing)) { + return c.json({ error: { message: `Sandbox environment not found or you don't have access to it: ${id}` } }, 404); + } + await validateManifestAgainstProvider({ + manifest: body.manifest, + tenant_id: requestContext.tenant_id, + sandboxProviderStore: deps.resolveSandboxProviderStore(c), + }); + const record = await deps.resolveSandboxEnvironmentStore(c).updateSandboxEnvironment({ + tenant_id: requestContext.tenant_id, + id, + description: body.description, + manifest: body.manifest, + }); + if (!record) { + return c.json({ error: { message: `Sandbox environment not found or you don't have access to it: ${id}` } }, 404); + } + return c.json({ data: toWire(record) }, 200); + }; + + const deleteHandler: RouteHandler = async c => { + const { sandbox_environment_id: id } = c.req.valid('param'); + const requestContext = deps.resolveRequestContext(c); + const existing = await deps.resolveSandboxEnvironmentStore(c).getSandboxEnvironment({ + tenant_id: requestContext.tenant_id, + id, + }); + if (!existing || !isOwner(requestContext.subject.id, existing)) { + return c.json({ error: { message: `Sandbox environment not found or you don't have access to it: ${id}` } }, 404); + } + const agentIds = await deps.agentStore.listAgentIdsUsingSandboxEnvironment({ + tenant_id: requestContext.tenant_id, + environment_name: existing.name, + }); + if (agentIds.length) { + return c.json( + { + error: { + message: `Sandbox environment "${existing.name}" is referenced by ${String(agentIds.length)} agent(s)`, + }, + }, + 409, + ); + } + await deps.resolveSandboxEnvironmentStore(c).deleteSandboxEnvironment({ + tenant_id: requestContext.tenant_id, + id, + }); + return c.json({}, 200); + }; + + const router = new OpenAPIHono(); + router.openapi(listSandboxEnvironmentsRoute, listHandler); + router.openapi(createSandboxEnvironmentRoute, createHandler); + router.openapi(getSandboxEnvironmentRoute, getHandler); + router.openapi(putSandboxEnvironmentRoute, putHandler); + router.openapi(deleteSandboxEnvironmentRoute, deleteHandler); + return router; +} diff --git a/packages/trueforge/src/apis/sessions.ts b/packages/trueforge/src/apis/sessions.ts index 37083e58b..4f9fa5534 100644 --- a/packages/trueforge/src/apis/sessions.ts +++ b/packages/trueforge/src/apis/sessions.ts @@ -27,6 +27,7 @@ import configuration from '../config'; import type { IAgentStore } from '../db/agentStore'; import type { IMcpServerStore } from '../db/mcpServerStore'; import type { IModelProviderStore } from '../db/modelProviderStore'; +import type { ISandboxEnvironmentStore } from '../db/sandboxEnvironmentStore'; import type { ISandboxProviderStore } from '../db/sandboxProviderStore'; import { cancelSessionRoute, @@ -80,6 +81,7 @@ export interface SessionsRouterDeps { resolveSkillStore: ResolveSkillStore; resolveAgentStore: (c: Context) => IAgentStore; resolveSandboxProviderStore: (c: Context) => ISandboxProviderStore; + resolveSandboxEnvironmentStore: (c: Context) => ISandboxEnvironmentStore; redis?: RedisClientType | undefined; requestReplyRouter: RequestReplyRouter; resolveRequestContext: ResolveRequestContext; @@ -236,6 +238,7 @@ type InternalSessionsRouterDeps = Pick< | 'resolveSkillStore' | 'resolveAgentStore' | 'resolveSandboxProviderStore' + | 'resolveSandboxEnvironmentStore' | 'resolveRequestContext' | 'authorizer' >; @@ -285,10 +288,12 @@ function createGetOrCreateSessionByExternalIdHandler( await validateAgentSpec({ spec: body.agent.spec, tenant_id: requestContext.tenant_id, + subject_id: requestContext.subject.id, modelProviderStore: deps.resolveModelProviderStore(c), mcpServerStore: deps.resolveMcpServerStore(c), skillStore: deps.resolveSkillStore(c), sandboxProviderStore: deps.resolveSandboxProviderStore(c), + sandboxEnvironmentStore: deps.resolveSandboxEnvironmentStore(c), }); agent = { type: 'inline', spec: body.agent.spec }; } @@ -357,10 +362,12 @@ export function createSessionsRouter(deps: SessionsRouterDeps) { await validateAgentSpec({ spec: body.agent.spec, tenant_id: requestContext.tenant_id, + subject_id: requestContext.subject.id, modelProviderStore: deps.resolveModelProviderStore(c), mcpServerStore: deps.resolveMcpServerStore(c), skillStore: deps.resolveSkillStore(c), sandboxProviderStore: deps.resolveSandboxProviderStore(c), + sandboxEnvironmentStore: deps.resolveSandboxEnvironmentStore(c), }); const session = await deps.sessions.create({ tenant_id: requestContext.tenant_id, @@ -448,10 +455,12 @@ export function createSessionsRouter(deps: SessionsRouterDeps) { await validateAgentSpec({ spec: body.agent.spec, tenant_id: requestContext.tenant_id, + subject_id: requestContext.subject.id, modelProviderStore: deps.resolveModelProviderStore(c), mcpServerStore: deps.resolveMcpServerStore(c), skillStore: deps.resolveSkillStore(c), sandboxProviderStore: deps.resolveSandboxProviderStore(c), + sandboxEnvironmentStore: deps.resolveSandboxEnvironmentStore(c), }); } try { diff --git a/packages/trueforge/src/app.ts b/packages/trueforge/src/app.ts index a8c9ef36e..080353fef 100644 --- a/packages/trueforge/src/app.ts +++ b/packages/trueforge/src/app.ts @@ -19,6 +19,7 @@ import { createMcpOAuthRouter } from './apis/mcpOAuth'; import { createMcpServersRouter } from './apis/mcpServers'; import { createModelsRouter } from './apis/models'; import { createPermissionsRouter } from './apis/permissions'; +import { createSandboxEnvironmentsRouter } from './apis/sandboxEnvironments'; import { createScheduleExecutionRouter, createSchedulesRouter } from './apis/schedules'; import { createInternalMetricsRouter } from './apis/sessionMetrics'; import { createInternalSessionsRouter, createSessionsRouter } from './apis/sessions'; @@ -42,6 +43,7 @@ import configuration, { getPublicUiBasePath, getTrueForgeAuthMode, TrueForgeAuth import type { AgentRecord, IAgentStore } from './db/agentStore'; import type { IMcpServerWithAuthStore } from './db/mcpServerStore'; import type { IModelProviderStore } from './db/modelProviderStore'; +import type { ISandboxEnvironmentStore } from './db/sandboxEnvironmentStore'; import type { ISandboxProviderStore } from './db/sandboxProviderStore'; import type { IScheduleStore } from './db/scheduleStore'; import type { ISessionMetricsStore } from './db/sessionMetricsStore'; @@ -193,6 +195,8 @@ export interface ServerDeps { * (`TRUEFOUNDRY_SANDBOX_*` + static SETTINGS JSON). */ resolveSandboxProviderStore: (c: Context) => ISandboxProviderStore; + /** Per-request store: DB sandbox environments, or TrueFoundry empty stub. */ + resolveSandboxEnvironmentStore: (c: Context) => ISandboxEnvironmentStore; /** Per-request store: DB git skills, or TrueFoundry registry catalog in TrueFoundry mode. */ resolveSkillStore: ResolveSkillStore; withTransaction: WithTransaction; @@ -336,6 +340,7 @@ export function createServerApp(deps: ServerDeps) { resolveMcpServerStore: deps.resolveMcpServerStore, resolveSkillStore: deps.resolveSkillStore, resolveSandboxProviderStore: deps.resolveSandboxProviderStore, + resolveSandboxEnvironmentStore: deps.resolveSandboxEnvironmentStore, withTransaction: deps.withTransaction, resolveRequestContext, authorizer: deps.authorizer, @@ -343,6 +348,19 @@ export function createServerApp(deps: ServerDeps) { authMiddleware, ), ); + app.route( + '/api/v1/sandbox-environments', + withAuth( + createSandboxEnvironmentsRouter({ + resolveSandboxEnvironmentStore: deps.resolveSandboxEnvironmentStore, + resolveSandboxProviderStore: deps.resolveSandboxProviderStore, + agentStore: deps.agentStore, + withTransaction: deps.withTransaction, + resolveRequestContext, + }), + authMiddleware, + ), + ); app.route( '/api/internal/schedules', withAuth(createScheduleExecutionRouter(scheduleTurnDeps), scheduleExecutionAuthMiddleware), @@ -396,6 +414,7 @@ export function createServerApp(deps: ServerDeps) { resolveSkillStore: deps.resolveSkillStore, resolveAgentStore: deps.resolveAgentStore, resolveSandboxProviderStore: deps.resolveSandboxProviderStore, + resolveSandboxEnvironmentStore: deps.resolveSandboxEnvironmentStore, resolveRequestContext, authorizer: deps.authorizer, }), @@ -439,6 +458,7 @@ export function createServerApp(deps: ServerDeps) { resolveSkillStore: deps.resolveSkillStore, resolveAgentStore: deps.resolveAgentStore, resolveSandboxProviderStore: deps.resolveSandboxProviderStore, + resolveSandboxEnvironmentStore: deps.resolveSandboxEnvironmentStore, redis: deps.redis, requestReplyRouter: deps.requestReplyRouter, resolveRequestContext, diff --git a/packages/trueforge/src/auth/authorizer.ts b/packages/trueforge/src/auth/authorizer.ts index c7724baf9..6d43e60a8 100644 --- a/packages/trueforge/src/auth/authorizer.ts +++ b/packages/trueforge/src/auth/authorizer.ts @@ -9,6 +9,7 @@ import { SCHEDULE_OWNER_PERMISSIONS, SESSION_OWNER_PERMISSIONS, TENANT_CREATE_AGENT_PERMISSIONS, + TENANT_CREATE_SANDBOX_ENVIRONMENT_PERMISSIONS, type ListPermissionsData, } from '../schemas/permissions'; import type { RequestContext } from './identity'; @@ -49,7 +50,10 @@ export class TrueForgeAuthorizer implements Authorizer { async getPermissions(input: GetPermissionsInput): Promise { if (input.resourceType === 'tenant') { - return listPermissionsData('tenant', { agent: [...TENANT_CREATE_AGENT_PERMISSIONS] }); + return listPermissionsData('tenant', { + agent: [...TENANT_CREATE_AGENT_PERMISSIONS], + 'sandbox-environment': [...TENANT_CREATE_SANDBOX_ENVIRONMENT_PERMISSIONS], + }); } const data = emptyPermissionsByResourceId(input.resourceIds); diff --git a/packages/trueforge/src/db/agentStore.ts b/packages/trueforge/src/db/agentStore.ts index 8430680d8..8e61a5cc8 100644 --- a/packages/trueforge/src/db/agentStore.ts +++ b/packages/trueforge/src/db/agentStore.ts @@ -91,6 +91,11 @@ export interface DeleteAgentInput { id: string; } +export interface ListAgentIdsUsingSandboxEnvironmentInput { + tenant_id: string; + environment_name: string; +} + /** Unique `(tenant_id, name)` violation on create. */ export class AgentNameConflictError extends Error { readonly tenant_id: string; @@ -136,4 +141,9 @@ export interface IAgentStore { updateAgent(input: UpdateAgentInput, transaction?: TTransaction): Promise; /** Deletes by immutable id. Idempotent if already missing. */ deleteAgent(input: DeleteAgentInput, transaction?: TTransaction): Promise; + /** Agent ids whose manifest references `environment_name` under config.sandbox.environment. */ + listAgentIdsUsingSandboxEnvironment( + input: ListAgentIdsUsingSandboxEnvironmentInput, + transaction?: TTransaction, + ): Promise; } diff --git a/packages/trueforge/src/db/indexes.ts b/packages/trueforge/src/db/indexes.ts index 22ab4c7b5..5511220b1 100644 --- a/packages/trueforge/src/db/indexes.ts +++ b/packages/trueforge/src/db/indexes.ts @@ -30,3 +30,9 @@ export const SCHEDULE_RUN_CREATED_BY_SUBJECT_ID_IDX = 'schedule_run_created_by_s /** `(tenant_id, name)` unique on sandbox_provider. */ export const SANDBOX_PROVIDER_TENANT_NAME_UQ = 'sandbox_provider_tenant_name_uq'; + +/** `(tenant_id, name)` unique on sandbox_environment. */ +export const SANDBOX_ENVIRONMENT_TENANT_NAME_UQ = 'sandbox_environment_tenant_name_uq'; + +/** `(tenant_id, created_by_subject.subject_id)` on sandbox_environment. */ +export const SANDBOX_ENVIRONMENT_CREATED_BY_SUBJECT_ID_IDX = 'sandbox_environment_created_by_subject_id_idx'; diff --git a/packages/trueforge/src/db/postgres/agent-store/PostgresAgentStore.ts b/packages/trueforge/src/db/postgres/agent-store/PostgresAgentStore.ts index 5a647a839..53fc022c6 100644 --- a/packages/trueforge/src/db/postgres/agent-store/PostgresAgentStore.ts +++ b/packages/trueforge/src/db/postgres/agent-store/PostgresAgentStore.ts @@ -17,6 +17,7 @@ import { type GetExternalIdsByIdsInput, type GetOwnedIdsInput, type IAgentStore, + type ListAgentIdsUsingSandboxEnvironmentInput, type ListAgentsInput, type UpdateAgentInput, } from '../../agentStore'; @@ -205,4 +206,18 @@ export class PostgresAgentStore implements IAgentStore> { const db = transaction ?? this.#db; await db.deleteFrom('agent').where('tenant_id', '=', input.tenant_id).where('id', '=', input.id).execute(); } + + async listAgentIdsUsingSandboxEnvironment( + input: ListAgentIdsUsingSandboxEnvironmentInput, + transaction?: Transaction, + ): Promise { + const db = transaction ?? this.#db; + const rows = await db + .selectFrom('agent') + .select('id') + .where('tenant_id', '=', input.tenant_id) + .where(sql`manifest->'config'->'sandbox'->>'environment'`, '=', input.environment_name) + .execute(); + return rows.map(row => row.id); + } } diff --git a/packages/trueforge/src/db/postgres/migrations/20260918_000003_sandbox_environment.ts b/packages/trueforge/src/db/postgres/migrations/20260918_000003_sandbox_environment.ts new file mode 100644 index 000000000..e13f9699f --- /dev/null +++ b/packages/trueforge/src/db/postgres/migrations/20260918_000003_sandbox_environment.ts @@ -0,0 +1,35 @@ +import { sql, type Kysely } from 'kysely'; +import { SANDBOX_ENVIRONMENT_CREATED_BY_SUBJECT_ID_IDX, SANDBOX_ENVIRONMENT_TENANT_NAME_UQ } from '../../indexes'; + +/** + * Owned sandbox environments: immutable ULID `id` PK, unique `(tenant_id, name)`, + * Zod-validated `manifest` jsonb, creator in `created_by_subject`. + */ +export async function up(db: Kysely): Promise { + await sql`SET LOCAL lock_timeout = '5s'`.execute(db); + + await db.schema + .createTable('sandbox_environment') + .addColumn('id', 'text', col => col.notNull()) + .addColumn('tenant_id', 'text', col => col.notNull()) + .addColumn('name', 'text', col => col.notNull()) + .addColumn('description', 'text') + .addColumn('manifest', 'jsonb', col => col.notNull()) + .addColumn('created_by_subject', 'jsonb', col => col.notNull()) + .addColumn('created_at', 'timestamptz', col => col.notNull()) + .addColumn('updated_at', 'timestamptz', col => col.notNull()) + .addPrimaryKeyConstraint('sandbox_environment_pkey', ['id']) + .addUniqueConstraint(SANDBOX_ENVIRONMENT_TENANT_NAME_UQ, ['tenant_id', 'name']) + .execute(); + + await sql` + CREATE INDEX ${sql.raw(SANDBOX_ENVIRONMENT_CREATED_BY_SUBJECT_ID_IDX)} + ON sandbox_environment (tenant_id, (created_by_subject->>'subject_id')) + `.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`SET LOCAL lock_timeout = '5s'`.execute(db); + await sql`DROP INDEX IF EXISTS ${sql.raw(SANDBOX_ENVIRONMENT_CREATED_BY_SUBJECT_ID_IDX)}`.execute(db); + await db.schema.dropTable('sandbox_environment').ifExists().execute(); +} diff --git a/packages/trueforge/src/db/postgres/sandbox-environment-store/PostgresSandboxEnvironmentStore.ts b/packages/trueforge/src/db/postgres/sandbox-environment-store/PostgresSandboxEnvironmentStore.ts new file mode 100644 index 000000000..980db65ea --- /dev/null +++ b/packages/trueforge/src/db/postgres/sandbox-environment-store/PostgresSandboxEnvironmentStore.ts @@ -0,0 +1,157 @@ +import { CreatedBySubjectSchema, type TokenPagination } from '@truefoundry/trueforge-core/agent-session'; +import { + decodeOffsetPageToken, + paginateOffsetRows, +} from '@truefoundry/trueforge-core/agent-session/store/OffsetPageToken'; +import { sql, type Kysely, type Selectable, type Transaction } from 'kysely'; +import { newId } from '../../../utils/id'; +import { + parseStoredSandboxEnvironmentManifest, + SandboxEnvironmentNameConflictError, + type CreateSandboxEnvironmentInput, + type DeleteSandboxEnvironmentInput, + type GetOwnedIdsInput, + type GetSandboxEnvironmentInput, + type ISandboxEnvironmentStore, + type ListSandboxEnvironmentsInput, + type SandboxEnvironmentRecord, + type UpdateSandboxEnvironmentInput, +} from '../../sandboxEnvironmentStore'; +import { isUniqueViolation } from '../client'; +import { json, now } from '../sqlExpressions'; +import type { Database, SandboxEnvironmentTable } from '../types'; + +function toRecord(row: Selectable): SandboxEnvironmentRecord { + return { + id: row.id, + tenant_id: row.tenant_id, + name: row.name, + description: row.description, + manifest: parseStoredSandboxEnvironmentManifest(row.manifest), + created_by_subject: CreatedBySubjectSchema.parse(row.created_by_subject), + created_at: row.created_at.toISOString(), + updated_at: row.updated_at.toISOString(), + }; +} + +export class PostgresSandboxEnvironmentStore implements ISandboxEnvironmentStore> { + readonly #db: Kysely; + + constructor(db: Kysely) { + this.#db = db; + } + + async createSandboxEnvironment( + input: CreateSandboxEnvironmentInput, + transaction?: Transaction, + ): Promise { + const db = transaction ?? this.#db; + try { + const row = await db + .insertInto('sandbox_environment') + .values({ + id: newId(), + tenant_id: input.tenant_id, + name: input.name, + description: input.description, + manifest: json(input.manifest), + created_by_subject: json(input.created_by_subject), + created_at: now(), + updated_at: now(), + }) + .returningAll() + .executeTakeFirstOrThrow(); + return toRecord(row); + } catch (error) { + if (isUniqueViolation(error)) { + throw new SandboxEnvironmentNameConflictError( + { tenant_id: input.tenant_id, name: input.name }, + { cause: error }, + ); + } + throw error; + } + } + + async getSandboxEnvironment( + input: GetSandboxEnvironmentInput, + transaction?: Transaction, + ): Promise { + const db = transaction ?? this.#db; + let query = db.selectFrom('sandbox_environment').selectAll().where('tenant_id', '=', input.tenant_id); + if ('id' in input) { + query = query.where('id', '=', input.id); + } else { + query = query.where('name', '=', input.name); + } + const row = await query.executeTakeFirst(); + return row === undefined ? undefined : toRecord(row); + } + + async listSandboxEnvironments( + input: ListSandboxEnvironmentsInput, + transaction?: Transaction, + ): Promise<{ data: SandboxEnvironmentRecord[]; pagination: TokenPagination }> { + const offset = decodeOffsetPageToken(input.page_token); + const db = transaction ?? this.#db; + const rows = await db + .selectFrom('sandbox_environment') + .selectAll() + .where('tenant_id', '=', input.tenant_id) + .where(sql`created_by_subject->>'subject_id'`, '=', input.created_by_subject_id) + .orderBy('created_at', 'desc') + .orderBy('id') + .limit(input.limit + 1) + .offset(offset) + .execute(); + const { data, pagination } = paginateOffsetRows(rows, input.limit, offset); + return { data: data.map(toRecord), pagination }; + } + + async updateSandboxEnvironment( + input: UpdateSandboxEnvironmentInput, + transaction?: Transaction, + ): Promise { + const db = transaction ?? this.#db; + const row = await db + .updateTable('sandbox_environment') + .set({ + ...(input.description === undefined ? {} : { description: input.description }), + manifest: json(input.manifest), + updated_at: now(), + }) + .where('tenant_id', '=', input.tenant_id) + .where('id', '=', input.id) + .returningAll() + .executeTakeFirst(); + return row === undefined ? undefined : toRecord(row); + } + + async deleteSandboxEnvironment( + input: DeleteSandboxEnvironmentInput, + transaction?: Transaction, + ): Promise { + const db = transaction ?? this.#db; + const result = await db + .deleteFrom('sandbox_environment') + .where('tenant_id', '=', input.tenant_id) + .where('id', '=', input.id) + .executeTakeFirst(); + return Number(result.numDeletedRows) > 0; + } + + async getOwnedIds(input: GetOwnedIdsInput, transaction?: Transaction): Promise { + if (input.ids.length === 0) { + return []; + } + const db = transaction ?? this.#db; + const rows = await db + .selectFrom('sandbox_environment') + .select('id') + .where('tenant_id', '=', input.tenant_id) + .where('id', 'in', [...input.ids]) + .where(sql`created_by_subject->>'subject_id'`, '=', input.subject_id) + .execute(); + return rows.map(row => row.id); + } +} diff --git a/packages/trueforge/src/db/postgres/types.ts b/packages/trueforge/src/db/postgres/types.ts index 7723013e9..edd5c5c61 100644 --- a/packages/trueforge/src/db/postgres/types.ts +++ b/packages/trueforge/src/db/postgres/types.ts @@ -25,6 +25,7 @@ import type { CurrentContextUsage } from '@truefoundry/trueforge-core/core/runti import type { ColumnType, Generated, JSONColumnType } from 'kysely'; import type { McpServerManifest } from '../../schemas/mcpServer'; import type { ModelProviderManifest } from '../../schemas/modelProvider'; +import type { StoredSandboxEnvironmentManifest } from '../../schemas/sandboxEnvironment'; import type { SandboxBuildMetadata, SandboxBuildStatus, @@ -377,6 +378,25 @@ export interface SandboxProviderTable { updated_at: Date; } +/** + * Configured sandbox environments — immutable ULID `id` PK; UNIQUE (tenant_id, name). + * PRIMARY KEY (id) + */ +export interface SandboxEnvironmentTable { + id: string; + tenant_id: string; + name: string; + description: string | null; + manifest: JSONColumnType< + StoredSandboxEnvironmentManifest, + StoredSandboxEnvironmentManifest, + StoredSandboxEnvironmentManifest + >; + created_by_subject: JSONColumnType; + created_at: Date; + updated_at: Date; +} + /** * Configured agents — immutable ULID `id` PK; UNIQUE immutable (tenant_id, name). * PRIMARY KEY (id) @@ -527,6 +547,7 @@ export interface Database { model_provider: ModelProviderTable; skill: SkillTable; sandbox_provider: SandboxProviderTable; + sandbox_environment: SandboxEnvironmentTable; agent: AgentTable; schedule: ScheduleTable; schedule_run: ScheduleRunTable; diff --git a/packages/trueforge/src/db/sandboxEnvironmentStore.ts b/packages/trueforge/src/db/sandboxEnvironmentStore.ts new file mode 100644 index 000000000..05a58c5e0 --- /dev/null +++ b/packages/trueforge/src/db/sandboxEnvironmentStore.ts @@ -0,0 +1,97 @@ +/** + * DB-backed sandbox environments: owned per creator within a tenant. + * Immutable ULID `id`, unique `name` within a tenant, Zod-validated manifest jsonb. + * Implementations: PostgresSandboxEnvironmentStore and SqliteSandboxEnvironmentStore. + */ +import type { CreatedBySubject, TokenPagination } from '@truefoundry/trueforge-core/agent-session'; +import type { ResourceName } from '../schemas/common'; +import { + StoredSandboxEnvironmentManifestSchema, + type StoredSandboxEnvironmentManifest, +} from '../schemas/sandboxEnvironment'; + +/** Re-parse persisted manifest JSON so schema defaults materialize for older rows. */ +export function parseStoredSandboxEnvironmentManifest(manifest: unknown): StoredSandboxEnvironmentManifest { + return StoredSandboxEnvironmentManifestSchema.parse(manifest); +} + +export interface SandboxEnvironmentRecord { + id: string; + tenant_id: string; + name: ResourceName; + description: string | null; + manifest: StoredSandboxEnvironmentManifest; + created_by_subject: CreatedBySubject; + /** ISO-8601 UTC instant. */ + created_at: string; + /** ISO-8601 UTC instant. */ + updated_at: string; +} + +export type GetSandboxEnvironmentInput = { tenant_id: string } & ({ id: string } | { name: string }); + +export interface ListSandboxEnvironmentsInput { + tenant_id: string; + created_by_subject_id: string; + limit: number; + page_token: string | undefined; +} + +export interface GetOwnedIdsInput { + tenant_id: string; + ids: readonly string[]; + subject_id: string; +} + +export interface CreateSandboxEnvironmentInput { + tenant_id: string; + name: ResourceName; + description: string | null; + manifest: StoredSandboxEnvironmentManifest; + created_by_subject: CreatedBySubject; +} + +export interface UpdateSandboxEnvironmentInput { + tenant_id: string; + id: string; + description: string | null | undefined; + manifest: StoredSandboxEnvironmentManifest; +} + +export interface DeleteSandboxEnvironmentInput { + tenant_id: string; + id: string; +} + +export class SandboxEnvironmentNameConflictError extends Error { + readonly tenant_id: string; + readonly conflict_name: string; + + constructor(input: { tenant_id: string; name: string }, options?: ErrorOptions) { + super(`Sandbox environment name "${input.name}" already exists`, options); + this.name = 'SandboxEnvironmentNameConflictError'; + this.tenant_id = input.tenant_id; + this.conflict_name = input.name; + } +} + +export interface ISandboxEnvironmentStore { + createSandboxEnvironment( + input: CreateSandboxEnvironmentInput, + transaction?: TTransaction, + ): Promise; + getSandboxEnvironment( + input: GetSandboxEnvironmentInput, + transaction?: TTransaction, + ): Promise; + listSandboxEnvironments( + input: ListSandboxEnvironmentsInput, + transaction?: TTransaction, + ): Promise<{ data: SandboxEnvironmentRecord[]; pagination: TokenPagination }>; + updateSandboxEnvironment( + input: UpdateSandboxEnvironmentInput, + transaction?: TTransaction, + ): Promise; + deleteSandboxEnvironment(input: DeleteSandboxEnvironmentInput, transaction?: TTransaction): Promise; + getOwnedIds(input: GetOwnedIdsInput, transaction?: TTransaction): Promise; +} diff --git a/packages/trueforge/src/db/sqlite/agent-store/SqliteAgentStore.ts b/packages/trueforge/src/db/sqlite/agent-store/SqliteAgentStore.ts index 27b1daa6a..117c4b8fd 100644 --- a/packages/trueforge/src/db/sqlite/agent-store/SqliteAgentStore.ts +++ b/packages/trueforge/src/db/sqlite/agent-store/SqliteAgentStore.ts @@ -22,6 +22,7 @@ import { type GetExternalIdsByIdsInput, type GetOwnedIdsInput, type IAgentStore, + type ListAgentIdsUsingSandboxEnvironmentInput, type ListAgentsInput, type UpdateAgentInput, } from '../../agentStore'; @@ -213,6 +214,20 @@ export class SqliteAgentStore implements IAgentStore> { await db.deleteFrom('agent').where('tenant_id', '=', input.tenant_id).where('id', '=', input.id).execute(); } + async listAgentIdsUsingSandboxEnvironment( + input: ListAgentIdsUsingSandboxEnvironmentInput, + transaction?: Transaction, + ): Promise { + const db = transaction ?? this.#db; + const rows = await db + .selectFrom('agent') + .select('id') + .where('tenant_id', '=', input.tenant_id) + .where(sql`json_extract(manifest, '$.config.sandbox.environment')`, '=', input.environment_name) + .execute(); + return rows.map(row => row.id); + } + /** Map unique violations to external_id vs name conflicts. */ private async throwCreateUnique({ error, diff --git a/packages/trueforge/src/db/sqlite/migrations/20260918_000003_sandbox_environment.ts b/packages/trueforge/src/db/sqlite/migrations/20260918_000003_sandbox_environment.ts new file mode 100644 index 000000000..45e71fe2a --- /dev/null +++ b/packages/trueforge/src/db/sqlite/migrations/20260918_000003_sandbox_environment.ts @@ -0,0 +1,45 @@ +import { sql, type Kysely } from 'kysely'; +import { SANDBOX_ENVIRONMENT_CREATED_BY_SUBJECT_ID_IDX, SANDBOX_ENVIRONMENT_TENANT_NAME_UQ } from '../../indexes'; + +/** + * Owned sandbox environments for SQLite — mirrors + * db/postgres/migrations/20260918_000002_sandbox_environment.ts. + * + * SQLite differences: `manifest` / `created_by_subject` are BLOB JSONB, timestamps + * are ISO TEXT, table is STRICT. + */ +export async function up(db: Kysely): Promise { + await db.transaction().execute(async trx => { + await sql` + CREATE TABLE sandbox_environment ( + id TEXT NOT NULL, + tenant_id TEXT NOT NULL, + name TEXT NOT NULL, + description TEXT, + manifest BLOB NOT NULL, + created_by_subject BLOB NOT NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + PRIMARY KEY (id) + ) STRICT + `.execute(trx); + + await sql` + CREATE UNIQUE INDEX ${sql.raw(SANDBOX_ENVIRONMENT_TENANT_NAME_UQ)} + ON sandbox_environment (tenant_id, name) + `.execute(trx); + + await sql` + CREATE INDEX ${sql.raw(SANDBOX_ENVIRONMENT_CREATED_BY_SUBJECT_ID_IDX)} + ON sandbox_environment (tenant_id, json_extract(created_by_subject, '$.subject_id')) + `.execute(trx); + }); +} + +export async function down(db: Kysely): Promise { + await db.transaction().execute(async trx => { + await sql`DROP INDEX IF EXISTS ${sql.raw(SANDBOX_ENVIRONMENT_CREATED_BY_SUBJECT_ID_IDX)}`.execute(trx); + await sql`DROP INDEX IF EXISTS ${sql.raw(SANDBOX_ENVIRONMENT_TENANT_NAME_UQ)}`.execute(trx); + await sql`DROP TABLE IF EXISTS sandbox_environment`.execute(trx); + }); +} diff --git a/packages/trueforge/src/db/sqlite/sandbox-environment-store/SqliteSandboxEnvironmentStore.ts b/packages/trueforge/src/db/sqlite/sandbox-environment-store/SqliteSandboxEnvironmentStore.ts new file mode 100644 index 000000000..379704fee --- /dev/null +++ b/packages/trueforge/src/db/sqlite/sandbox-environment-store/SqliteSandboxEnvironmentStore.ts @@ -0,0 +1,185 @@ +import { + CreatedBySubjectSchema, + type CreatedBySubject, + type TokenPagination, +} from '@truefoundry/trueforge-core/agent-session'; +import { + decodeOffsetPageToken, + paginateOffsetRows, +} from '@truefoundry/trueforge-core/agent-session/store/OffsetPageToken'; +import { sql, type ExpressionBuilder, type Kysely, type Transaction } from 'kysely'; +import type { StoredSandboxEnvironmentManifest } from '../../../schemas/sandboxEnvironment'; +import { newId } from '../../../utils/id'; +import { + parseStoredSandboxEnvironmentManifest, + SandboxEnvironmentNameConflictError, + type CreateSandboxEnvironmentInput, + type DeleteSandboxEnvironmentInput, + type GetOwnedIdsInput, + type GetSandboxEnvironmentInput, + type ISandboxEnvironmentStore, + type ListSandboxEnvironmentsInput, + type SandboxEnvironmentRecord, + type UpdateSandboxEnvironmentInput, +} from '../../sandboxEnvironmentStore'; +import { isUniqueViolation } from '../client'; +import { jsonbBind, jsonText, nowIso } from '../sqlExpressions'; +import type { Database } from '../types'; + +function environmentColumns(eb: ExpressionBuilder) { + return [ + 'id' as const, + 'tenant_id' as const, + 'name' as const, + 'description' as const, + jsonText(eb.ref('manifest')).as('manifest'), + jsonText(eb.ref('created_by_subject')).as('created_by_subject'), + 'created_at' as const, + 'updated_at' as const, + ]; +} + +interface SandboxEnvironmentRow { + id: string; + tenant_id: string; + name: string; + description: string | null; + manifest: StoredSandboxEnvironmentManifest; + created_by_subject: CreatedBySubject; + created_at: string; + updated_at: string; +} + +function toRecord(row: SandboxEnvironmentRow): SandboxEnvironmentRecord { + return { + ...row, + manifest: parseStoredSandboxEnvironmentManifest(row.manifest), + created_by_subject: CreatedBySubjectSchema.parse(row.created_by_subject), + }; +} + +export class SqliteSandboxEnvironmentStore implements ISandboxEnvironmentStore> { + readonly #db: Kysely; + + constructor(db: Kysely) { + this.#db = db; + } + + async createSandboxEnvironment( + input: CreateSandboxEnvironmentInput, + transaction?: Transaction, + ): Promise { + const db = transaction ?? this.#db; + const timestamp = nowIso(); + try { + const row = await db + .insertInto('sandbox_environment') + .values({ + id: newId(), + tenant_id: input.tenant_id, + name: input.name, + description: input.description, + manifest: jsonbBind(input.manifest), + created_by_subject: jsonbBind(input.created_by_subject), + created_at: timestamp, + updated_at: timestamp, + }) + .returning(environmentColumns) + .executeTakeFirstOrThrow(); + return toRecord(row); + } catch (error) { + if (isUniqueViolation(error)) { + throw new SandboxEnvironmentNameConflictError( + { tenant_id: input.tenant_id, name: input.name }, + { cause: error }, + ); + } + throw error; + } + } + + async getSandboxEnvironment( + input: GetSandboxEnvironmentInput, + transaction?: Transaction, + ): Promise { + const db = transaction ?? this.#db; + let query = db + .selectFrom('sandbox_environment') + .select(environmentColumns) + .where('tenant_id', '=', input.tenant_id); + if ('id' in input) { + query = query.where('id', '=', input.id); + } else { + query = query.where('name', '=', input.name); + } + const row = await query.executeTakeFirst(); + return row === undefined ? undefined : toRecord(row); + } + + async listSandboxEnvironments( + input: ListSandboxEnvironmentsInput, + transaction?: Transaction, + ): Promise<{ data: SandboxEnvironmentRecord[]; pagination: TokenPagination }> { + const offset = decodeOffsetPageToken(input.page_token); + const db = transaction ?? this.#db; + const rows = await db + .selectFrom('sandbox_environment') + .select(environmentColumns) + .where('tenant_id', '=', input.tenant_id) + .where(sql`json_extract(created_by_subject, '$.subject_id')`, '=', input.created_by_subject_id) + .orderBy('created_at', 'desc') + .orderBy('id') + .limit(input.limit + 1) + .offset(offset) + .execute(); + const { data, pagination } = paginateOffsetRows(rows, input.limit, offset); + return { data: data.map(toRecord), pagination }; + } + + async updateSandboxEnvironment( + input: UpdateSandboxEnvironmentInput, + transaction?: Transaction, + ): Promise { + const db = transaction ?? this.#db; + const row = await db + .updateTable('sandbox_environment') + .set({ + ...(input.description === undefined ? {} : { description: input.description }), + manifest: jsonbBind(input.manifest), + updated_at: nowIso(), + }) + .where('tenant_id', '=', input.tenant_id) + .where('id', '=', input.id) + .returning(environmentColumns) + .executeTakeFirst(); + return row === undefined ? undefined : toRecord(row); + } + + async deleteSandboxEnvironment( + input: DeleteSandboxEnvironmentInput, + transaction?: Transaction, + ): Promise { + const db = transaction ?? this.#db; + const result = await db + .deleteFrom('sandbox_environment') + .where('tenant_id', '=', input.tenant_id) + .where('id', '=', input.id) + .executeTakeFirst(); + return Number(result.numDeletedRows) > 0; + } + + async getOwnedIds(input: GetOwnedIdsInput, transaction?: Transaction): Promise { + if (input.ids.length === 0) { + return []; + } + const db = transaction ?? this.#db; + const rows = await db + .selectFrom('sandbox_environment') + .select('id') + .where('tenant_id', '=', input.tenant_id) + .where('id', 'in', [...input.ids]) + .where(sql`json_extract(created_by_subject, '$.subject_id')`, '=', input.subject_id) + .execute(); + return rows.map(row => row.id); + } +} diff --git a/packages/trueforge/src/db/sqlite/types.ts b/packages/trueforge/src/db/sqlite/types.ts index e19c679d4..2a4ad4fa5 100644 --- a/packages/trueforge/src/db/sqlite/types.ts +++ b/packages/trueforge/src/db/sqlite/types.ts @@ -28,6 +28,7 @@ import type { CurrentContextUsage } from '@truefoundry/trueforge-core/core/runti import type { ColumnType, Generated, JSONColumnType } from 'kysely'; import type { McpServerManifest } from '../../schemas/mcpServer'; import type { ModelProviderManifest } from '../../schemas/modelProvider'; +import type { StoredSandboxEnvironmentManifest } from '../../schemas/sandboxEnvironment'; import type { SandboxBuildMetadata, SandboxBuildStatus, @@ -221,6 +222,22 @@ export interface SandboxProviderTable { updated_at: string; } +/** + * Configured sandbox environments — mirrors the Postgres `sandbox_environment` table. + * PRIMARY KEY (id); UNIQUE (tenant_id, name). + */ +export interface SandboxEnvironmentTable { + id: string; + tenant_id: string; + name: string; + description: string | null; + /** StoredSandboxEnvironmentManifest document; replaced whole on every update */ + manifest: JsonbColumn; + created_by_subject: JsonbColumn; + created_at: string; + updated_at: string; +} + /** * Configured agents — mirrors the Postgres `agent` table. * PRIMARY KEY (id); UNIQUE (tenant_id, name). @@ -347,6 +364,7 @@ export interface Database { model_provider: ModelProviderTable; skill: SkillTable; sandbox_provider: SandboxProviderTable; + sandbox_environment: SandboxEnvironmentTable; agent: AgentTable; schedule: ScheduleTable; schedule_run: ScheduleRunTable; diff --git a/packages/trueforge/src/main.ts b/packages/trueforge/src/main.ts index 944500498..d1a405ef3 100644 --- a/packages/trueforge/src/main.ts +++ b/packages/trueforge/src/main.ts @@ -73,6 +73,7 @@ import { McpServerWithAuthStore } from './db/McpServerWithAuthStore'; import type { IModelProviderStore } from './db/modelProviderStore'; import type { PostgresAgentStore } from './db/postgres/agent-store/PostgresAgentStore'; import type { Database as PostgresDatabase } from './db/postgres/types'; +import type { ISandboxEnvironmentStore } from './db/sandboxEnvironmentStore'; import type { ISandboxProviderStore } from './db/sandboxProviderStore'; import type { IScheduleStore } from './db/scheduleStore'; import type { ISessionMetricsStore } from './db/sessionMetricsStore'; @@ -118,6 +119,7 @@ interface ServerPersistence { perServerHeaders?: PerServerMcpHeaders, ) => IMcpServerWithAuthStore; resolveSandboxProviderStore: (rc: RequestContext) => ISandboxProviderStore; + resolveSandboxEnvironmentStore: (rc: RequestContext) => ISandboxEnvironmentStore; /** Per-request store: DB git skills, or TrueFoundry registry catalog in TrueFoundry mode. */ resolveSkillStore: (rc: RequestContext) => ISkillStore; resolveAgentStore: (rc: RequestContext) => IAgentStore; @@ -292,6 +294,7 @@ async function createStandalonePersistence(options: { import('./db/sqlite/token-store/SqliteOAuthTokenStore'), import('./db/sqlite/skill-store/SqliteSkillStore'), import('./db/sqlite/sandbox-provider-store/SqliteSandboxProviderStore'), + import('./db/sqlite/sandbox-environment-store/SqliteSandboxEnvironmentStore'), import('./db/sqlite/agent-store/SqliteAgentStore'), import('./db/sqlite/schedule-store/SqliteScheduleStore'), ]), @@ -304,6 +307,7 @@ async function createStandalonePersistence(options: { { SqliteOAuthTokenStore }, { SqliteSkillStore }, { SqliteSandboxProviderStore }, + { SqliteSandboxEnvironmentStore }, { SqliteAgentStore }, { SqliteScheduleStore }, ] = sqliteStores; @@ -322,6 +326,7 @@ async function createStandalonePersistence(options: { clientName: configuration.MCP_DCR_OAUTH_CLIENT_NAME, }); const sandboxProviderStore = new SqliteSandboxProviderStore(db); + const sandboxEnvironmentStore = new SqliteSandboxEnvironmentStore(db); const skillStore = new SqliteSkillStore(db); return { withTransaction: callback => db.transaction().execute(callback), @@ -333,6 +338,7 @@ async function createStandalonePersistence(options: { resolveModelProviderStore: () => modelProviderStore, resolveMcpServerStore: () => mcpServerStore, resolveSandboxProviderStore: () => sandboxProviderStore, + resolveSandboxEnvironmentStore: () => sandboxEnvironmentStore, resolveSkillStore: () => skillStore, resolveAgentStore: () => agentStore, resolveImportAgentStore: () => agentStore, @@ -372,6 +378,7 @@ async function createDistributedPersistence(options: { import('./db/postgres/token-store/PostgresOAuthTokenStore'), import('./db/postgres/skill-store/PostgresSkillStore'), import('./db/postgres/sandbox-provider-store/PostgresSandboxProviderStore'), + import('./db/postgres/sandbox-environment-store/PostgresSandboxEnvironmentStore'), import('./db/postgres/agent-store/PostgresAgentStore'), import('./db/postgres/schedule-store/PostgresScheduleStore'), ]), @@ -384,6 +391,7 @@ async function createDistributedPersistence(options: { { PostgresOAuthTokenStore }, { PostgresSkillStore }, { PostgresSandboxProviderStore }, + { PostgresSandboxEnvironmentStore }, { PostgresAgentStore }, { PostgresScheduleStore }, ] = postgresStores; @@ -408,6 +416,7 @@ async function createDistributedPersistence(options: { clientName: configuration.MCP_DCR_OAUTH_CLIENT_NAME, }); const sandboxProviderStore = new PostgresSandboxProviderStore(db); + const sandboxEnvironmentStore = new PostgresSandboxEnvironmentStore(db); const skillStore = new PostgresSkillStore(db); const agentStore = new PostgresAgentStore(db); const turnSkillsResolverStore = buildTurnSkillsResolverStore({ @@ -452,6 +461,7 @@ async function createDistributedPersistence(options: { const resolveSandboxProviderStore = buildResolveSandboxProviderStore({ persistenceStore: sandboxProviderStore, }); + const resolveSandboxEnvironmentStore = () => sandboxEnvironmentStore; const resolveSkillStore = buildResolveSkillStore({ persistenceStore: skillStore, client: serviceFoundryClient, @@ -466,6 +476,7 @@ async function createDistributedPersistence(options: { resolveModelProviderStore, resolveMcpServerStore, resolveSandboxProviderStore, + resolveSandboxEnvironmentStore, resolveSkillStore, resolveAgentStore, resolveImportAgentStore, @@ -561,6 +572,8 @@ async function createServerRuntime(persistence: ServerPersistence< }; const resolveAgentStore = (c: Context) => persistence.resolveAgentStore(resolveRequestContext(c)); const resolveSandboxProviderStore = (c: Context) => persistence.resolveSandboxProviderStore(resolveRequestContext(c)); + const resolveSandboxEnvironmentStore = (c: Context) => + persistence.resolveSandboxEnvironmentStore(resolveRequestContext(c)); const resolveSkillStore = (c: Context) => { const store = persistence.resolveSkillStore(resolveRequestContext(c)); if (!isTrueFoundryModeEnabled(configuration)) { @@ -582,6 +595,7 @@ async function createServerRuntime(persistence: ServerPersistence< resolveAgentStore, resolveImportAgentStore, resolveSandboxProviderStore, + resolveSandboxEnvironmentStore, resolveSkillStore, withTransaction, tokenStore, diff --git a/packages/trueforge/src/routes/sandboxEnvironmentRoutes.ts b/packages/trueforge/src/routes/sandboxEnvironmentRoutes.ts new file mode 100644 index 000000000..63b2fb099 --- /dev/null +++ b/packages/trueforge/src/routes/sandboxEnvironmentRoutes.ts @@ -0,0 +1,197 @@ +/** + * Sandbox environment route definitions (mounted at /api/v1/sandbox-environments). + * Handlers are registered in apis/sandboxEnvironments.ts. + */ +import { createRoute, z } from '@hono/zod-openapi'; +import { PAGE_LIMIT } from '../schemas/common'; +import { RequestErrorResponseSchema } from '../schemas/errors'; +import { + CreateSandboxEnvironmentRequestSchema, + DeleteSandboxEnvironmentResponseSchema, + GetSandboxEnvironmentResponseSchema, + ListSandboxEnvironmentsResponseSchema, + UpdateSandboxEnvironmentRequestSchema, +} from '../schemas/sandboxEnvironment'; +import { TOKEN_PAGINATION } from './fernExtensions'; +import { OpenApiTag } from './openapiTags'; + +export const SandboxEnvironmentIdParamsSchema = z.object({ + sandbox_environment_id: z.string().min(1).max(64).describe('Immutable sandbox environment identifier.'), +}); + +export const ListSandboxEnvironmentsQuerySchema = z + .object({ + limit: z.coerce + .number() + .int() + .min(1) + .max(PAGE_LIMIT) + .optional() + .default(PAGE_LIMIT) + .describe(`Page size. Defaults to ${String(PAGE_LIMIT)}`), + page_token: z.string().optional().describe('Opaque token from a previous response `next_page_token`.'), + }) + .openapi('ListSandboxEnvironmentsQuery'); + +export const listSandboxEnvironmentsRoute = createRoute({ + method: 'get', + path: '/', + tags: [OpenApiTag.SANDBOXES], + summary: 'List sandbox environments', + description: 'List sandbox environments created by the authenticated subject, newest first.', + 'x-fern-sdk-group-name': ['sandbox_environments'], + 'x-fern-sdk-method-name': 'list', + 'x-fern-pagination': TOKEN_PAGINATION, + request: { + query: ListSandboxEnvironmentsQuerySchema, + }, + responses: { + 200: { + content: { 'application/json': { schema: ListSandboxEnvironmentsResponseSchema } }, + description: 'Paginated caller-owned sandbox environments.', + }, + 400: { + content: { 'application/json': { schema: RequestErrorResponseSchema } }, + description: 'Invalid query parameters or page token.', + }, + 401: { + content: { 'application/json': { schema: RequestErrorResponseSchema } }, + description: 'Unauthenticated.', + }, + }, +}); + +export const createSandboxEnvironmentRoute = createRoute({ + method: 'post', + path: '/', + tags: [OpenApiTag.SANDBOXES], + summary: 'Create a sandbox environment', + description: 'Create a Daytona sandbox environment owned by the authenticated subject.', + 'x-fern-sdk-group-name': ['sandbox_environments'], + 'x-fern-sdk-method-name': 'create', + request: { + body: { + content: { 'application/json': { schema: CreateSandboxEnvironmentRequestSchema } }, + required: true, + }, + }, + responses: { + 201: { + content: { 'application/json': { schema: GetSandboxEnvironmentResponseSchema } }, + description: 'Created sandbox environment.', + }, + 400: { + content: { 'application/json': { schema: RequestErrorResponseSchema } }, + description: 'Invalid request body.', + }, + 401: { + content: { 'application/json': { schema: RequestErrorResponseSchema } }, + description: 'Unauthenticated.', + }, + 409: { + content: { 'application/json': { schema: RequestErrorResponseSchema } }, + description: 'Name already exists in the tenant.', + }, + 422: { + content: { 'application/json': { schema: RequestErrorResponseSchema } }, + description: 'Provider, snapshot, or secret validation failed.', + }, + }, +}); + +export const getSandboxEnvironmentRoute = createRoute({ + method: 'get', + path: '/{sandbox_environment_id}', + tags: [OpenApiTag.SANDBOXES], + summary: 'Get a sandbox environment', + description: 'Get a sandbox environment owned by the authenticated subject.', + 'x-fern-sdk-group-name': ['sandbox_environments'], + 'x-fern-sdk-method-name': 'get', + request: { + params: SandboxEnvironmentIdParamsSchema, + }, + responses: { + 200: { + content: { 'application/json': { schema: GetSandboxEnvironmentResponseSchema } }, + description: 'The sandbox environment.', + }, + 401: { + content: { 'application/json': { schema: RequestErrorResponseSchema } }, + description: 'Unauthenticated.', + }, + 404: { + content: { 'application/json': { schema: RequestErrorResponseSchema } }, + description: 'Not found or you do not have access.', + }, + }, +}); + +export const putSandboxEnvironmentRoute = createRoute({ + method: 'put', + path: '/{sandbox_environment_id}', + tags: [OpenApiTag.SANDBOXES], + summary: 'Update a sandbox environment', + description: 'Replace the manifest (and optionally description). Name is immutable.', + 'x-fern-sdk-group-name': ['sandbox_environments'], + 'x-fern-sdk-method-name': 'update', + request: { + params: SandboxEnvironmentIdParamsSchema, + body: { + content: { 'application/json': { schema: UpdateSandboxEnvironmentRequestSchema } }, + required: true, + }, + }, + responses: { + 200: { + content: { 'application/json': { schema: GetSandboxEnvironmentResponseSchema } }, + description: 'Updated sandbox environment.', + }, + 400: { + content: { 'application/json': { schema: RequestErrorResponseSchema } }, + description: 'Invalid request body.', + }, + 401: { + content: { 'application/json': { schema: RequestErrorResponseSchema } }, + description: 'Unauthenticated.', + }, + 404: { + content: { 'application/json': { schema: RequestErrorResponseSchema } }, + description: 'Not found or you do not have access.', + }, + 422: { + content: { 'application/json': { schema: RequestErrorResponseSchema } }, + description: 'Provider, snapshot, or secret validation failed.', + }, + }, +}); + +export const deleteSandboxEnvironmentRoute = createRoute({ + method: 'delete', + path: '/{sandbox_environment_id}', + tags: [OpenApiTag.SANDBOXES], + summary: 'Delete a sandbox environment', + description: 'Delete a caller-owned sandbox environment. Fails if any agent references it.', + 'x-fern-sdk-group-name': ['sandbox_environments'], + 'x-fern-sdk-method-name': 'delete', + request: { + params: SandboxEnvironmentIdParamsSchema, + }, + responses: { + 200: { + content: { 'application/json': { schema: DeleteSandboxEnvironmentResponseSchema } }, + description: 'Deleted.', + }, + 401: { + content: { 'application/json': { schema: RequestErrorResponseSchema } }, + description: 'Unauthenticated.', + }, + 404: { + content: { 'application/json': { schema: RequestErrorResponseSchema } }, + description: 'Not found or you do not have access.', + }, + 409: { + content: { 'application/json': { schema: RequestErrorResponseSchema } }, + description: 'Environment is referenced by one or more agents.', + }, + }, +}); diff --git a/packages/trueforge/src/runtime/sessionResources.ts b/packages/trueforge/src/runtime/sessionResources.ts index c1287be3e..d34df8b6f 100644 --- a/packages/trueforge/src/runtime/sessionResources.ts +++ b/packages/trueforge/src/runtime/sessionResources.ts @@ -14,9 +14,10 @@ import { HTTPException } from 'hono/http-exception'; import { join } from 'node:path'; import type { Logger } from 'winston'; import { z } from 'zod'; -import configuration from '../config'; +import configuration, { isTrueFoundryModeEnabled } from '../config'; import type { IMcpServerStore, IMcpServerWithAuthStore } from '../db/mcpServerStore'; import type { IModelProviderStore } from '../db/modelProviderStore'; +import type { ISandboxEnvironmentStore } from '../db/sandboxEnvironmentStore'; import type { ISandboxProviderStore } from '../db/sandboxProviderStore'; import type { ISkillStore } from '../db/skillStore'; import { LocalSandboxProvider } from '../sandbox/local/provider/LocalSandboxProvider'; @@ -290,17 +291,21 @@ export function buildTurnSandbox(input: { export async function validateAgentSpec({ spec, tenant_id, + subject_id, modelProviderStore, mcpServerStore, skillStore, sandboxProviderStore, + sandboxEnvironmentStore, }: { spec: AgentSpec; tenant_id: string; + subject_id: string; modelProviderStore: IModelProviderStore; mcpServerStore: IMcpServerStore; skillStore: ISkillStore; sandboxProviderStore: ISandboxProviderStore; + sandboxEnvironmentStore: ISandboxEnvironmentStore; }): Promise { const resolved = await getModelDetails({ tenant_id, @@ -345,6 +350,20 @@ export async function validateAgentSpec({ const wantsSandbox = spec.config.sandbox.enabled; const hasSkills = requestedSkills.length > 0; + const environmentName = spec.config.sandbox.environment; + if (environmentName) { + if (!wantsSandbox) { + throw new HTTPException(422, { + message: 'sandbox.environment requires sandbox.enabled to be true', + }); + } + if (isTrueFoundryModeEnabled(configuration)) { + throw new HTTPException(422, { + message: 'sandbox.environment is not supported in TrueFoundry mode', + }); + } + } + if (wantsSandbox || hasSkills) { const record = await sandboxProviderStore.getSandboxProvider(tenant_id); if (record === undefined && !isLocalSandboxFallbackEnabled()) { @@ -354,9 +373,30 @@ export async function validateAgentSpec({ : 'sandbox is enabled but no sandbox provider is configured — PUT /settings/sandbox-providers', }); } + if (environmentName) { + if (record?.manifest.type !== 'daytona') { + throw new HTTPException(422, { + message: 'sandbox.environment requires a configured Daytona sandbox provider', + }); + } + const environment = await sandboxEnvironmentStore.getSandboxEnvironment({ + tenant_id, + name: environmentName, + }); + if (environment?.created_by_subject.subject_id !== subject_id) { + throw new HTTPException(422, { + message: `Unknown sandbox environment "${environmentName}" — not found or not owned by the caller`, + }); + } + if (environment.manifest.provider !== record.name) { + throw new HTTPException(422, { + message: `Sandbox environment "${environmentName}" provider does not match the configured sandbox provider`, + }); + } + } } - if (spec.config.web_search.enabled && resolveWebSearchProvider() === undefined) { + if (spec.config.web_search.enabled && !resolveWebSearchProvider()) { throw new HTTPException(422, { message: 'web_search is enabled but no web-search provider is configured', }); diff --git a/packages/trueforge/src/schemas/permissions.ts b/packages/trueforge/src/schemas/permissions.ts index 68e91cd04..3825ebece 100644 --- a/packages/trueforge/src/schemas/permissions.ts +++ b/packages/trueforge/src/schemas/permissions.ts @@ -46,7 +46,7 @@ export const ListPermissionsDataSchema = z permissions: z .record(z.string(), z.array(ResourcePermissionSchema)) .describe( - 'For agent/schedule/session: keyed by resource id. For tenant: keyed by entity kind (e.g. `agent` → `CREATE`).', + 'For agent/schedule/session: keyed by resource id. For tenant: keyed by entity kind (e.g. `agent` or `sandbox-environment` → `CREATE`).', ), }) .openapi('ListPermissionsData'); @@ -72,6 +72,9 @@ export const AGENT_OWNER_PERMISSIONS = AgentResourcePermissionSchema.options; export const SCHEDULE_OWNER_PERMISSIONS = ScheduleResourcePermissionSchema.options; export const SESSION_OWNER_PERMISSIONS = SessionResourcePermissionSchema.options; export const TENANT_CREATE_AGENT_PERMISSIONS = [TenantScopedResourcePermissionSchema.enum.CREATE] as const; +export const TENANT_CREATE_SANDBOX_ENVIRONMENT_PERMISSIONS = [ + TenantScopedResourcePermissionSchema.enum.CREATE, +] as const; /** Response scaffold: every requested id starts with no grants. */ export function emptyPermissionsByResourceId(resourceIds: readonly string[]): Record { diff --git a/packages/trueforge/src/schemas/sandboxEnvironment.ts b/packages/trueforge/src/schemas/sandboxEnvironment.ts new file mode 100644 index 000000000..a73ec0502 --- /dev/null +++ b/packages/trueforge/src/schemas/sandboxEnvironment.ts @@ -0,0 +1,194 @@ +/** + * Sandbox environment domain + wire schemas: identity columns plus a nested + * DaytonaSandboxEnvironmentManifest document (JSON key `manifest`). + * + * Wire `SandboxEnvironmentManifest` is Daytona-only for now (avoid one-member oneOf). + * Store jsonb may widen to a type-discriminated union when another provider ships. + */ +import { GpuType } from '@daytona/sdk'; +import { z } from '@hono/zod-openapi'; +import { CreatedBySubjectSchema, TokenPaginationSchema } from '@truefoundry/trueforge-core/agent-session'; +import { NameSchema } from './common'; + +// random things in sdk:) +const DAYTONA_GPU_TYPE_VALUES = Object.values(GpuType).filter(value => value !== GpuType.UNKNOWN_DEFAULT_OPEN_API) as [ + string, + ...string[], +]; + +export const DaytonaGpuTypeSchema = z + .enum(DAYTONA_GPU_TYPE_VALUES) + .describe('Preferred Daytona GPU type.') + .openapi('DaytonaGpuType'); + +const DaytonaTrueforgeDefaultImageSchema = z + .object({ + type: z.literal('trueforge-default').describe('Use the tenant provider TrueForge release snapshot.'), + }) + .strict() + .openapi('DaytonaTrueforgeDefaultImage'); + +const DaytonaSnapshotImageSchema = z + .object({ + type: z.literal('snapshot').describe('Clone an existing Daytona snapshot by name.'), + name: z.string().min(1).describe('Daytona snapshot name.'), + }) + .strict() + .openapi('DaytonaSnapshotImage'); + +const DaytonaDockerImageSchema = z + .object({ + type: z.literal('docker').describe('Build/create from a container image reference.'), + ref: z.string().min(1).describe('Container image reference passed to Daytona create-from-image.'), + }) + .strict() + .openapi('DaytonaDockerImage'); + +export const DaytonaSandboxEnvironmentImageSchema = z + .discriminatedUnion('type', [ + DaytonaTrueforgeDefaultImageSchema, + DaytonaSnapshotImageSchema, + DaytonaDockerImageSchema, + ]) + .openapi('DaytonaSandboxEnvironmentImage'); + +export const DaytonaSandboxEnvironmentResourcesSchema = z + .object({ + cpu: z.number().positive().optional().describe('CPU allocation in cores.'), + memory: z.number().positive().optional().describe('Memory allocation in GiB.'), + disk: z.number().positive().optional().describe('Disk allocation in GiB.'), + gpu: z.number().nonnegative().optional().describe('GPU allocation in Daytona GPU units.'), + gpu_type: z + .union([DaytonaGpuTypeSchema, z.array(DaytonaGpuTypeSchema).min(1)]) + .optional() + .describe('Preferred GPU type, or an ordered fallback list.'), + }) + .strict() + .openapi('DaytonaSandboxEnvironmentResources'); + +export const DaytonaSandboxEnvironmentNetworkingSchema = z + .object({ + network_block_all: z.boolean().optional().describe('Block all outbound network access.'), + network_allow_list: z.string().min(1).optional().describe('Comma-separated allowed CIDR network addresses.'), + domain_allow_list: z.string().min(1).optional().describe('Comma-separated allowed domains.'), + outbound_proxy_url: z.string().min(1).optional().describe('Outbound HTTP(S) proxy URL.'), + }) + .strict() + .superRefine((value, ctx) => { + const modes = [value.network_block_all === true, !!value.network_allow_list, !!value.domain_allow_list].filter( + Boolean, + ).length; + if (modes > 1) { + ctx.addIssue({ + code: 'custom', + message: 'At most one of network_block_all, network_allow_list, or domain_allow_list may be set', + }); + } + }) + .openapi('DaytonaSandboxEnvironmentNetworking'); + +export const DaytonaSandboxEnvironmentLifecycleSchema = z + .object({ + auto_stop_interval_in_minutes: z + .number() + .int() + .nonnegative() + .optional() + .describe('Minutes of idle time before Daytona auto-stops the sandbox (0 disables).'), + auto_archive_interval_in_minutes: z + .number() + .int() + .nonnegative() + .optional() + .describe('Minutes before Daytona auto-archives the sandbox (0 disables).'), + auto_delete_interval_in_minutes: z + .number() + .int() + .nonnegative() + .optional() + .describe('Minutes before Daytona auto-deletes the sandbox (0 disables).'), + }) + .strict() + .openapi('DaytonaSandboxEnvironmentLifecycle'); + +export const DaytonaSandboxEnvironmentManifestSchema = z + .object({ + type: z.literal('daytona').describe('Daytona sandbox environment.'), + provider: z.literal('daytona').describe('Must match the configured sandbox provider name/type.'), + image: DaytonaSandboxEnvironmentImageSchema, + resources: DaytonaSandboxEnvironmentResourcesSchema.optional(), + secrets: z + .record(z.string().min(1), z.string().min(1)) + .optional() + .describe('Map of sandbox env var name to an existing Daytona organization secret name.'), + networking: DaytonaSandboxEnvironmentNetworkingSchema.optional(), + lifecycle: DaytonaSandboxEnvironmentLifecycleSchema.optional(), + }) + .strict() + .openapi('DaytonaSandboxEnvironmentManifest'); + +/** Settings / OpenAPI — Daytona only until a second provider ships. */ +export const SandboxEnvironmentManifestSchema = + DaytonaSandboxEnvironmentManifestSchema.openapi('SandboxEnvironmentManifest'); + +/** Store jsonb — Daytona today; widen with discriminatedUnion when another type lands. */ +export const StoredSandboxEnvironmentManifestSchema = DaytonaSandboxEnvironmentManifestSchema; + +export const SandboxEnvironmentDescriptionSchema = z + .string() + .trim() + .max(1024) + .describe('Optional human-readable description.'); + +export const CreateSandboxEnvironmentRequestSchema = z + .object({ + name: NameSchema, + description: SandboxEnvironmentDescriptionSchema.optional(), + manifest: SandboxEnvironmentManifestSchema, + }) + .strict() + .openapi('CreateSandboxEnvironmentRequest'); + +export const UpdateSandboxEnvironmentRequestSchema = z + .object({ + description: SandboxEnvironmentDescriptionSchema.nullable().optional(), + manifest: SandboxEnvironmentManifestSchema, + }) + .strict() + .openapi('UpdateSandboxEnvironmentRequest'); + +const IsoTimestamp = z.iso.datetime().openapi({ type: 'string', format: 'date-time' }); + +export const SandboxEnvironmentSchema = z + .object({ + id: z.string().min(1).describe('Immutable server-generated environment identifier.'), + name: NameSchema, + description: z.string().nullable().describe('Optional human-readable description.'), + manifest: SandboxEnvironmentManifestSchema, + created_by_subject: CreatedBySubjectSchema, + created_at: IsoTimestamp.describe('ISO-8601 create time.'), + updated_at: IsoTimestamp.describe('ISO-8601 last update time.'), + }) + .strict() + .openapi('SandboxEnvironment'); + +export const GetSandboxEnvironmentResponseSchema = z + .object({ data: SandboxEnvironmentSchema }) + .openapi('GetSandboxEnvironmentResponse'); + +export const ListSandboxEnvironmentsResponseSchema = z + .object({ + data: z.array(SandboxEnvironmentSchema), + pagination: TokenPaginationSchema, + }) + .openapi('ListSandboxEnvironmentsResponse'); + +export const DeleteSandboxEnvironmentResponseSchema = z.object({}).openapi('DeleteSandboxEnvironmentResponse'); + +export type DaytonaGpuType = z.infer; +export type DaytonaSandboxEnvironmentManifest = z.infer; +export type SandboxEnvironmentManifest = z.infer; +export type StoredSandboxEnvironmentManifest = z.infer; +export type CreateSandboxEnvironmentRequest = z.infer; +export type UpdateSandboxEnvironmentRequest = z.infer; +export type SandboxEnvironment = z.infer; diff --git a/packages/trueforge/src/truefoundry/TrueFoundryAgentStore.ts b/packages/trueforge/src/truefoundry/TrueFoundryAgentStore.ts index 2b96df378..4a53b1003 100644 --- a/packages/trueforge/src/truefoundry/TrueFoundryAgentStore.ts +++ b/packages/trueforge/src/truefoundry/TrueFoundryAgentStore.ts @@ -10,6 +10,7 @@ import { type GetExternalIdsByIdsInput, type GetOwnedIdsInput, type IAgentStore, + type ListAgentIdsUsingSandboxEnvironmentInput, type ListAgentsInput, type UpdateAgentInput, } from '../db/agentStore'; @@ -17,6 +18,7 @@ import { PostgresAgentStore } from '../db/postgres/agent-store/PostgresAgentStor import type { Database } from '../db/postgres/types'; import { AGENT_DESCRIPTION_MAX_LENGTH } from '../schemas/agent'; import { callerAccessToken, type ResolveAccessToken } from './accessToken'; +import { trueFoundryManaged } from './errors'; import { TrueFoundryServiceFoundryServerClient, type PutRemoteAgentInput, @@ -249,4 +251,13 @@ export class TrueFoundryAgentStore implements IAgentStore> await this.#inner.deleteAgent(input, txn); }); } + + listAgentIdsUsingSandboxEnvironment( + input: ListAgentIdsUsingSandboxEnvironmentInput, + transaction?: Transaction, + ): Promise { + void input; + void transaction; + return trueFoundryManaged(); + } } diff --git a/packages/trueforge/src/truefoundry/TrueFoundryAuthorizer.ts b/packages/trueforge/src/truefoundry/TrueFoundryAuthorizer.ts index f91e7a7ff..9b11a77e6 100644 --- a/packages/trueforge/src/truefoundry/TrueFoundryAuthorizer.ts +++ b/packages/trueforge/src/truefoundry/TrueFoundryAuthorizer.ts @@ -83,7 +83,7 @@ export class TrueFoundryAuthorizer implements Authorizer { accessToken: requireUserCredential(requestContext), }); const agent = tenantPermissions.includes('CREATE_AGENT') ? [...TENANT_CREATE_AGENT_PERMISSIONS] : []; - return listPermissionsData('tenant', { agent }); + return listPermissionsData('tenant', { agent, 'sandbox-environment': [] }); } const data = emptyPermissionsByResourceId(resourceIds); diff --git a/packages/trueforge/tests/unit/apis/agents.test.ts b/packages/trueforge/tests/unit/apis/agents.test.ts index dbb83d6c6..02a882fe9 100644 --- a/packages/trueforge/tests/unit/apis/agents.test.ts +++ b/packages/trueforge/tests/unit/apis/agents.test.ts @@ -7,6 +7,7 @@ import { SqliteAgentStore } from '../../../src/db/sqlite/agent-store/SqliteAgent import { createSqliteDb } from '../../../src/db/sqlite/client'; import { SqliteMcpServerStore } from '../../../src/db/sqlite/mcp-server-store/SqliteMcpServerStore'; import { SqliteModelProviderStore } from '../../../src/db/sqlite/model-provider-store/SqliteModelProviderStore'; +import { SqliteSandboxEnvironmentStore } from '../../../src/db/sqlite/sandbox-environment-store/SqliteSandboxEnvironmentStore'; import { SqliteSandboxProviderStore } from '../../../src/db/sqlite/sandbox-provider-store/SqliteSandboxProviderStore'; import { SqliteSkillStore } from '../../../src/db/sqlite/skill-store/SqliteSkillStore'; import { ListAgentsResponseSchema } from '../../../src/schemas/agent'; @@ -104,6 +105,7 @@ describe('agents router', () => { resolveMcpServerStore: () => new SqliteMcpServerStore(db), resolveSkillStore: () => new SqliteSkillStore(db), resolveSandboxProviderStore: () => new SqliteSandboxProviderStore(db), + resolveSandboxEnvironmentStore: () => new SqliteSandboxEnvironmentStore(db), withTransaction: callback => db.transaction().execute(callback), resolveRequestContext: () => STANDALONE_REQUEST_CONTEXT, authorizer: new TrueForgeAuthorizer(), @@ -114,6 +116,7 @@ describe('agents router', () => { resolveMcpServerStore: () => new SqliteMcpServerStore(db), resolveSkillStore: () => new SqliteSkillStore(db), resolveSandboxProviderStore: () => new SqliteSandboxProviderStore(db), + resolveSandboxEnvironmentStore: () => new SqliteSandboxEnvironmentStore(db), withTransaction: callback => db.transaction().execute(callback), resolveRequestContext: () => STANDALONE_REQUEST_CONTEXT, authorizer: denyAllAuthorizer, diff --git a/packages/trueforge/tests/unit/apis/deletedSessionCrud.test.ts b/packages/trueforge/tests/unit/apis/deletedSessionCrud.test.ts index 4c48f6f63..579bbf659 100644 --- a/packages/trueforge/tests/unit/apis/deletedSessionCrud.test.ts +++ b/packages/trueforge/tests/unit/apis/deletedSessionCrud.test.ts @@ -13,6 +13,7 @@ import { SqliteAgentStore } from '../../../src/db/sqlite/agent-store/SqliteAgent import { createSqliteDb } from '../../../src/db/sqlite/client'; import { SqliteMcpServerStore } from '../../../src/db/sqlite/mcp-server-store/SqliteMcpServerStore'; import { SqliteModelProviderStore } from '../../../src/db/sqlite/model-provider-store/SqliteModelProviderStore'; +import { SqliteSandboxEnvironmentStore } from '../../../src/db/sqlite/sandbox-environment-store/SqliteSandboxEnvironmentStore'; import { SqliteSandboxProviderStore } from '../../../src/db/sqlite/sandbox-provider-store/SqliteSandboxProviderStore'; import { SqliteSessionStore } from '../../../src/db/sqlite/session-store/SqliteSessionStore'; import { SqliteSkillStore } from '../../../src/db/sqlite/skill-store/SqliteSkillStore'; @@ -38,6 +39,7 @@ describe('public CRUD after session deletion', () => { const skillStore = new SqliteSkillStore(db); const agentStore = new SqliteAgentStore(db); const sandboxProviderStore = new SqliteSandboxProviderStore(db); + const sandboxEnvironmentStore = new SqliteSandboxEnvironmentStore(db); const app = new OpenAPIHono(); app.route( @@ -51,6 +53,7 @@ describe('public CRUD after session deletion', () => { resolveSkillStore: () => skillStore, resolveAgentStore: () => agentStore, resolveSandboxProviderStore: () => sandboxProviderStore, + resolveSandboxEnvironmentStore: () => sandboxEnvironmentStore, redis: createClient(), requestReplyRouter: new RequestReplyRouter(), resolveRequestContext: () => STANDALONE_REQUEST_CONTEXT, diff --git a/packages/trueforge/tests/unit/apis/sessionHttp.test.ts b/packages/trueforge/tests/unit/apis/sessionHttp.test.ts index 43e94f0b4..84aa8c784 100644 --- a/packages/trueforge/tests/unit/apis/sessionHttp.test.ts +++ b/packages/trueforge/tests/unit/apis/sessionHttp.test.ts @@ -16,6 +16,7 @@ import { SqliteAgentStore } from '../../../src/db/sqlite/agent-store/SqliteAgent import { createSqliteDb } from '../../../src/db/sqlite/client'; import { SqliteMcpServerStore } from '../../../src/db/sqlite/mcp-server-store/SqliteMcpServerStore'; import { SqliteModelProviderStore } from '../../../src/db/sqlite/model-provider-store/SqliteModelProviderStore'; +import { SqliteSandboxEnvironmentStore } from '../../../src/db/sqlite/sandbox-environment-store/SqliteSandboxEnvironmentStore'; import { SqliteSandboxProviderStore } from '../../../src/db/sqlite/sandbox-provider-store/SqliteSandboxProviderStore'; import { SqliteSessionMetricsStore } from '../../../src/db/sqlite/session-metrics/SqliteSessionMetricsStore'; import { SqliteSessionStore } from '../../../src/db/sqlite/session-store/SqliteSessionStore'; @@ -96,6 +97,7 @@ describe('sessions HTTP agent binding', () => { resolveSkillStore: () => skillStore, resolveAgentStore: () => agentStore, resolveSandboxProviderStore: () => sandboxProviderStore, + resolveSandboxEnvironmentStore: () => new SqliteSandboxEnvironmentStore(db), redis: createClient(), requestReplyRouter: new RequestReplyRouter(), resolveRequestContext: () => STANDALONE_REQUEST_CONTEXT, diff --git a/packages/trueforge/tests/unit/auth/authorizerPermissions.test.ts b/packages/trueforge/tests/unit/auth/authorizerPermissions.test.ts index 5b6627022..84536829c 100644 --- a/packages/trueforge/tests/unit/auth/authorizerPermissions.test.ts +++ b/packages/trueforge/tests/unit/auth/authorizerPermissions.test.ts @@ -93,7 +93,7 @@ describe('TrueForgeAuthorizer.getPermissions', () => { }); }); - it('always grants tenant agent CREATE in standalone mode', async () => { + it('always grants tenant agent and sandbox-environment CREATE in standalone mode', async () => { expect( await authorizer.getPermissions({ resourceType: 'tenant', @@ -102,7 +102,7 @@ describe('TrueForgeAuthorizer.getPermissions', () => { }), ).toEqual({ type: 'tenant', - permissions: { agent: ['CREATE'] }, + permissions: { agent: ['CREATE'], 'sandbox-environment': ['CREATE'] }, }); }); }); diff --git a/packages/trueforge/tests/unit/runtime/sessionResources.test.ts b/packages/trueforge/tests/unit/runtime/sessionResources.test.ts index c48863b1e..3107627d9 100644 --- a/packages/trueforge/tests/unit/runtime/sessionResources.test.ts +++ b/packages/trueforge/tests/unit/runtime/sessionResources.test.ts @@ -13,6 +13,7 @@ import type { ISkillStore } from '../../../src/db/skillStore'; import { createSqliteDb } from '../../../src/db/sqlite/client'; import { SqliteMcpServerStore } from '../../../src/db/sqlite/mcp-server-store/SqliteMcpServerStore'; import { SqliteModelProviderStore } from '../../../src/db/sqlite/model-provider-store/SqliteModelProviderStore'; +import { SqliteSandboxEnvironmentStore } from '../../../src/db/sqlite/sandbox-environment-store/SqliteSandboxEnvironmentStore'; import { SqliteSandboxProviderStore } from '../../../src/db/sqlite/sandbox-provider-store/SqliteSandboxProviderStore'; import { SqliteSkillStore } from '../../../src/db/sqlite/skill-store/SqliteSkillStore'; import { @@ -211,6 +212,8 @@ describe('validateAgentSpec', () => { mcpServerStore: new SqliteMcpServerStore(db), skillStore: new SqliteSkillStore(db), sandboxProviderStore: new SqliteSandboxProviderStore(db), + sandboxEnvironmentStore: new SqliteSandboxEnvironmentStore(db), + subject_id: 'test-subject', }; } diff --git a/python/trueforge_sdk/reference.md b/python/trueforge_sdk/reference.md index aa8c83132..3a399ffb1 100644 --- a/python/trueforge_sdk/reference.md +++ b/python/trueforge_sdk/reference.md @@ -1045,6 +1045,421 @@ client.models.list() + + + + +## SandboxEnvironments +
client.sandbox_environments.list(...) -> ListSandboxEnvironmentsResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +List sandbox environments created by the authenticated subject, newest first. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.sandbox_environments.list() + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**limit:** `typing.Optional[int]` — Page size. Defaults to 25 + +
+
+ +
+
+ +**page_token:** `typing.Optional[str]` — Opaque token from a previous response `next_page_token`. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.sandbox_environments.create(...) -> GetSandboxEnvironmentResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Create a Daytona sandbox environment owned by the authenticated subject. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge, SandboxEnvironmentManifest, DaytonaDockerImage + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.sandbox_environments.create( + manifest=SandboxEnvironmentManifest( + image=DaytonaDockerImage( + ref="ref", + type="docker", + ), + provider="daytona", + type="daytona", + ), + name="name", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**manifest:** `SandboxEnvironmentManifest` + +
+
+ +
+
+ +**name:** `ResourceName` + +
+
+ +
+
+ +**description:** `typing.Optional[str]` — Optional human-readable description. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.sandbox_environments.get(...) -> GetSandboxEnvironmentResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Get a sandbox environment owned by the authenticated subject. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.sandbox_environments.get( + sandbox_environment_id="sandbox_environment_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**sandbox_environment_id:** `str` — Immutable sandbox environment identifier. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.sandbox_environments.update(...) -> GetSandboxEnvironmentResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Replace the manifest (and optionally description). Name is immutable. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge, SandboxEnvironmentManifest, DaytonaDockerImage + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.sandbox_environments.update( + sandbox_environment_id="sandbox_environment_id", + manifest=SandboxEnvironmentManifest( + image=DaytonaDockerImage( + ref="ref", + type="docker", + ), + provider="daytona", + type="daytona", + ), +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**sandbox_environment_id:** `str` — Immutable sandbox environment identifier. + +
+
+ +
+
+ +**manifest:** `SandboxEnvironmentManifest` + +
+
+ +
+
+ +**description:** `typing.Optional[str]` — Optional human-readable description. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.sandbox_environments.delete(...) -> DeleteSandboxEnvironmentResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Delete a caller-owned sandbox environment. Fails if any agent references it. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.sandbox_environments.delete( + sandbox_environment_id="sandbox_environment_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**sandbox_environment_id:** `str` — Immutable sandbox environment identifier. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ +
diff --git a/python/trueforge_sdk/src/trueforge_sdk/__init__.py b/python/trueforge_sdk/src/trueforge_sdk/__init__.py index 7242c3c63..891908359 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/__init__.py +++ b/python/trueforge_sdk/src/trueforge_sdk/__init__.py @@ -58,8 +58,18 @@ CreatedBySubject, CronExpression, CustomModelProvider, + DaytonaDockerImage, + DaytonaGpuType, + DaytonaSandboxEnvironmentImage, + DaytonaSandboxEnvironmentLifecycle, + DaytonaSandboxEnvironmentNetworking, + DaytonaSandboxEnvironmentResources, + DaytonaSandboxEnvironmentResourcesGpuType, DaytonaSandboxProviderAuth, + DaytonaSnapshotImage, + DaytonaTrueforgeDefaultImage, DeleteAgentResponse, + DeleteSandboxEnvironmentResponse, DeleteScheduleResponse, DynamicSubAgentsConfig, ExtendedChunkDeltaToolCall, @@ -77,6 +87,7 @@ GetMeSubject, GetModelProviderCatalogResponse, GetModelProviderResponse, + GetSandboxEnvironmentResponse, GetSandboxProviderCatalogResponse, GetSandboxProviderResponse, GetScheduleResponse, @@ -101,6 +112,7 @@ ListModelProvidersResponse, ListPermissionsData, ListPermissionsResponse, + ListSandboxEnvironmentsResponse, ListScheduleRunsResponse, ListSchedulesResponse, ListSessionEventsResponse, @@ -164,6 +176,8 @@ SandboxCapability, SandboxConfig, SandboxCreatedEvent, + SandboxEnvironment, + SandboxEnvironmentManifest, SandboxProviderManifest, Schedule, ScheduleManifest, @@ -259,7 +273,20 @@ UnauthorizedError, UnprocessableEntityError, ) - from . import agents, auth, catalogs, internal, mcp_servers, models, schedules, server, sessions, settings, skills + from . import ( + agents, + auth, + catalogs, + internal, + mcp_servers, + models, + sandbox_environments, + schedules, + server, + sessions, + settings, + skills, + ) from ._default_clients import DefaultAioHttpClient, DefaultAsyncHttpxClient from .client import AsyncTrueForge, TrueForge from .version import __version__ @@ -320,10 +347,20 @@ "CreatedBySubject": ".types", "CronExpression": ".types", "CustomModelProvider": ".types", + "DaytonaDockerImage": ".types", + "DaytonaGpuType": ".types", + "DaytonaSandboxEnvironmentImage": ".types", + "DaytonaSandboxEnvironmentLifecycle": ".types", + "DaytonaSandboxEnvironmentNetworking": ".types", + "DaytonaSandboxEnvironmentResources": ".types", + "DaytonaSandboxEnvironmentResourcesGpuType": ".types", "DaytonaSandboxProviderAuth": ".types", + "DaytonaSnapshotImage": ".types", + "DaytonaTrueforgeDefaultImage": ".types", "DefaultAioHttpClient": "._default_clients", "DefaultAsyncHttpxClient": "._default_clients", "DeleteAgentResponse": ".types", + "DeleteSandboxEnvironmentResponse": ".types", "DeleteScheduleResponse": ".types", "DynamicSubAgentsConfig": ".types", "ExtendedChunkDeltaToolCall": ".types", @@ -343,6 +380,7 @@ "GetMeSubject": ".types", "GetModelProviderCatalogResponse": ".types", "GetModelProviderResponse": ".types", + "GetSandboxEnvironmentResponse": ".types", "GetSandboxProviderCatalogResponse": ".types", "GetSandboxProviderResponse": ".types", "GetScheduleResponse": ".types", @@ -369,6 +407,7 @@ "ListModelProvidersResponse": ".types", "ListPermissionsData": ".types", "ListPermissionsResponse": ".types", + "ListSandboxEnvironmentsResponse": ".types", "ListScheduleRunsResponse": ".types", "ListSchedulesResponse": ".types", "ListSessionEventsResponse": ".types", @@ -434,6 +473,8 @@ "SandboxCapability": ".types", "SandboxConfig": ".types", "SandboxCreatedEvent": ".types", + "SandboxEnvironment": ".types", + "SandboxEnvironmentManifest": ".types", "SandboxProviderManifest": ".types", "Schedule": ".types", "ScheduleManifest": ".types", @@ -524,6 +565,7 @@ "internal": ".internal", "mcp_servers": ".mcp_servers", "models": ".models", + "sandbox_environments": ".sandbox_environments", "schedules": ".schedules", "server": ".server", "sessions": ".sessions", @@ -610,10 +652,20 @@ def __dir__(): "CreatedBySubject", "CronExpression", "CustomModelProvider", + "DaytonaDockerImage", + "DaytonaGpuType", + "DaytonaSandboxEnvironmentImage", + "DaytonaSandboxEnvironmentLifecycle", + "DaytonaSandboxEnvironmentNetworking", + "DaytonaSandboxEnvironmentResources", + "DaytonaSandboxEnvironmentResourcesGpuType", "DaytonaSandboxProviderAuth", + "DaytonaSnapshotImage", + "DaytonaTrueforgeDefaultImage", "DefaultAioHttpClient", "DefaultAsyncHttpxClient", "DeleteAgentResponse", + "DeleteSandboxEnvironmentResponse", "DeleteScheduleResponse", "DynamicSubAgentsConfig", "ExtendedChunkDeltaToolCall", @@ -633,6 +685,7 @@ def __dir__(): "GetMeSubject", "GetModelProviderCatalogResponse", "GetModelProviderResponse", + "GetSandboxEnvironmentResponse", "GetSandboxProviderCatalogResponse", "GetSandboxProviderResponse", "GetScheduleResponse", @@ -659,6 +712,7 @@ def __dir__(): "ListModelProvidersResponse", "ListPermissionsData", "ListPermissionsResponse", + "ListSandboxEnvironmentsResponse", "ListScheduleRunsResponse", "ListSchedulesResponse", "ListSessionEventsResponse", @@ -724,6 +778,8 @@ def __dir__(): "SandboxCapability", "SandboxConfig", "SandboxCreatedEvent", + "SandboxEnvironment", + "SandboxEnvironmentManifest", "SandboxProviderManifest", "Schedule", "ScheduleManifest", @@ -814,6 +870,7 @@ def __dir__(): "internal", "mcp_servers", "models", + "sandbox_environments", "schedules", "server", "sessions", diff --git a/python/trueforge_sdk/src/trueforge_sdk/base_client.py b/python/trueforge_sdk/src/trueforge_sdk/base_client.py index e086197b1..f2fbb2e2c 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/base_client.py +++ b/python/trueforge_sdk/src/trueforge_sdk/base_client.py @@ -15,6 +15,7 @@ from .internal.client import AsyncInternalClient, InternalClient from .mcp_servers.client import AsyncMcpServersClient, McpServersClient from .models.client import AsyncModelsClient, ModelsClient + from .sandbox_environments.client import AsyncSandboxEnvironmentsClient, SandboxEnvironmentsClient from .schedules.client import AsyncSchedulesClient, SchedulesClient from .server.client import AsyncServerClient, ServerClient from .sessions.client import AsyncSessionsClient, SessionsClient @@ -103,6 +104,7 @@ def __init__( self._server: typing.Optional[ServerClient] = None self._mcp_servers: typing.Optional[McpServersClient] = None self._models: typing.Optional[ModelsClient] = None + self._sandbox_environments: typing.Optional[SandboxEnvironmentsClient] = None self._schedules: typing.Optional[SchedulesClient] = None self._sessions: typing.Optional[SessionsClient] = None self._skills: typing.Optional[SkillsClient] = None @@ -157,6 +159,14 @@ def models(self): self._models = ModelsClient(client_wrapper=self._client_wrapper) return self._models + @property + def sandbox_environments(self): + if self._sandbox_environments is None: + from .sandbox_environments.client import SandboxEnvironmentsClient # noqa: E402 + + self._sandbox_environments = SandboxEnvironmentsClient(client_wrapper=self._client_wrapper) + return self._sandbox_environments + @property def schedules(self): if self._schedules is None: @@ -300,6 +310,7 @@ def __init__( self._server: typing.Optional[AsyncServerClient] = None self._mcp_servers: typing.Optional[AsyncMcpServersClient] = None self._models: typing.Optional[AsyncModelsClient] = None + self._sandbox_environments: typing.Optional[AsyncSandboxEnvironmentsClient] = None self._schedules: typing.Optional[AsyncSchedulesClient] = None self._sessions: typing.Optional[AsyncSessionsClient] = None self._skills: typing.Optional[AsyncSkillsClient] = None @@ -354,6 +365,14 @@ def models(self): self._models = AsyncModelsClient(client_wrapper=self._client_wrapper) return self._models + @property + def sandbox_environments(self): + if self._sandbox_environments is None: + from .sandbox_environments.client import AsyncSandboxEnvironmentsClient # noqa: E402 + + self._sandbox_environments = AsyncSandboxEnvironmentsClient(client_wrapper=self._client_wrapper) + return self._sandbox_environments + @property def schedules(self): if self._schedules is None: diff --git a/python/trueforge_sdk/src/trueforge_sdk/sandbox_environments/__init__.py b/python/trueforge_sdk/src/trueforge_sdk/sandbox_environments/__init__.py new file mode 100644 index 000000000..5cde0202d --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/sandbox_environments/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/python/trueforge_sdk/src/trueforge_sdk/sandbox_environments/client.py b/python/trueforge_sdk/src/trueforge_sdk/sandbox_environments/client.py new file mode 100644 index 000000000..bbf512221 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/sandbox_environments/client.py @@ -0,0 +1,542 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.pagination import AsyncPager, SyncPager +from ..core.request_options import RequestOptions +from ..types.delete_sandbox_environment_response import DeleteSandboxEnvironmentResponse +from ..types.get_sandbox_environment_response import GetSandboxEnvironmentResponse +from ..types.list_sandbox_environments_response import ListSandboxEnvironmentsResponse +from ..types.resource_name import ResourceName +from ..types.sandbox_environment import SandboxEnvironment +from ..types.sandbox_environment_manifest import SandboxEnvironmentManifest +from .raw_client import AsyncRawSandboxEnvironmentsClient, RawSandboxEnvironmentsClient + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class SandboxEnvironmentsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawSandboxEnvironmentsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawSandboxEnvironmentsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawSandboxEnvironmentsClient + """ + return self._raw_client + + def list( + self, + *, + limit: typing.Optional[int] = 25, + page_token: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> SyncPager[SandboxEnvironment, ListSandboxEnvironmentsResponse]: + """ + List sandbox environments created by the authenticated subject, newest first. + + Parameters + ---------- + limit : typing.Optional[int] + Page size. Defaults to 25 + + page_token : typing.Optional[str] + Opaque token from a previous response `next_page_token`. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + SyncPager[SandboxEnvironment, ListSandboxEnvironmentsResponse] + Paginated caller-owned sandbox environments. + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + response = client.sandbox_environments.list() + for item in response: + yield item + # alternatively, you can paginate page-by-page + for page in response.iter_pages(): + yield page + """ + return self._raw_client.list(limit=limit, page_token=page_token, request_options=request_options) + + def create( + self, + *, + manifest: SandboxEnvironmentManifest, + name: ResourceName, + description: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> GetSandboxEnvironmentResponse: + """ + Create a Daytona sandbox environment owned by the authenticated subject. + + Parameters + ---------- + manifest : SandboxEnvironmentManifest + + name : ResourceName + + description : typing.Optional[str] + Optional human-readable description. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetSandboxEnvironmentResponse + Created sandbox environment. + + Examples + -------- + from trueforge_sdk import ( + DaytonaDockerImage, + SandboxEnvironmentManifest, + TrueForge, + ) + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.sandbox_environments.create( + manifest=SandboxEnvironmentManifest( + image=DaytonaDockerImage( + ref="ref", + ), + ), + name="name", + ) + """ + _response = self._raw_client.create( + manifest=manifest, name=name, description=description, request_options=request_options + ) + return _response.data + + def get( + self, *, sandbox_environment_id: str, request_options: typing.Optional[RequestOptions] = None + ) -> GetSandboxEnvironmentResponse: + """ + Get a sandbox environment owned by the authenticated subject. + + Parameters + ---------- + sandbox_environment_id : str + Immutable sandbox environment identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetSandboxEnvironmentResponse + The sandbox environment. + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.sandbox_environments.get( + sandbox_environment_id="sandbox_environment_id", + ) + """ + _response = self._raw_client.get(sandbox_environment_id=sandbox_environment_id, request_options=request_options) + return _response.data + + def update( + self, + *, + sandbox_environment_id: str, + manifest: SandboxEnvironmentManifest, + description: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> GetSandboxEnvironmentResponse: + """ + Replace the manifest (and optionally description). Name is immutable. + + Parameters + ---------- + sandbox_environment_id : str + Immutable sandbox environment identifier. + + manifest : SandboxEnvironmentManifest + + description : typing.Optional[str] + Optional human-readable description. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetSandboxEnvironmentResponse + Updated sandbox environment. + + Examples + -------- + from trueforge_sdk import ( + DaytonaDockerImage, + SandboxEnvironmentManifest, + TrueForge, + ) + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.sandbox_environments.update( + sandbox_environment_id="sandbox_environment_id", + manifest=SandboxEnvironmentManifest( + image=DaytonaDockerImage( + ref="ref", + ), + ), + ) + """ + _response = self._raw_client.update( + sandbox_environment_id=sandbox_environment_id, + manifest=manifest, + description=description, + request_options=request_options, + ) + return _response.data + + def delete( + self, *, sandbox_environment_id: str, request_options: typing.Optional[RequestOptions] = None + ) -> DeleteSandboxEnvironmentResponse: + """ + Delete a caller-owned sandbox environment. Fails if any agent references it. + + Parameters + ---------- + sandbox_environment_id : str + Immutable sandbox environment identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + DeleteSandboxEnvironmentResponse + Deleted. + + Examples + -------- + from trueforge_sdk import TrueForge + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.sandbox_environments.delete( + sandbox_environment_id="sandbox_environment_id", + ) + """ + _response = self._raw_client.delete( + sandbox_environment_id=sandbox_environment_id, request_options=request_options + ) + return _response.data + + +class AsyncSandboxEnvironmentsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawSandboxEnvironmentsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawSandboxEnvironmentsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawSandboxEnvironmentsClient + """ + return self._raw_client + + async def list( + self, + *, + limit: typing.Optional[int] = 25, + page_token: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncPager[SandboxEnvironment, ListSandboxEnvironmentsResponse]: + """ + List sandbox environments created by the authenticated subject, newest first. + + Parameters + ---------- + limit : typing.Optional[int] + Page size. Defaults to 25 + + page_token : typing.Optional[str] + Opaque token from a previous response `next_page_token`. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncPager[SandboxEnvironment, ListSandboxEnvironmentsResponse] + Paginated caller-owned sandbox environments. + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + response = await client.sandbox_environments.list() + async for item in response: + yield item + + # alternatively, you can paginate page-by-page + async for page in response.iter_pages(): + yield page + + + asyncio.run(main()) + """ + return await self._raw_client.list(limit=limit, page_token=page_token, request_options=request_options) + + async def create( + self, + *, + manifest: SandboxEnvironmentManifest, + name: ResourceName, + description: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> GetSandboxEnvironmentResponse: + """ + Create a Daytona sandbox environment owned by the authenticated subject. + + Parameters + ---------- + manifest : SandboxEnvironmentManifest + + name : ResourceName + + description : typing.Optional[str] + Optional human-readable description. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetSandboxEnvironmentResponse + Created sandbox environment. + + Examples + -------- + import asyncio + + from trueforge_sdk import ( + AsyncTrueForge, + DaytonaDockerImage, + SandboxEnvironmentManifest, + ) + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.sandbox_environments.create( + manifest=SandboxEnvironmentManifest( + image=DaytonaDockerImage( + ref="ref", + ), + ), + name="name", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.create( + manifest=manifest, name=name, description=description, request_options=request_options + ) + return _response.data + + async def get( + self, *, sandbox_environment_id: str, request_options: typing.Optional[RequestOptions] = None + ) -> GetSandboxEnvironmentResponse: + """ + Get a sandbox environment owned by the authenticated subject. + + Parameters + ---------- + sandbox_environment_id : str + Immutable sandbox environment identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetSandboxEnvironmentResponse + The sandbox environment. + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.sandbox_environments.get( + sandbox_environment_id="sandbox_environment_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get( + sandbox_environment_id=sandbox_environment_id, request_options=request_options + ) + return _response.data + + async def update( + self, + *, + sandbox_environment_id: str, + manifest: SandboxEnvironmentManifest, + description: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> GetSandboxEnvironmentResponse: + """ + Replace the manifest (and optionally description). Name is immutable. + + Parameters + ---------- + sandbox_environment_id : str + Immutable sandbox environment identifier. + + manifest : SandboxEnvironmentManifest + + description : typing.Optional[str] + Optional human-readable description. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetSandboxEnvironmentResponse + Updated sandbox environment. + + Examples + -------- + import asyncio + + from trueforge_sdk import ( + AsyncTrueForge, + DaytonaDockerImage, + SandboxEnvironmentManifest, + ) + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.sandbox_environments.update( + sandbox_environment_id="sandbox_environment_id", + manifest=SandboxEnvironmentManifest( + image=DaytonaDockerImage( + ref="ref", + ), + ), + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.update( + sandbox_environment_id=sandbox_environment_id, + manifest=manifest, + description=description, + request_options=request_options, + ) + return _response.data + + async def delete( + self, *, sandbox_environment_id: str, request_options: typing.Optional[RequestOptions] = None + ) -> DeleteSandboxEnvironmentResponse: + """ + Delete a caller-owned sandbox environment. Fails if any agent references it. + + Parameters + ---------- + sandbox_environment_id : str + Immutable sandbox environment identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + DeleteSandboxEnvironmentResponse + Deleted. + + Examples + -------- + import asyncio + + from trueforge_sdk import AsyncTrueForge + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.sandbox_environments.delete( + sandbox_environment_id="sandbox_environment_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.delete( + sandbox_environment_id=sandbox_environment_id, request_options=request_options + ) + return _response.data diff --git a/python/trueforge_sdk/src/trueforge_sdk/sandbox_environments/raw_client.py b/python/trueforge_sdk/src/trueforge_sdk/sandbox_environments/raw_client.py new file mode 100644 index 000000000..9a4dce64f --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/sandbox_environments/raw_client.py @@ -0,0 +1,927 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.jsonable_encoder import encode_path_param +from ..core.pagination import AsyncPager, SyncPager +from ..core.parse_error import ParsingError +from ..core.request_options import RequestOptions +from ..core.serialization import convert_and_respect_annotation_metadata +from ..core.unchecked_base_model import construct_type +from ..errors.bad_request_error import BadRequestError +from ..errors.conflict_error import ConflictError +from ..errors.not_found_error import NotFoundError +from ..errors.unauthorized_error import UnauthorizedError +from ..errors.unprocessable_entity_error import UnprocessableEntityError +from ..types.delete_sandbox_environment_response import DeleteSandboxEnvironmentResponse +from ..types.get_sandbox_environment_response import GetSandboxEnvironmentResponse +from ..types.list_sandbox_environments_response import ListSandboxEnvironmentsResponse +from ..types.request_error_response import RequestErrorResponse +from ..types.resource_name import ResourceName +from ..types.sandbox_environment import SandboxEnvironment +from ..types.sandbox_environment_manifest import SandboxEnvironmentManifest +from pydantic import ValidationError + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawSandboxEnvironmentsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def list( + self, + *, + limit: typing.Optional[int] = 25, + page_token: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> SyncPager[SandboxEnvironment, ListSandboxEnvironmentsResponse]: + """ + List sandbox environments created by the authenticated subject, newest first. + + Parameters + ---------- + limit : typing.Optional[int] + Page size. Defaults to 25 + + page_token : typing.Optional[str] + Opaque token from a previous response `next_page_token`. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + SyncPager[SandboxEnvironment, ListSandboxEnvironmentsResponse] + Paginated caller-owned sandbox environments. + """ + _response = self._client_wrapper.httpx_client.request( + "api/v1/sandbox-environments", + method="GET", + params={ + "limit": limit, + "page_token": page_token, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _parsed_response = typing.cast( + ListSandboxEnvironmentsResponse, + construct_type( + type_=ListSandboxEnvironmentsResponse, # type: ignore + object_=_response.json(), + ), + ) + _items = _parsed_response.data + _has_next = False + _get_next = None + if _parsed_response.pagination is not None: + _parsed_next = _parsed_response.pagination.next_page_token + _has_next = _parsed_next is not None and _parsed_next != "" + _get_next = lambda: self.list( + limit=limit, + page_token=_parsed_next, + request_options=request_options, + ) + return SyncPager(has_next=_has_next, items=_items, get_next=_get_next, response=_parsed_response) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def create( + self, + *, + manifest: SandboxEnvironmentManifest, + name: ResourceName, + description: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[GetSandboxEnvironmentResponse]: + """ + Create a Daytona sandbox environment owned by the authenticated subject. + + Parameters + ---------- + manifest : SandboxEnvironmentManifest + + name : ResourceName + + description : typing.Optional[str] + Optional human-readable description. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[GetSandboxEnvironmentResponse] + Created sandbox environment. + """ + _response = self._client_wrapper.httpx_client.request( + "api/v1/sandbox-environments", + method="POST", + json={ + "description": description, + "manifest": convert_and_respect_annotation_metadata( + object_=manifest, annotation=SandboxEnvironmentManifest, direction="write" + ), + "name": name, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetSandboxEnvironmentResponse, + construct_type( + type_=GetSandboxEnvironmentResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def get( + self, *, sandbox_environment_id: str, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[GetSandboxEnvironmentResponse]: + """ + Get a sandbox environment owned by the authenticated subject. + + Parameters + ---------- + sandbox_environment_id : str + Immutable sandbox environment identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[GetSandboxEnvironmentResponse] + The sandbox environment. + """ + _response = self._client_wrapper.httpx_client.request( + f"api/v1/sandbox-environments/{encode_path_param(sandbox_environment_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetSandboxEnvironmentResponse, + construct_type( + type_=GetSandboxEnvironmentResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def update( + self, + *, + sandbox_environment_id: str, + manifest: SandboxEnvironmentManifest, + description: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[GetSandboxEnvironmentResponse]: + """ + Replace the manifest (and optionally description). Name is immutable. + + Parameters + ---------- + sandbox_environment_id : str + Immutable sandbox environment identifier. + + manifest : SandboxEnvironmentManifest + + description : typing.Optional[str] + Optional human-readable description. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[GetSandboxEnvironmentResponse] + Updated sandbox environment. + """ + _response = self._client_wrapper.httpx_client.request( + f"api/v1/sandbox-environments/{encode_path_param(sandbox_environment_id)}", + method="PUT", + json={ + "description": description, + "manifest": convert_and_respect_annotation_metadata( + object_=manifest, annotation=SandboxEnvironmentManifest, direction="write" + ), + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetSandboxEnvironmentResponse, + construct_type( + type_=GetSandboxEnvironmentResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def delete( + self, *, sandbox_environment_id: str, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[DeleteSandboxEnvironmentResponse]: + """ + Delete a caller-owned sandbox environment. Fails if any agent references it. + + Parameters + ---------- + sandbox_environment_id : str + Immutable sandbox environment identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[DeleteSandboxEnvironmentResponse] + Deleted. + """ + _response = self._client_wrapper.httpx_client.request( + f"api/v1/sandbox-environments/{encode_path_param(sandbox_environment_id)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + DeleteSandboxEnvironmentResponse, + construct_type( + type_=DeleteSandboxEnvironmentResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawSandboxEnvironmentsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def list( + self, + *, + limit: typing.Optional[int] = 25, + page_token: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncPager[SandboxEnvironment, ListSandboxEnvironmentsResponse]: + """ + List sandbox environments created by the authenticated subject, newest first. + + Parameters + ---------- + limit : typing.Optional[int] + Page size. Defaults to 25 + + page_token : typing.Optional[str] + Opaque token from a previous response `next_page_token`. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncPager[SandboxEnvironment, ListSandboxEnvironmentsResponse] + Paginated caller-owned sandbox environments. + """ + _response = await self._client_wrapper.httpx_client.request( + "api/v1/sandbox-environments", + method="GET", + params={ + "limit": limit, + "page_token": page_token, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _parsed_response = typing.cast( + ListSandboxEnvironmentsResponse, + construct_type( + type_=ListSandboxEnvironmentsResponse, # type: ignore + object_=_response.json(), + ), + ) + _items = _parsed_response.data + _has_next = False + _get_next = None + if _parsed_response.pagination is not None: + _parsed_next = _parsed_response.pagination.next_page_token + _has_next = _parsed_next is not None and _parsed_next != "" + + async def _get_next(): + return await self.list( + limit=limit, + page_token=_parsed_next, + request_options=request_options, + ) + + return AsyncPager(has_next=_has_next, items=_items, get_next=_get_next, response=_parsed_response) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def create( + self, + *, + manifest: SandboxEnvironmentManifest, + name: ResourceName, + description: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[GetSandboxEnvironmentResponse]: + """ + Create a Daytona sandbox environment owned by the authenticated subject. + + Parameters + ---------- + manifest : SandboxEnvironmentManifest + + name : ResourceName + + description : typing.Optional[str] + Optional human-readable description. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[GetSandboxEnvironmentResponse] + Created sandbox environment. + """ + _response = await self._client_wrapper.httpx_client.request( + "api/v1/sandbox-environments", + method="POST", + json={ + "description": description, + "manifest": convert_and_respect_annotation_metadata( + object_=manifest, annotation=SandboxEnvironmentManifest, direction="write" + ), + "name": name, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetSandboxEnvironmentResponse, + construct_type( + type_=GetSandboxEnvironmentResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def get( + self, *, sandbox_environment_id: str, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[GetSandboxEnvironmentResponse]: + """ + Get a sandbox environment owned by the authenticated subject. + + Parameters + ---------- + sandbox_environment_id : str + Immutable sandbox environment identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[GetSandboxEnvironmentResponse] + The sandbox environment. + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/v1/sandbox-environments/{encode_path_param(sandbox_environment_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetSandboxEnvironmentResponse, + construct_type( + type_=GetSandboxEnvironmentResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def update( + self, + *, + sandbox_environment_id: str, + manifest: SandboxEnvironmentManifest, + description: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[GetSandboxEnvironmentResponse]: + """ + Replace the manifest (and optionally description). Name is immutable. + + Parameters + ---------- + sandbox_environment_id : str + Immutable sandbox environment identifier. + + manifest : SandboxEnvironmentManifest + + description : typing.Optional[str] + Optional human-readable description. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[GetSandboxEnvironmentResponse] + Updated sandbox environment. + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/v1/sandbox-environments/{encode_path_param(sandbox_environment_id)}", + method="PUT", + json={ + "description": description, + "manifest": convert_and_respect_annotation_metadata( + object_=manifest, annotation=SandboxEnvironmentManifest, direction="write" + ), + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetSandboxEnvironmentResponse, + construct_type( + type_=GetSandboxEnvironmentResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def delete( + self, *, sandbox_environment_id: str, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[DeleteSandboxEnvironmentResponse]: + """ + Delete a caller-owned sandbox environment. Fails if any agent references it. + + Parameters + ---------- + sandbox_environment_id : str + Immutable sandbox environment identifier. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[DeleteSandboxEnvironmentResponse] + Deleted. + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/v1/sandbox-environments/{encode_path_param(sandbox_environment_id)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + DeleteSandboxEnvironmentResponse, + construct_type( + type_=DeleteSandboxEnvironmentResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/__init__.py b/python/trueforge_sdk/src/trueforge_sdk/types/__init__.py index 2e83715b6..114cda8c3 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/types/__init__.py +++ b/python/trueforge_sdk/src/trueforge_sdk/types/__init__.py @@ -57,8 +57,18 @@ from .created_by_subject import CreatedBySubject from .cron_expression import CronExpression from .custom_model_provider import CustomModelProvider + from .daytona_docker_image import DaytonaDockerImage + from .daytona_gpu_type import DaytonaGpuType + from .daytona_sandbox_environment_image import DaytonaSandboxEnvironmentImage + from .daytona_sandbox_environment_lifecycle import DaytonaSandboxEnvironmentLifecycle + from .daytona_sandbox_environment_networking import DaytonaSandboxEnvironmentNetworking + from .daytona_sandbox_environment_resources import DaytonaSandboxEnvironmentResources + from .daytona_sandbox_environment_resources_gpu_type import DaytonaSandboxEnvironmentResourcesGpuType from .daytona_sandbox_provider_auth import DaytonaSandboxProviderAuth + from .daytona_snapshot_image import DaytonaSnapshotImage + from .daytona_trueforge_default_image import DaytonaTrueforgeDefaultImage from .delete_agent_response import DeleteAgentResponse + from .delete_sandbox_environment_response import DeleteSandboxEnvironmentResponse from .delete_schedule_response import DeleteScheduleResponse from .dynamic_sub_agents_config import DynamicSubAgentsConfig from .extended_chunk_delta_tool_call import ExtendedChunkDeltaToolCall @@ -76,6 +86,7 @@ from .get_me_subject import GetMeSubject from .get_model_provider_catalog_response import GetModelProviderCatalogResponse from .get_model_provider_response import GetModelProviderResponse + from .get_sandbox_environment_response import GetSandboxEnvironmentResponse from .get_sandbox_provider_catalog_response import GetSandboxProviderCatalogResponse from .get_sandbox_provider_response import GetSandboxProviderResponse from .get_schedule_response import GetScheduleResponse @@ -100,6 +111,7 @@ from .list_model_providers_response import ListModelProvidersResponse from .list_permissions_data import ListPermissionsData from .list_permissions_response import ListPermissionsResponse + from .list_sandbox_environments_response import ListSandboxEnvironmentsResponse from .list_schedule_runs_response import ListScheduleRunsResponse from .list_schedules_response import ListSchedulesResponse from .list_session_events_response import ListSessionEventsResponse @@ -163,6 +175,8 @@ from .sandbox_capability import SandboxCapability from .sandbox_config import SandboxConfig from .sandbox_created_event import SandboxCreatedEvent + from .sandbox_environment import SandboxEnvironment + from .sandbox_environment_manifest import SandboxEnvironmentManifest from .sandbox_provider_manifest import SandboxProviderManifest from .schedule import Schedule from .schedule_manifest import ScheduleManifest @@ -295,8 +309,18 @@ "CreatedBySubject": ".created_by_subject", "CronExpression": ".cron_expression", "CustomModelProvider": ".custom_model_provider", + "DaytonaDockerImage": ".daytona_docker_image", + "DaytonaGpuType": ".daytona_gpu_type", + "DaytonaSandboxEnvironmentImage": ".daytona_sandbox_environment_image", + "DaytonaSandboxEnvironmentLifecycle": ".daytona_sandbox_environment_lifecycle", + "DaytonaSandboxEnvironmentNetworking": ".daytona_sandbox_environment_networking", + "DaytonaSandboxEnvironmentResources": ".daytona_sandbox_environment_resources", + "DaytonaSandboxEnvironmentResourcesGpuType": ".daytona_sandbox_environment_resources_gpu_type", "DaytonaSandboxProviderAuth": ".daytona_sandbox_provider_auth", + "DaytonaSnapshotImage": ".daytona_snapshot_image", + "DaytonaTrueforgeDefaultImage": ".daytona_trueforge_default_image", "DeleteAgentResponse": ".delete_agent_response", + "DeleteSandboxEnvironmentResponse": ".delete_sandbox_environment_response", "DeleteScheduleResponse": ".delete_schedule_response", "DynamicSubAgentsConfig": ".dynamic_sub_agents_config", "ExtendedChunkDeltaToolCall": ".extended_chunk_delta_tool_call", @@ -314,6 +338,7 @@ "GetMeSubject": ".get_me_subject", "GetModelProviderCatalogResponse": ".get_model_provider_catalog_response", "GetModelProviderResponse": ".get_model_provider_response", + "GetSandboxEnvironmentResponse": ".get_sandbox_environment_response", "GetSandboxProviderCatalogResponse": ".get_sandbox_provider_catalog_response", "GetSandboxProviderResponse": ".get_sandbox_provider_response", "GetScheduleResponse": ".get_schedule_response", @@ -338,6 +363,7 @@ "ListModelProvidersResponse": ".list_model_providers_response", "ListPermissionsData": ".list_permissions_data", "ListPermissionsResponse": ".list_permissions_response", + "ListSandboxEnvironmentsResponse": ".list_sandbox_environments_response", "ListScheduleRunsResponse": ".list_schedule_runs_response", "ListSchedulesResponse": ".list_schedules_response", "ListSessionEventsResponse": ".list_session_events_response", @@ -401,6 +427,8 @@ "SandboxCapability": ".sandbox_capability", "SandboxConfig": ".sandbox_config", "SandboxCreatedEvent": ".sandbox_created_event", + "SandboxEnvironment": ".sandbox_environment", + "SandboxEnvironmentManifest": ".sandbox_environment_manifest", "SandboxProviderManifest": ".sandbox_provider_manifest", "Schedule": ".schedule", "ScheduleManifest": ".schedule_manifest", @@ -557,8 +585,18 @@ def __dir__(): "CreatedBySubject", "CronExpression", "CustomModelProvider", + "DaytonaDockerImage", + "DaytonaGpuType", + "DaytonaSandboxEnvironmentImage", + "DaytonaSandboxEnvironmentLifecycle", + "DaytonaSandboxEnvironmentNetworking", + "DaytonaSandboxEnvironmentResources", + "DaytonaSandboxEnvironmentResourcesGpuType", "DaytonaSandboxProviderAuth", + "DaytonaSnapshotImage", + "DaytonaTrueforgeDefaultImage", "DeleteAgentResponse", + "DeleteSandboxEnvironmentResponse", "DeleteScheduleResponse", "DynamicSubAgentsConfig", "ExtendedChunkDeltaToolCall", @@ -576,6 +614,7 @@ def __dir__(): "GetMeSubject", "GetModelProviderCatalogResponse", "GetModelProviderResponse", + "GetSandboxEnvironmentResponse", "GetSandboxProviderCatalogResponse", "GetSandboxProviderResponse", "GetScheduleResponse", @@ -600,6 +639,7 @@ def __dir__(): "ListModelProvidersResponse", "ListPermissionsData", "ListPermissionsResponse", + "ListSandboxEnvironmentsResponse", "ListScheduleRunsResponse", "ListSchedulesResponse", "ListSessionEventsResponse", @@ -663,6 +703,8 @@ def __dir__(): "SandboxCapability", "SandboxConfig", "SandboxCreatedEvent", + "SandboxEnvironment", + "SandboxEnvironmentManifest", "SandboxProviderManifest", "Schedule", "ScheduleManifest", diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/daytona_docker_image.py b/python/trueforge_sdk/src/trueforge_sdk/types/daytona_docker_image.py new file mode 100644 index 000000000..e79bb5d30 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/daytona_docker_image.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class DaytonaDockerImage(UncheckedBaseModel): + ref: str = pydantic.Field() + """ + Container image reference passed to Daytona create-from-image. + """ + + type: typing.Literal["docker"] = "docker" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/daytona_gpu_type.py b/python/trueforge_sdk/src/trueforge_sdk/types/daytona_gpu_type.py new file mode 100644 index 000000000..de3da4844 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/daytona_gpu_type.py @@ -0,0 +1,50 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core import enum + +T_Result = typing.TypeVar("T_Result") + + +class DaytonaGpuType(enum.StrEnum): + """ + Preferred Daytona GPU type. + """ + + H100 = "H100" + H200 = "H200" + RTX_PRO6000 = "RTX-PRO-6000" + RTX4090 = "RTX-4090" + RTX5090 = "RTX-5090" + _UNKNOWN = "__DAYTONAGPUTYPE_UNKNOWN__" + """ + This member is used for forward compatibility. If the value is not recognized by the enum, it will be stored here, and the raw value is accessible through `.value`. + """ + + @classmethod + def _missing_(cls, value: typing.Any) -> "DaytonaGpuType": + unknown = cls._UNKNOWN + unknown._value_ = value + return unknown + + def visit( + self, + h100: typing.Callable[[], T_Result], + h200: typing.Callable[[], T_Result], + rtx_pro6000: typing.Callable[[], T_Result], + rtx4090: typing.Callable[[], T_Result], + rtx5090: typing.Callable[[], T_Result], + _unknown_member: typing.Callable[[str], T_Result], + ) -> T_Result: + if self is DaytonaGpuType.H100: + return h100() + if self is DaytonaGpuType.H200: + return h200() + if self is DaytonaGpuType.RTX_PRO6000: + return rtx_pro6000() + if self is DaytonaGpuType.RTX4090: + return rtx4090() + if self is DaytonaGpuType.RTX5090: + return rtx5090() + return _unknown_member(self._value_) diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/daytona_sandbox_environment_image.py b/python/trueforge_sdk/src/trueforge_sdk/types/daytona_sandbox_environment_image.py new file mode 100644 index 000000000..ca966c3a6 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/daytona_sandbox_environment_image.py @@ -0,0 +1,9 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .daytona_docker_image import DaytonaDockerImage +from .daytona_snapshot_image import DaytonaSnapshotImage +from .daytona_trueforge_default_image import DaytonaTrueforgeDefaultImage + +DaytonaSandboxEnvironmentImage = typing.Union[DaytonaDockerImage, DaytonaSnapshotImage, DaytonaTrueforgeDefaultImage] diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/daytona_sandbox_environment_lifecycle.py b/python/trueforge_sdk/src/trueforge_sdk/types/daytona_sandbox_environment_lifecycle.py new file mode 100644 index 000000000..245d1cccc --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/daytona_sandbox_environment_lifecycle.py @@ -0,0 +1,32 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class DaytonaSandboxEnvironmentLifecycle(UncheckedBaseModel): + auto_archive_interval_in_minutes: typing.Optional[int] = pydantic.Field(default=None) + """ + Minutes before Daytona auto-archives the sandbox (0 disables). + """ + + auto_delete_interval_in_minutes: typing.Optional[int] = pydantic.Field(default=None) + """ + Minutes before Daytona auto-deletes the sandbox (0 disables). + """ + + auto_stop_interval_in_minutes: typing.Optional[int] = pydantic.Field(default=None) + """ + Minutes of idle time before Daytona auto-stops the sandbox (0 disables). + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/daytona_sandbox_environment_networking.py b/python/trueforge_sdk/src/trueforge_sdk/types/daytona_sandbox_environment_networking.py new file mode 100644 index 000000000..6b64599dc --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/daytona_sandbox_environment_networking.py @@ -0,0 +1,37 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class DaytonaSandboxEnvironmentNetworking(UncheckedBaseModel): + domain_allow_list: typing.Optional[str] = pydantic.Field(default=None) + """ + Comma-separated allowed domains. + """ + + network_allow_list: typing.Optional[str] = pydantic.Field(default=None) + """ + Comma-separated allowed CIDR network addresses. + """ + + network_block_all: typing.Optional[bool] = pydantic.Field(default=None) + """ + Block all outbound network access. + """ + + outbound_proxy_url: typing.Optional[str] = pydantic.Field(default=None) + """ + Outbound HTTP(S) proxy URL. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/daytona_sandbox_environment_resources.py b/python/trueforge_sdk/src/trueforge_sdk/types/daytona_sandbox_environment_resources.py new file mode 100644 index 000000000..265b378e4 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/daytona_sandbox_environment_resources.py @@ -0,0 +1,43 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .daytona_sandbox_environment_resources_gpu_type import DaytonaSandboxEnvironmentResourcesGpuType + + +class DaytonaSandboxEnvironmentResources(UncheckedBaseModel): + cpu: typing.Optional[float] = pydantic.Field(default=None) + """ + CPU allocation in cores. + """ + + disk: typing.Optional[float] = pydantic.Field(default=None) + """ + Disk allocation in GiB. + """ + + gpu: typing.Optional[float] = pydantic.Field(default=None) + """ + GPU allocation in Daytona GPU units. + """ + + gpu_type: typing.Optional[DaytonaSandboxEnvironmentResourcesGpuType] = pydantic.Field(default=None) + """ + Preferred GPU type, or an ordered fallback list. + """ + + memory: typing.Optional[float] = pydantic.Field(default=None) + """ + Memory allocation in GiB. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/daytona_sandbox_environment_resources_gpu_type.py b/python/trueforge_sdk/src/trueforge_sdk/types/daytona_sandbox_environment_resources_gpu_type.py new file mode 100644 index 000000000..f0589953a --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/daytona_sandbox_environment_resources_gpu_type.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .daytona_gpu_type import DaytonaGpuType + +DaytonaSandboxEnvironmentResourcesGpuType = typing.Union[DaytonaGpuType, typing.List[DaytonaGpuType]] diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/daytona_snapshot_image.py b/python/trueforge_sdk/src/trueforge_sdk/types/daytona_snapshot_image.py new file mode 100644 index 000000000..06c1be73f --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/daytona_snapshot_image.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class DaytonaSnapshotImage(UncheckedBaseModel): + name: str = pydantic.Field() + """ + Daytona snapshot name. + """ + + type: typing.Literal["snapshot"] = "snapshot" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/daytona_trueforge_default_image.py b/python/trueforge_sdk/src/trueforge_sdk/types/daytona_trueforge_default_image.py new file mode 100644 index 000000000..064b102d3 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/daytona_trueforge_default_image.py @@ -0,0 +1,19 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class DaytonaTrueforgeDefaultImage(UncheckedBaseModel): + type: typing.Literal["trueforge-default"] = "trueforge-default" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/delete_sandbox_environment_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/delete_sandbox_environment_response.py new file mode 100644 index 000000000..11de44ee7 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/delete_sandbox_environment_response.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class DeleteSandboxEnvironmentResponse(UncheckedBaseModel): + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/get_sandbox_environment_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/get_sandbox_environment_response.py new file mode 100644 index 000000000..9bb4ff8b9 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/get_sandbox_environment_response.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .sandbox_environment import SandboxEnvironment + + +class GetSandboxEnvironmentResponse(UncheckedBaseModel): + data: SandboxEnvironment + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/list_permissions_data.py b/python/trueforge_sdk/src/trueforge_sdk/types/list_permissions_data.py index ea978b7fc..b14db41f4 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/types/list_permissions_data.py +++ b/python/trueforge_sdk/src/trueforge_sdk/types/list_permissions_data.py @@ -12,7 +12,7 @@ class ListPermissionsData(UncheckedBaseModel): permissions: typing.Dict[str, typing.List[ResourcePermission]] = pydantic.Field() """ - For agent/schedule/session: keyed by resource id. For tenant: keyed by entity kind (e.g. `agent` → `CREATE`). + For agent/schedule/session: keyed by resource id. For tenant: keyed by entity kind (e.g. `agent` or `sandbox-environment` → `CREATE`). """ type: PermissionResourceType diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/list_sandbox_environments_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/list_sandbox_environments_response.py new file mode 100644 index 000000000..1f93f47e8 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/list_sandbox_environments_response.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .sandbox_environment import SandboxEnvironment +from .token_pagination import TokenPagination + + +class ListSandboxEnvironmentsResponse(UncheckedBaseModel): + data: typing.List[SandboxEnvironment] + pagination: TokenPagination + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/sandbox_config.py b/python/trueforge_sdk/src/trueforge_sdk/types/sandbox_config.py index d3e7b3ae5..acc99c7e9 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/types/sandbox_config.py +++ b/python/trueforge_sdk/src/trueforge_sdk/types/sandbox_config.py @@ -13,6 +13,11 @@ class SandboxConfig(UncheckedBaseModel): Give the agent a sandbox. Required for skills and Code Mode. """ + environment: typing.Optional[str] = pydantic.Field(default=None) + """ + Caller-owned sandbox environment name. Omit to use the tenant provider defaults. + """ + file_downloads: typing.Optional[bool] = pydantic.Field(default=True) """ Allow downloading agent-produced files via the turn download endpoint. Default: true. diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/sandbox_environment.py b/python/trueforge_sdk/src/trueforge_sdk/types/sandbox_environment.py new file mode 100644 index 000000000..3638c6d7b --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/sandbox_environment.py @@ -0,0 +1,44 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .created_by_subject import CreatedBySubject +from .resource_name import ResourceName +from .sandbox_environment_manifest import SandboxEnvironmentManifest + + +class SandboxEnvironment(UncheckedBaseModel): + created_at: dt.datetime = pydantic.Field() + """ + ISO-8601 create time. + """ + + created_by_subject: CreatedBySubject + description: typing.Optional[str] = pydantic.Field(default=None) + """ + Optional human-readable description. + """ + + id: str = pydantic.Field() + """ + Immutable server-generated environment identifier. + """ + + manifest: SandboxEnvironmentManifest + name: ResourceName + updated_at: dt.datetime = pydantic.Field() + """ + ISO-8601 last update time. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/sandbox_environment_manifest.py b/python/trueforge_sdk/src/trueforge_sdk/types/sandbox_environment_manifest.py new file mode 100644 index 000000000..4863e6a02 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/sandbox_environment_manifest.py @@ -0,0 +1,40 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .daytona_sandbox_environment_image import DaytonaSandboxEnvironmentImage +from .daytona_sandbox_environment_lifecycle import DaytonaSandboxEnvironmentLifecycle +from .daytona_sandbox_environment_networking import DaytonaSandboxEnvironmentNetworking +from .daytona_sandbox_environment_resources import DaytonaSandboxEnvironmentResources + + +class SandboxEnvironmentManifest(UncheckedBaseModel): + image: DaytonaSandboxEnvironmentImage + lifecycle: typing.Optional[DaytonaSandboxEnvironmentLifecycle] = None + networking: typing.Optional[DaytonaSandboxEnvironmentNetworking] = None + provider: typing.Literal["daytona"] = pydantic.Field(default="daytona") + """ + Must match the configured sandbox provider name/type. + """ + + resources: typing.Optional[DaytonaSandboxEnvironmentResources] = None + secrets: typing.Optional[typing.Dict[str, str]] = pydantic.Field(default=None) + """ + Map of sandbox env var name to an existing Daytona organization secret name. + """ + + type: typing.Literal["daytona"] = pydantic.Field(default="daytona") + """ + Daytona sandbox environment. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow